import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { serveStatic } from "@hono/node-server/serve-static";
import { readFile } from "node:fs/promises";
import { readFileSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { loadConfig, dataDir } from "../infra/config.js";
import { PACKAGE_ROOT } from "../infra/paths.js";
import { syncSkills } from "../infra/skill-sync.js";
import { initProviders } from "../providers/registry.js";
import { ensureSharedDirs } from "../shared-assets.js";
import { ensureSpawnPath } from "./spawn-path.js";
import { ensureManaged, detect } from "../infra/deps.js";
import { apiRoutes, setWsBridge, setRenderQueue } from "./api.js";
import { WsBridge } from "../ws-bridge.js";
import { attachTerminalWebSocket } from "./terminal/terminal-ws.js";
import { attachBridgeWebSocket } from "./bridge/bridge-ws.js";
import { startAnalyticsCollector } from "../domain/analytics-collector.js";
import { startResearchScheduler } from "../research-scheduler.js";
import { RenderQueue, defaultDbPath } from "./render-queue/index.js";
import { RenderWsRouter } from "./render-ws.js";
import { runRenderPipeline } from "./render-pipeline.js";
import { CompositionSchema } from "../shared/composition.js";
import yaml from "js-yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Resolve web/dist relative to the package root (two levels up from dist/server/)
const WEB_DIST = join(__dirname, "..", "..", "web", "dist");
export async function startServer(port) {
    // Repair PATH before initProviders() probes ffmpeg/edge-tts. A daemon started
    // outside a login shell lacks /opt/homebrew/bin, making bare-name spawns fail
    // with ENOENT. Idempotent — also called at the CLI entry. See spawn-path.ts.
    ensureSpawnPath();
    // Populate the managed binary location (~/.autoviral/bin) from the vendored
    // ffmpeg-static / @ffprobe-installer binaries so I14's doctor can detect them
    // there. Best-effort + fire-and-forget — resolution already falls back to the
    // vendored absolute path, so a failed/slow copy must never block boot or a
    // render. See PRD-0003 §1 + src/infra/deps.ts.
    void ensureManaged().catch(() => { });
    // First-run guidance (I14, PRD-0003 §1): if a CORE binary (ffmpeg/ffprobe)
    // resolves ONLY to the bare-name PATH tier — meaning there's no env override,
    // no managed copy, AND no vendored binary — render/export/waveform/TTS-probe
    // will later spawn with an opaque ENOENT. Surface a visible, actionable hint
    // at boot instead of letting the user hit a bare crash mid-render. This is the
    // only thing standing between the failure and `autoviral setup`.
    warnIfCoreDepsMissing();
    // Expose the bound port so render-pipeline can rewrite relative asset
    // URLs into HTTP URLs the Remotion renderer can fetch.
    process.env.AUTOVIRAL_PORT = String(port);
    // 1. Load config
    const config = await loadConfig();
    // 2. Initialize providers
    await initProviders(config);
    // 3. Ensure shared asset directories
    await ensureSharedDirs();
    // 3.5. Sync the bundled skills into ~/.claude/skills/ so the EXTERNAL CLI agent
    // reads the current autoviral skill. This is the keystone that makes the
    // DESKTOP update path work: Electron never runs `npm postinstall`, so without a
    // boot-time sync a desktop user's external agent would read a stale skill
    // forever. The daemon spawned by `autoviral start` AND the daemon spawned by
    // the Electron shell are the SAME process, so hooking here covers both routes.
    //
    // syncSkills is the SHARED core (also called by src/postinstall.ts): version-
    // gated (skip when the marker already records this version + the skill is
    // present), missing-skill recovery, symlink self-copy guard, and the .yaml /
    // permitted_skills.md never-overwrite rule. Wrapped so a sync failure is logged
    // but NEVER blocks boot — the daemon must come up even if skill sync can't.
    await bootSyncSkills();
    // 4. Create WsBridge
    const wsBridge = new WsBridge(port);
    setWsBridge(wsBridge);
    // 4.5. Create RenderQueue + render-ws router (Phase 7.B).
    const renderQueue = new RenderQueue({
        dbPath: process.env.AUTOVIRAL_RENDER_DB ?? defaultDbPath(),
        runRenderPipeline,
        loadComposition: async (workId) => {
            const raw = await readFile(join(dataDir, "works", workId, "composition.yaml"), "utf-8");
            const parsed = CompositionSchema.safeParse(yaml.load(raw));
            if (!parsed.success) {
                throw new Error(`composition invalid: ${parsed.error.message}`);
            }
            return parsed.data;
        },
        outDirFor: (workId) => join(dataDir, "works", workId, "output"),
        concurrency: Number.parseInt(process.env.AUTOVIRAL_RENDER_CONCURRENCY ?? "1", 10),
    });
    setRenderQueue(renderQueue);
    const renderWs = new RenderWsRouter(renderQueue);
    const app = new Hono();
    // 5. Mount API routes
    app.route("/", apiRoutes);
    // 8. Serve static frontend files from web/dist/
    app.use("/*", serveStatic({ root: WEB_DIST }));
    // SPA fallback: serve index.html for any non-API GET request that didn't match a static file
    app.get("*", async (c) => {
        try {
            const indexPath = join(WEB_DIST, "index.html");
            const html = await readFile(indexPath, "utf-8");
            return c.html(html);
        }
        catch {
            return c.text("Dashboard not built. Run: npm run build:frontend", 404);
        }
    });
    // 6. Start HTTP server + WebSocket upgrade handler
    const nodeServer = serve({
        fetch: app.fetch,
        port,
    });
    const httpServer = nodeServer;
    // Fail fast + clearly on a held port (e.g. another AutoViral daemon). Without
    // this the desktop shell's health probe would just spin until its 10s timeout.
    httpServer.on("error", (err) => {
        if (err.code === "EADDRINUSE") {
            console.error(`Port ${port} is already in use — is another AutoViral daemon running?`);
            process.exit(1);
        }
        throw err;
    });
    // Terminal WS adapter — attached without auto-binding so we can multiplex
    // through the single upgrade handler below.
    const terminalWs = attachTerminalWebSocket(null, port);
    // Bridge WS adapter (Phase 3) — subscribes Studio UI to UiEventBus and
    // accepts approval-response frames from ApprovalPrompt.
    const bridgeWs = attachBridgeWebSocket(null);
    // Route HTTP upgrade events.
    // Render-ws goes FIRST so it gets first dibs on /ws/render/jobs/:id; the
    // wsBridge handles /ws/browser/:workId[/:sessionId]; terminalWs handles
    // /ws/terminal/:workId[/:sessionId] (ADR-008 — sessionId optional, defaults
    // to the work's first session); bridgeWs handles /ws/bridge/:workId;
    // everything else is rejected.
    httpServer.on("upgrade", (req, socket, head) => {
        if (renderWs.handleUpgrade(req, socket, head))
            return;
        if (wsBridge.handleUpgrade(req, socket, head))
            return;
        if (terminalWs.handleUpgrade(req, socket, head))
            return;
        if (bridgeWs.handleUpgrade(req, socket, head))
            return;
        // Unknown upgrade — destroy socket
        socket.destroy();
    });
    // 7. Start background services
    await startAnalyticsCollector();
    // #64 — boot the auto-research scheduler so config.research.{enabled,schedule}
    // actually drives periodic trend collection (was a fully orphaned control).
    await startResearchScheduler();
    // 7.B. Backfill missing audio peaks (.peaks.json next to every audio
    // asset). Fire-and-forget — never blocks startup; failures are logged.
    // Closes the loop on Phase B of the 2026-05-25 multi-track + waveform
    // PRD: works opened before this build only had on-the-fly WebAudio
    // peaks; backfill makes the JSON-fetch path the steady-state route.
    void import("./audio/peaks-backfill.js").then(({ backfillPeaks }) => backfillPeaks(join(dataDir, "works")).catch((err) => console.warn("[peaks-backfill] startup scan failed:", err)));
    // 7.C. Idle-TTL session auto-archive sweep (ADR-008 §5). Fire-and-forget —
    // releases memory pinned by chat sessions idle past SESSION_IDLE_TTL while
    // keeping their logs on disk (archived sessions stay restorable). Never
    // blocks startup; per-work failures don't abort the sweep.
    void wsBridge.sweepAllIdleSessions().catch((err) => console.warn("[session-sweep] startup archive sweep failed:", err));
    return { server: httpServer };
}
/**
 * Boot-time skill sync (covers npm CLI + Electron desktop in one place).
 *
 * Resolves the bundled skills/ as a SIBLING of PACKAGE_ROOT — `dist/` and
 * `skills/` sit side by side in the npm tarball (the `files` array ships both as
 * top-level entries) AND under app.asar.unpacked in the packaged Electron app, so
 * the source is `PACKAGE_ROOT/../skills`, NOT `PACKAGE_ROOT/skills`. This mirrors
 * postinstall.ts's `join(__dirname, "..", "skills")` so the npm-install route and
 * the desktop boot route resolve to the very same directory (boot-sync-skills
 * test pins this). Install target is ~/.claude/skills. Delegates to the SHARED
 * `syncSkills` core (version-gated, missing-skill recovery, symlink guard, yaml /
 * permitted_skills.md exemption). Wrapped in try/catch: a sync failure is a
 * `console.warn`, never a boot blocker — the daemon must always come up, even if
 * skill sync can't (read-only HOME, permission denied, etc.). Runs ONCE per
 * process (called from startServer), not per request.
 */
export async function bootSyncSkills() {
    try {
        // skills/ is a SIBLING of dist/ (= PACKAGE_ROOT), so go up one then into
        // skills/. syncSkills still skips gracefully if the dir is ever absent.
        const sourceSkillsDir = join(PACKAGE_ROOT, "..", "skills");
        const targetSkillsDir = join(homedir(), ".claude", "skills");
        await syncSkills({
            sourceSkillsDir,
            targetSkillsDir,
            version: readPackageVersion(),
            // Marker is a sibling of the per-skill dirs (NOT inside autoviral/), so a
            // copy of autoviral/ can never delete it.
            markerPath: join(targetSkillsDir, ".autoviral-synced.json"),
        });
    }
    catch (err) {
        console.warn("Warning: failed to sync skills to ~/.claude/skills/:", err instanceof Error ? err.message : err);
    }
}
/**
 * The package version, resolved by walking up from PACKAGE_ROOT to the nearest
 * package.json with a `version`. Mirrors bridge/routes.ts readPackageVersion:
 *   - packaged daemon: PACKAGE_ROOT = dist/, write-dist-pkg.mjs stamps the real
 *     version into dist/package.json → found on step 0.
 *   - dev/test: PACKAGE_ROOT = src/ (no package.json) → walk up to the repo root.
 * The "0.0.0" fallback only fires if no manifest is reachable; it still lets the
 * sync run on a missing-skill / no-marker target.
 */
function readPackageVersion() {
    let dir = PACKAGE_ROOT;
    for (let i = 0; i < 4; i++) {
        try {
            const raw = readFileSync(join(dir, "package.json"), "utf-8");
            const v = JSON.parse(raw).version;
            if (typeof v === "string" && v.length > 0)
                return v;
        }
        catch {
            // no package.json here (or unparseable) — keep walking up.
        }
        const parent = resolve(dir, "..");
        if (parent === dir)
            break;
        dir = parent;
    }
    return "0.0.0";
}
/**
 * Boot-time first-run guidance for missing CORE deps (I14, PRD-0003 §1).
 *
 * detect() classifies ffmpeg/ffprobe by precedence (env → managed → vendored →
 * bare PATH). A "path" source means NONE of the reliable tiers resolved — the
 * daemon would fall back to spawning the bare name against $PATH, which throws
 * an opaque ENOENT if the binary isn't there. We do a cheap synchronous PATH
 * sweep (no spawn — never blocks boot) and, only when the binary truly isn't
 * reachable, print a single visible, actionable line pointing at `autoviral
 * setup` / `autoviral doctor` instead of letting render/export crash bare later.
 */
function warnIfCoreDepsMissing() {
    try {
        const resolution = detect();
        const missing = [];
        for (const dep of ["ffmpeg", "ffprobe"]) {
            const r = resolution[dep];
            if (r.source !== "path")
                continue; // env/managed/vendored → reliably spawnable
            if (!binaryOnPath(dep))
                missing.push(dep);
        }
        if (missing.length > 0) {
            console.warn(`\n[deps] Missing core dependency: ${missing.join(", ")}. ` +
                `Render / export / waveform / TTS-duration will fail with ENOENT until it's installed.\n` +
                `[deps] Run \`autoviral setup\` to install it (or \`autoviral doctor\` to see what's missing).\n`);
        }
    }
    catch {
        // Guidance is best-effort — a probe failure must never abort startup.
    }
}
/** Cheap synchronous check: does `name` resolve on $PATH? No spawn (so boot
 *  never blocks); mirrors the doctor's PATH sweep. */
function binaryOnPath(name) {
    const pathEnv = process.env.PATH ?? "";
    const exts = process.platform === "win32"
        ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
        : [""];
    for (const dir of pathEnv.split(process.platform === "win32" ? ";" : ":")) {
        if (!dir)
            continue;
        for (const ext of exts) {
            if (existsSync(join(dir, `${name}${ext}`)))
                return true;
        }
    }
    return false;
}
//# sourceMappingURL=index.js.map