import "dotenv/config";
import express from "express";
import { readFile } from "fs/promises";
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync as fsReadFileSync, } from "fs";
import { dirname, join } from "path";
import { fileURLToPath, pathToFileURL } from "url";
import { onShutdown } from "./bin/lib/platform.js";
import { ensureDefaultSession } from "./lib/sessionStore.js";
import { startGrokProxy } from "./lib/grokProxyLauncher.js";
import { startOAuthProxy } from "./lib/oauthLauncher.js";
import { migrateGeneratedStorage } from "./lib/storageMigration.js";
import { purgeStaleJobs } from "./lib/inflight.js";
import { configureLogger } from "./lib/logger.js";
import { createRequestLogger } from "./lib/requestLogger.js";
import { configureApiCachePolicy } from "./lib/apiCachePolicy.js";
import { configureRoutes } from "./routes/index.js";
import { config } from "./config.js";
import { getServerPort, listenWithPortFallback } from "./lib/runtimePorts.js";
import { closeDb } from "./lib/db.js";
import { stopAgentQueueWorker } from "./lib/agentQueueWorker.js";
import { reapCardNewsJobs } from "./lib/cardNewsJobStore.js";
import { reapTerminalJobs } from "./lib/inflight.js";
import { errInfo } from "./lib/errInfo.js";
const rootDir = dirname(fileURLToPath(import.meta.url));
async function loadApiKey() {
    if (process.env.OPENAI_API_KEY) {
        return { apiKey: process.env.OPENAI_API_KEY, apiKeySource: "env" };
    }
    const candidates = [
        config.storage.configFile,
        join(rootDir, ".ima2", "config.json"),
    ];
    for (const cfgPath of candidates) {
        if (!existsSync(cfgPath))
            continue;
        try {
            const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
            if (cfg.apiKey)
                return { apiKey: cfg.apiKey, apiKeySource: "config" };
        }
        catch { }
    }
    return { apiKey: null, apiKeySource: "none" };
}
async function loadXaiApiKey() {
    if (process.env.XAI_API_KEY) {
        return { apiKey: process.env.XAI_API_KEY, apiKeySource: "env" };
    }
    const candidates = [
        config.storage.configFile,
        join(rootDir, ".ima2", "config.json"),
    ];
    for (const cfgPath of candidates) {
        if (!existsSync(cfgPath))
            continue;
        try {
            const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
            if (cfg.xaiApiKey)
                return { apiKey: cfg.xaiApiKey, apiKeySource: "config" };
        }
        catch { }
    }
    return { apiKey: null, apiKeySource: "none" };
}
async function loadGeminiApiKey() {
    if (process.env.GEMINI_API_KEY) {
        return { apiKey: process.env.GEMINI_API_KEY, apiKeySource: "env" };
    }
    const candidates = [
        config.storage.configFile,
        join(rootDir, ".ima2", "config.json"),
    ];
    for (const cfgPath of candidates) {
        if (!existsSync(cfgPath))
            continue;
        try {
            const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
            if (cfg.geminiApiKey)
                return { apiKey: cfg.geminiApiKey, apiKeySource: "config" };
        }
        catch { }
    }
    return { apiKey: null, apiKeySource: "none" };
}
async function loadVertexKey() {
    const envJson = process.env.VERTEX_SERVICE_ACCOUNT_JSON;
    if (envJson) {
        try {
            const parsed = JSON.parse(envJson);
            return { json: envJson, projectId: parsed.project_id || null, source: "env" };
        }
        catch {
            return { json: null, projectId: null, source: "none" };
        }
    }
    const candidates = [
        config.storage.configFile,
        join(rootDir, ".ima2", "config.json"),
    ];
    for (const cfgPath of candidates) {
        if (!existsSync(cfgPath))
            continue;
        try {
            const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
            if (cfg.vertexServiceAccountJson) {
                const parsed = JSON.parse(cfg.vertexServiceAccountJson);
                return { json: cfg.vertexServiceAccountJson, projectId: parsed.project_id || null, source: "config" };
            }
        }
        catch { }
    }
    return { json: null, projectId: null, source: "none" };
}
async function loadGeminiAuthMode() {
    const candidates = [
        config.storage.configFile,
        join(rootDir, ".ima2", "config.json"),
    ];
    for (const cfgPath of candidates) {
        if (!existsSync(cfgPath))
            continue;
        try {
            const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
            if (cfg.geminiAuthMode === "vertex" || cfg.geminiAuthMode === "apikey")
                return cfg.geminiAuthMode;
        }
        catch { }
    }
    return undefined;
}
async function createOpenAI(apiKey) {
    if (!apiKey)
        return null;
    const OpenAI = (await import("openai")).default;
    return new OpenAI({ apiKey });
}
function readPackageVersion() {
    try {
        return JSON.parse(fsReadFileSync(join(rootDir, "package.json"), "utf-8")).version ?? "0.0.0";
    }
    catch {
        return "0.0.0";
    }
}
function setUiStaticHeaders(res, filePath) {
    const normalized = filePath.replace(/\\/g, "/");
    if (normalized.endsWith("/index.html")) {
        res.setHeader("Cache-Control", "no-store, max-age=0");
        return;
    }
    if (normalized.includes("/assets/")) {
        res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
    }
}
export function buildApp(ctx) {
    const app = express();
    configureApiCachePolicy(app);
    configureLogger({ level: ctx.config.log.level });
    app.use(createRequestLogger());
    app.use(express.json({ limit: ctx.config.server.bodyLimit }));
    app.use(express.static(join(ctx.rootDir, "ui", "dist"), {
        setHeaders: setUiStaticHeaders,
    }));
    app.use("/assets", (_req, res) => {
        res.status(404).type("text/plain").send("Asset not found");
    });
    app.use("/generated", (req, res, next) => {
        if (req.path.endsWith(".json"))
            return res.status(404).type("text/plain").send("Generated metadata is not public");
        return next();
    }, express.static(ctx.config.storage.generatedDir, {
        maxAge: ctx.config.storage.staticMaxAge,
        immutable: true,
    }));
    configureRoutes(app, ctx);
    return app;
}
function runtimeHostUrl(host) {
    if (!host || host === "0.0.0.0" || host === "::")
        return "localhost";
    return host;
}
function advertise(ctx) {
    try {
        mkdirSync(dirname(ctx.config.storage.advertiseFile), { recursive: true });
        writeFileSync(ctx.config.storage.advertiseFile, JSON.stringify({
            port: Number(ctx.serverActualPort || ctx.config.server.port),
            url: ctx.serverUrl,
            pid: process.pid,
            startedAt: ctx.startedAt,
            version: ctx.packageVersion,
            backend: {
                configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
                actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
                url: ctx.serverUrl,
            },
            oauth: {
                configuredPort: Number(ctx.oauthPort),
                actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
                url: ctx.oauthUrl,
                status: ctx.oauthReadyState,
            },
            grok: {
                configuredPort: Number(ctx.grokPort),
                actualPort: Number(ctx.grokActualPort || ctx.grokPort),
                url: ctx.grokUrl,
            },
        }));
    }
    catch (e) {
        const err = errInfo(e);
        console.warn("[advertise] skipped:", err.message);
    }
}
function unadvertise(ctx) {
    try {
        if (!existsSync(ctx.config.storage.advertiseFile))
            return;
        const cur = JSON.parse(fsReadFileSync(ctx.config.storage.advertiseFile, "utf-8"));
        if (cur.pid === process.pid)
            unlinkSync(ctx.config.storage.advertiseFile);
    }
    catch { }
}
export async function createRuntimeContext(overrides = {}) {
    const loadedKey = overrides.apiKey !== undefined
        ? {
            apiKey: overrides.apiKey,
            apiKeySource: overrides.apiKeySource ?? (overrides.apiKey ? "env" : "none"),
        }
        : await loadApiKey();
    const loadedXaiKey = await loadXaiApiKey();
    const loadedGeminiKey = await loadGeminiApiKey();
    const loadedVertexKey = await loadVertexKey();
    const geminiAuthMode = await loadGeminiAuthMode();
    const apiKey = loadedKey.apiKey;
    const openai = overrides.openai ?? await createOpenAI(apiKey);
    const oauthPort = config.oauth.proxyPort;
    const grokPort = config.grokProvider.proxyPort;
    let resolveOAuthReady = () => { };
    const oauthReadyPromise = new Promise((resolve) => {
        resolveOAuthReady = resolve;
    });
    const ctx = {
        rootDir,
        config,
        serverConfiguredPort: config.server.port,
        serverActualPort: undefined,
        serverUrl: `http://${runtimeHostUrl(config.server.host)}:${config.server.port}`,
        grokPort,
        grokActualPort: grokPort,
        grokUrl: `http://${config.grokProvider.proxyHost}:${grokPort}/v1`,
        oauthPort,
        oauthActualPort: oauthPort,
        oauthUrl: `http://127.0.0.1:${oauthPort}`,
        oauthReadyState: config.oauth.autoStart ? "starting" : "disabled",
        hasApiKey: !!apiKey,
        apiKey: apiKey ?? undefined,
        apiKeySource: loadedKey.apiKeySource,
        openai,
        startedAt: overrides.startedAt ?? Date.now(),
        packageVersion: overrides.packageVersion ?? readPackageVersion(),
        xaiApiKey: loadedXaiKey.apiKey ?? undefined,
        xaiApiKeySource: loadedXaiKey.apiKeySource,
        hasXaiApiKey: !!loadedXaiKey.apiKey,
        geminiApiKey: loadedGeminiKey.apiKey ?? undefined,
        geminiApiKeySource: loadedGeminiKey.apiKeySource,
        hasGeminiApiKey: !!loadedGeminiKey.apiKey,
        vertexServiceAccountJson: loadedVertexKey.json ?? undefined,
        vertexProjectId: loadedVertexKey.projectId ?? undefined,
        hasVertexKey: !!loadedVertexKey.json,
        geminiAuthMode,
        oauthReadyPromise: oauthReadyPromise,
        markGrokProxyPort: ({ url, port } = {}) => {
            if (port)
                ctx.grokActualPort = port;
            if (url)
                ctx.grokUrl = url;
            else if (port)
                ctx.grokUrl = `http://${ctx.config.grokProvider.proxyHost}:${port}/v1`;
        },
        markOAuthReady: ({ url, port } = {}) => {
            if (url)
                ctx.oauthUrl = url;
            if (port)
                ctx.oauthActualPort = port;
            ctx.oauthReadyState = "ready";
            resolveOAuthReady(ctx.oauthUrl);
        },
        markOAuthFailed: () => {
            ctx.oauthReadyState = "failed";
            resolveOAuthReady(null);
        },
    };
    if (!config.oauth.autoStart)
        ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
    if (loadedVertexKey.json) {
        try {
            const { initVertexAuth } = await import("./lib/vertexAuth.js");
            initVertexAuth(loadedVertexKey.json);
        }
        catch { /* vertex init failure is non-fatal */ }
    }
    return ctx;
}
export async function startServer(overrides = {}) {
    const ctx = await createRuntimeContext(overrides);
    await migrateGeneratedStorage(ctx);
    purgeStaleJobs();
    const app = buildApp(ctx);
    const oauthChild = overrides.oauthChild !== undefined
        ? overrides.oauthChild
        : !ctx.config.oauth.autoStart
            ? null
            : startOAuthProxy({
                oauthPort: ctx.oauthPort,
                restartDelayMs: ctx.config.oauth.restartDelayMs,
                onReady: ({ url, port }) => {
                    ctx.markOAuthReady({ url, port });
                    advertise(ctx);
                },
                onExit: () => ctx.markOAuthFailed(),
            });
    if (overrides.oauthChild !== undefined || !ctx.config.oauth.autoStart) {
        ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
    }
    const grokChild = ctx.config.grokProvider.autoStart
        ? await startGrokProxy({
            host: ctx.config.grokProvider.proxyHost,
            port: ctx.config.grokProvider.proxyPort,
            restartDelayMs: ctx.config.grokProvider.restartDelayMs,
            onPortSelected: ({ url, port }) => {
                ctx.markGrokProxyPort({ url, port });
                advertise(ctx);
            },
            onReady: ({ url, port }) => {
                ctx.markGrokProxyPort({ url, port });
                advertise(ctx);
            },
        })
        : null;
    let server;
    let reapTimer;
    onShutdown(async () => {
        unadvertise(ctx);
        try {
            oauthChild?.stop?.();
        }
        catch { }
        try {
            oauthChild?.kill?.();
        }
        catch { }
        try {
            grokChild?.stop?.();
        }
        catch { }
        try {
            grokChild?.kill?.();
        }
        catch { }
        stopAgentQueueWorker();
        clearInterval(reapTimer);
        await new Promise((resolve) => {
            if (server)
                server.close(() => resolve());
            else
                resolve();
        });
        closeDb();
    });
    process.on("exit", () => unadvertise(ctx));
    server = await listenWithPortFallback(app, ctx.config.server.port, {
        host: ctx.config.server.host,
        label: "server",
        onFallback: ({ requestedPort, actualPort }) => {
            console.log(`[server.port] requested=${requestedPort} actual=${actualPort} reason=EADDRINUSE`);
        },
    });
    ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
    ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
    console.log(`Image Gen running at ${ctx.serverUrl}`);
    console.log(`Provider policy: GPT OAuth, API-key Responses, and Grok Images providers. GPT OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
    advertise(ctx);
    try {
        const s = ensureDefaultSession();
        console.log(`[db] default session: ${s.id} (${s.title})`);
    }
    catch (e) {
        const err = errInfo(e);
        console.error("[db] bootstrap failed:", err.message);
    }
    // Background thumbnail backfill for updated users (recursive — covers video
    // series subdirectories like continuous_*/clip_NN.mp4, not just top level).
    (async () => {
        try {
            const { backfillThumbnails } = await import("./lib/thumbBackfill.js");
            const r = await backfillThumbnails(ctx.config.storage.generatedDir);
            if (r.created > 0) {
                console.log(`[thumbs] backfill: ${r.created} created, ${r.skipped} skipped, ${r.failed} failed (${r.total} media files)`);
                const { invalidateHistoryIndex } = await import("./lib/historyIndex.js");
                invalidateHistoryIndex();
            }
        }
        catch (e) {
            console.warn("[thumbs] backfill failed:", e instanceof Error ? e.message : e);
        }
    })();
    server.on("error", (err) => {
        console.error("[server] Failed to start:", err?.message || err);
        process.exit(1);
    });
    reapTimer = setInterval(() => {
        reapTerminalJobs();
        reapCardNewsJobs();
    }, 60_000);
    reapTimer.unref?.();
    process.on("uncaughtException", (err) => {
        console.error("[fatal] uncaughtException:", err);
        process.exit(1);
    });
    process.on("unhandledRejection", (reason) => {
        console.error("[fatal] unhandledRejection:", reason);
    });
    return { app, server, oauthChild, ctx };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
    await startServer();
}
