/**
 * @bobfrankston/mailx-server
 * Express server: serves client, mounts API, manages WebSocket push.
 */

import express from "express";
import { WebSocketServer, WebSocket } from "ws";
import * as path from "node:path";
import * as fs from "node:fs";
import { MailxDB, FileMessageStore, Store } from "@bobfrankston/mailx-store";
import { ImapManager } from "@bobfrankston/mailx-imap";
import { createApiRouter } from "@bobfrankston/mailx-api";
import { loadSettings, loadAccountsAsync, getConfigDir, getStorePath, getSharedDir, getStorageInfo } from "@bobfrankston/mailx-settings";
import type { WsEvent } from "@bobfrankston/mailx-types";
import { ports } from "@bobfrankston/miscinfo";
import { createServer } from "node:http";

const PORT = ports.mailx;

// ── File logging ──
const logDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail", "logs");
fs.mkdirSync(logDir, { recursive: true });
// Rotate: delete logs older than 7 days
try {
    for (const f of fs.readdirSync(logDir).filter(f => f.startsWith("mailx-") && f.endsWith(".log"))) {
        const stat = fs.statSync(path.join(logDir, f));
        if (Date.now() - stat.mtimeMs > 7 * 86400000) fs.unlinkSync(path.join(logDir, f));
    }
} catch { /* ignore cleanup errors */ }
const logDate = new Date().toISOString().slice(0, 10);
const logPath = path.join(logDir, `mailx-${logDate}.log`);
const logStream = fs.createWriteStream(logPath, { flags: "a" });
const origLog = console.log;
const origErr = console.error;
function logTs(): string { return new Date().toLocaleString("se", { hour12: false }); }
console.log = (...args: any[]) => {
    const msg = `${logTs()} ${args.join(" ")}`;
    origLog(msg);
    logStream.write(msg + "\n");
};
console.error = (...args: any[]) => {
    const msg = `${logTs()} ERROR ${args.join(" ")}`;
    origErr(msg);
    logStream.write(msg + "\n");
};
// Read version from root package.json (the published version)
const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
const SERVER_VERSION = rootPkg.version;

// ── Initialize ──
// No initLocalConfig() here — config is created when user adds an account (initCloudConfig).
// On fresh install, loadSettings() returns defaults and the setup form handles configuration.

let settings = loadSettings();
if (settings.accounts.length === 0) {
    // Try cloud API fallback (Google Drive, OneDrive) if filesystem mount not available
    const cloudAccounts = await loadAccountsAsync();
    if (cloudAccounts.length > 0) {
        settings = { ...settings, accounts: cloudAccounts };
        console.log(`  Loaded ${cloudAccounts.length} account(s) from cloud API`);
    } else {
        console.log("  No accounts configured. Open http://127.0.0.1:9333 to add your email account.");
    }
}
const dbDir = getConfigDir();
const db = new MailxDB(dbDir);
import { NodeTcpTransport } from "@bobfrankston/node-tcp-transport";
const store = new Store(db, new FileMessageStore(getStorePath()));
const imapManager = new ImapManager(store, () => new NodeTcpTransport());

// ── Express App ──

const app = express();
app.use(express.json({ limit: "Infinity" }));

// Optional token gate — required whenever the server binds to anything other
// than a loopback address. Set via `MAILX_SERVER_TOKEN=<secret>` (or the
// shorter `MAILX_TOKEN`). When the server is loopback-only (the default), the
// gate is a no-op since nothing outside the machine can reach it. When bound
// externally (`MAILX_SERVER_HOST=0.0.0.0` or `--external`), connections must
// present the token in either `?t=<token>` or the `x-mailx-token` header. No
// token configured + external bind → server refuses to start (safer than
// serving open).
const SERVER_TOKEN = process.env.MAILX_SERVER_TOKEN || process.env.MAILX_TOKEN || "";
const SERVER_HOST = process.env.MAILX_SERVER_HOST || "";
const IS_EXTERNAL_BIND = SERVER_HOST && SERVER_HOST !== "127.0.0.1" && SERVER_HOST !== "localhost" && SERVER_HOST !== "::1";
if (IS_EXTERNAL_BIND && !SERVER_TOKEN) {
    console.error("[server] Refusing to bind externally without MAILX_SERVER_TOKEN. Set the env var or drop the external host.");
    process.exit(2);
}
app.use((req, res, next) => {
    if (!IS_EXTERNAL_BIND) return next();
    // Allow the bare static file fetch so the login page can render; every
    // /api/* path requires the token.
    if (!req.path.startsWith("/api/")) return next();
    const provided = (req.query.t as string) || req.header("x-mailx-token") || "";
    if (provided !== SERVER_TOKEN) {
        res.status(401).json({ error: "missing or invalid token" });
        return;
    }
    next();
});

// Request logging
app.use((req, res, next) => {
    const start = Date.now();
    res.on("finish", () => {
        const ms = Date.now() - start;
        // Skip noisy polling endpoints
        if (req.path.endsWith("/sync/pending")) return;
        console.log(`  ${req.method} ${req.path} ${res.statusCode} ${ms}ms`);
    });
    next();
});

// Serve client static files
const clientDir = path.join(import.meta.dirname, "..", "..", "client");
const rootDir = path.join(import.meta.dirname, "..", "..");
app.use(express.static(clientDir, { etag: false, lastModified: false, maxAge: 0 }));
app.use("/node_modules", express.static(path.join(rootDir, "node_modules"), { etag: false, lastModified: false, maxAge: 0 }));

// Mount API
const apiRouter = createApiRouter(store, imapManager);
app.use("/api", apiRouter);

app.get("/api/version", (req, res) => {
    const storage = getStorageInfo();
    const imapAccounts = imapManager.getAccountCount();
    const dbAccounts = db.getAccounts().length;
    // Warn if DB has accounts but IMAP has none — stale DB, settings missing
    const settingsError = (dbAccounts > 0 && imapAccounts === 0)
        ? "No accounts loaded from settings — showing stale data. Check accounts.jsonc on your cloud drive."
        : undefined;
    res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage, imapAccounts, hasAccounts: imapAccounts > 0 || dbAccounts > 0, settingsError });
});

app.all("/info", (req, res) => {
    res.json({ version: SERVER_VERSION, uptime: Math.round(process.uptime()), port: PORT, imap: imapManager.useNativeClient ? "native" : "imapflow" });
});

app.get("/status", (req, res) => {
    const accounts = db.getAccounts();
    const pendingSync = db.getTotalPendingSyncCount();
    const uptime = process.uptime();
    const mem = process.memoryUsage();

    const accountInfo = accounts.map((a: any) => {
        const folders = db.getFolders(a.id);
        const inbox = folders.find((f: any) => f.specialUse === "inbox");
        return {
            id: a.id, name: a.name, folders: folders.length,
            inboxTotal: inbox?.totalCount || 0,
            inboxUnread: inbox?.unreadCount || 0,
            lastSync: a.lastSync ? new Date(a.lastSync).toLocaleString() : "never",
        };
    });

    res.type("html").send(`<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>mailx status</title>
<meta http-equiv="refresh" content="10">
<style>body{font-family:system-ui;padding:2rem;max-width:600px;color:#cdd6f4;background:#1e1e2e}
table{border-collapse:collapse;width:100%}td,th{text-align:left;padding:4px 12px;border-bottom:1px solid #333}
h1{font-size:1.2rem}h2{font-size:1rem;margin-top:1.5rem}.ok{color:#a6e3a1}.warn{color:#f9e2af}
a{color:#89b4fa}</style></head>
<body>
<h1>mailx status</h1>
<p>mailx v${SERVER_VERSION}</p>
<p>Uptime: ${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m | Memory: ${Math.round(mem.rss / 1048576)} MB</p>
<p>Pending sync: <span class="${pendingSync > 0 ? "warn" : "ok"}">${pendingSync}</span></p>
<h2>Accounts</h2>
<table><tr><th>Account</th><th>Folders</th><th>Inbox</th><th>Unread</th><th>Last Sync</th></tr>
${accountInfo.map((a: any) => `<tr><td>${a.name}</td><td>${a.folders}</td><td>${a.inboxTotal}</td><td>${a.inboxUnread}</td><td>${a.lastSync}</td></tr>`).join("")}
</table>
<p style="margin-top:2rem;font-size:0.8rem"><a href="/">Open mailx</a> | Auto-refreshes every 10s</p>
</body></html>`);
});

// Graceful exit — close IMAP, DB, HTTP then exit
app.all("/api/exit", (req, res) => {
    res.json({ ok: true });
    setTimeout(() => shutdown(), 100);
});

// Restart server + reload clients
app.post("/api/restart", (req, res) => {
    res.json({ ok: true });
    broadcast({ type: "reload" });
    setTimeout(async () => {
        console.log("  Restart requested via API");
        await shutdown();
    }, 500);
});

// Rebuild: wipe DB + message store, keep accounts/settings, restart
app.post("/api/rebuild", (req, res) => {
    res.json({ ok: true });
    broadcast({ type: "reload" });
    setTimeout(async () => {
        console.log("  Rebuild requested — wiping DB and message store...");
        imapManager.stopPeriodicSync();
        try { await imapManager.shutdown(); } catch { /* proceed */ }
        db.close();

        // Remove DB files
        const dbDir = getConfigDir();
        for (const f of ["mailx.db", "mailx.db-wal", "mailx.db-shm"]) {
            const p = path.join(dbDir, f);
            if (fs.existsSync(p)) { fs.unlinkSync(p); console.log(`  Deleted ${f}`); }
        }

        // Remove message store
        const storePath = getStorePath();
        if (fs.existsSync(storePath)) {
            fs.rmSync(storePath, { recursive: true });
            console.log(`  Deleted ${storePath}`);
        }

        console.log("  Rebuild complete — restarting...");
        server?.close();
        process.exit(0);
    }, 500);
});

// SPA fallback
app.get("*", (req, res) => {
    if (!req.path.startsWith("/api"))
        res.sendFile(path.join(clientDir, "index.html"));
});

// JSON error handler — all errors return JSON, never HTML
app.use((err: any, _req: any, res: any, _next: any) => {
    console.error(`ERROR ${err.message}`);
    const status = err.status || err.statusCode || 500;
    res.status(status).json({ error: err.message || "Internal server error" });
});

// ── HTTP Server ──

let server: ReturnType<typeof createServer>;
let wss: WebSocketServer;

const clients = new Set<WebSocket>();

function wireWebSocket(): void {
    wss.on("connection", (ws) => {
        clients.add(ws);
        (ws as any).__alive = true;
        const connected: WsEvent = { type: "connected" };
        ws.send(JSON.stringify(connected));

        ws.on("pong", () => { (ws as any).__alive = true; });
        ws.on("close", () => clients.delete(ws));
    });

    // Heartbeat — detect dead connections, prevent CLOSE_WAIT zombie accumulation
    setInterval(() => {
        for (const ws of clients) {
            if ((ws as any).__alive === false) {
                clients.delete(ws);
                ws.terminate();
                continue;
            }
            (ws as any).__alive = false;
            ws.ping();
        }
    }, 30000);
}

function broadcast(event: WsEvent): void {
    const data = JSON.stringify(event);
    for (const ws of clients) {
        if (ws.readyState === WebSocket.OPEN)
            ws.send(data);
    }
}

// Wire IMAP events to WebSocket broadcasts
imapManager.on("syncProgress", (accountId: string, phase: string, progress: number) => {
    broadcast({ type: "syncProgress", accountId, phase, progress });
});

imapManager.on("folderCountsChanged", (accountId: string, counts: Record<number, { total: number; unread: number }>) => {
    broadcast({ type: "folderCountsChanged", accountId, counts });
});

imapManager.on("folderSynced", (accountId: string, folderId: number, syncedAt: number) => {
    broadcast({ type: "folderSynced", accountId, entries: [{ folderId, syncedAt }] });
});

imapManager.on("syncError", (accountId: string, error: string) => {
    broadcast({ type: "error", message: `${accountId}: ${error}` });
});

imapManager.on("accountError", (accountId: string, error: string, hint: string, isOAuth: boolean) => {
    broadcast({ type: "accountError", accountId, error, hint, isOAuth });
});

// Local-first: body fetch lifecycle for the HTTP/WebSocket dev mode.
// Mirrors the bin/mailx.ts IPC forwarders so a click on an uncached row
// gets `cached:false` envelope back instantly and the body lands via a
// WebSocket event instead of a per-click await.
imapManager.on("bodyAvailable", (payload: any) => {
    broadcast({ type: "bodyAvailable", ...payload });
});
imapManager.on("bodyFetchError", (payload: any) => {
    broadcast({ type: "bodyFetchError", ...payload });
});
imapManager.on("messageRemoved", (payload: any) => {
    broadcast({ type: "messageRemoved", ...payload });
});

// ── Startup ──

async function start(): Promise<void> {
    console.log("rmfmail server starting...");

    // Start HTTP server FIRST so UI is always reachable (even during IMAP startup)
    const externalAccess = process.argv.includes("--external");
    const hostname = externalAccess ? "0.0.0.0" : "127.0.0.1";

    // Retry listen with backoff — Windows CLOSE_WAIT zombies can hold the port for minutes after a crash
    for (let attempt = 0; attempt < 30; attempt++) {
        server = createServer(app);
        // Prevent CLOSE_WAIT accumulation: short keepAlive timeout + connection close headers
        server.keepAliveTimeout = 5000;   // close idle keep-alive connections after 5s
        server.headersTimeout = 10000;    // kill connections with no headers after 10s
        // Track connections for clean shutdown (prevents CLOSE_WAIT zombies on Windows)
        server.on("connection", (conn) => {
            openConnections.add(conn);
            conn.on("close", () => openConnections.delete(conn));
        });
        // Suppress EADDRINUSE from bubbling to uncaughtException — we handle it here
        server.on("error", () => {}); // will be replaced by listen handler below
        wss = new WebSocketServer({ server });
        wireWebSocket();

        const listenResult = await new Promise<"ok" | string>((resolve) => {
            server.removeAllListeners("error");
            server.once("error", (e: any) => { resolve(e.code || e.message); });
            server.listen({ port: PORT, host: hostname, exclusive: false }, () => {
                server.removeAllListeners("error");
                resolve("ok");
            });
        });
        if (listenResult === "ok") break;
        if (listenResult === "EADDRINUSE" && attempt < 29) {
            const wait = Math.min(2000 + attempt * 1000, 10000);
            console.log(`  Port ${PORT} in use (CLOSE_WAIT zombies?) — retry ${attempt + 1}/30 in ${wait / 1000}s...`);
            server.close();
            await new Promise(r => setTimeout(r, wait));
        } else {
            throw new Error(`Cannot bind port ${PORT}: ${listenResult}`);
        }
    }
    console.log(`rmfmail server running on http://${hostname}:${PORT}`);

    // Seed contacts — async + chunked, runs in the background.
    db.seedContactsFromMessages()
        .then(seeded => { if (seeded > 0) console.log(`  Seeded ${seeded} contacts`); })
        .catch(e => console.error(`  contact seed failed: ${e?.message || e}`));

    // Search index — rebuild in background (non-blocking)
    setTimeout(() => {
        let ftsCount = 0;
        try { ftsCount = ((db as any).db.prepare("SELECT COUNT(*) as cnt FROM messages_fts").get() as any)?.cnt || 0; } catch { /* */ }
        if (ftsCount === 0) {
            const indexed = db.rebuildSearchIndex();
            if (indexed > 0) console.log(`  Search index: ${indexed} messages`);
        }
    }, 5000);

    // Add configured accounts
    for (const account of settings.accounts) {
        if (!account.enabled && !(account as any).syncContacts) continue;
        try {
            await imapManager.addAccount(account);
            console.log(`  Account added: ${account.name} (${account.id})${!account.enabled ? " [contacts only]" : ""}`);
        } catch (e: any) {
            console.error(`  Failed to add account ${account.id}: ${e.message}`);
        }
    }

    // Initial sync, then start IDLE watchers
    if (settings.accounts.some(a => a.enabled)) {
        console.log("  Starting initial sync...");
        imapManager.syncAll().then(async () => {
            console.log("  Initial sync complete");
            // Start IDLE watchers for instant new-mail notifications
            await imapManager.startWatching();
            // Sync Google Contacts for OAuth accounts
            imapManager.syncAllContacts().catch(e =>
                console.error(`  Google Contacts sync error: ${e.message}`)
            );
        }).catch(e => {
            console.error(`  Initial sync error: ${e.message}`);
        });
    }

    // Periodic sync as fallback (IDLE handles most updates)
    imapManager.startPeriodicSync(settings.sync.intervalMinutes);

    // Outbox worker — processes queued outgoing messages
    imapManager.startOutboxWorker();
}

// ── Graceful Shutdown ──

/** Track all open connections so we can destroy them on shutdown (prevents CLOSE_WAIT zombies) */
const openConnections = new Set<import("node:net").Socket>();

async function shutdown(): Promise<void> {
    console.log("\nShutting down...");
    const forceExit = setTimeout(() => { console.log("Force exit"); process.exit(1); }, 3000);
    imapManager.stopPeriodicSync();
    try { await imapManager.shutdown(); } catch { /* proceed */ }
    db.close();
    // Destroy all open connections immediately — prevents CLOSE_WAIT zombies on Windows
    for (const conn of openConnections) {
        conn.destroy();
    }
    openConnections.clear();
    server?.close();
    clearTimeout(forceExit);
    process.exit(0);
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("unhandledRejection", (err: any) => {
    console.error("Unhandled rejection:", err?.message || err);
});
process.on("uncaughtException", (err) => {
    console.error("FATAL uncaught exception:", err.message);
    console.error(err.stack);
    // Other exceptions: stay alive, let node --watch handle file-change restarts
});
process.on("exit", (code) => {
    console.log(`Process exiting with code ${code}`);
});

// ── Entry Point ──

await start();

export { app, start };
