/**
 * Centralized Inspector SPA mount resolver and installer.
 *
 * Single source of truth for "where is the Inspector for this server":
 *   DISABLE > PUBLIC_INSPECTOR_URL > STATIC_DIR > bundled fallback
 *
 * Used by actions.ts (Express mount) and root_landing (landing-page link).
 */
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import expressStatic from "express";
const DEFAULT_BASE_PATH = "/inspector";
/**
 * Resolve the package root by walking up from this file's directory until we
 * find a directory containing package.json. Works for source checkouts,
 * npm-installed packages, and Docker.
 */
function resolvePackageRoot() {
    let dir = path.dirname(fileURLToPath(import.meta.url));
    for (let i = 0; i < 10; i++) {
        if (fs.existsSync(path.join(dir, "package.json")))
            return dir;
        const parent = path.dirname(dir);
        if (parent === dir)
            break;
        dir = parent;
    }
    return dir;
}
/**
 * Resolve the bundled Inspector dist directory. Returns null when the
 * bundled dist is not present (e.g. submodule not built, or SKIP_INSPECTOR_BUILD).
 */
export function resolveBundledInspectorDir() {
    const root = resolvePackageRoot();
    const distDir = path.join(root, "dist", "inspector");
    if (fs.existsSync(path.join(distDir, "index.html")))
        return distDir;
    // Source-checkout fallback: inspector/dist when the submodule is built.
    const submoduleDistDir = path.join(root, "inspector", "dist");
    if (fs.existsSync(path.join(submoduleDistDir, "index.html")))
        return submoduleDistDir;
    return null;
}
/**
 * Determine the Inspector configuration from environment variables.
 *
 * Precedence: DISABLE > PUBLIC_INSPECTOR_URL > STATIC_DIR > bundled fallback.
 */
export function resolveInspectorMount(env = process.env) {
    const basePath = ((env.NEOTOMA_INSPECTOR_BASE_PATH || DEFAULT_BASE_PATH).trim() || DEFAULT_BASE_PATH).replace(/\/$/, "");
    const normalizedBase = basePath.startsWith("/") ? basePath : `/${basePath}`;
    if (env.NEOTOMA_INSPECTOR_DISABLE === "1") {
        return { kind: "disabled", basePath: normalizedBase };
    }
    const publicUrl = (env.NEOTOMA_PUBLIC_INSPECTOR_URL || "").trim();
    if (publicUrl) {
        return { kind: "external", basePath: normalizedBase, externalUrl: publicUrl };
    }
    const staticDir = (env.NEOTOMA_INSPECTOR_STATIC_DIR || "").trim();
    if (staticDir) {
        return { kind: "local", basePath: normalizedBase, staticDir };
    }
    if (env.NEOTOMA_INSPECTOR_BUNDLED_DISABLE === "1") {
        return { kind: "disabled", basePath: normalizedBase };
    }
    const bundledDir = resolveBundledInspectorDir();
    if (bundledDir) {
        return { kind: "local", basePath: normalizedBase, staticDir: bundledDir };
    }
    return { kind: "disabled", basePath: normalizedBase };
}
/**
 * Inject `<meta name="neotoma-api-base" content="<origin>">` into the
 * Inspector's index.html so the SPA discovers the API origin at runtime.
 */
export function injectInspectorApiBaseMeta(html, origin) {
    const metaTag = `<meta name="neotoma-api-base" content="${origin}">`;
    const headIdx = html.indexOf("</head>");
    if (headIdx === -1)
        return html;
    return html.slice(0, headIdx) + metaTag + "\n" + html.slice(headIdx);
}
/** `stampPath` must be the absolute path (e.g. `/inspector/__live/build_stamp`). */
export function appendInspectorLiveReloadScript(html, stampPath) {
    const headIdx = html.indexOf("</head>");
    if (headIdx === -1)
        return html;
    const stampUrl = stampPath.replace(/\/{2,}/g, "/");
    const script = `<script>(function(){var u=${JSON.stringify(stampUrl)};var l=null;function c(){fetch(u,{cache:"no-store",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(j){var s=String(j.stamp||"");if(l!==null&&s&&s!==l)location.reload();l=s;}).catch(function(){})}setInterval(c,2000);c();})();</script>`;
    return html.slice(0, headIdx) + script + "\n" + html.slice(headIdx);
}
/**
 * Read the Inspector's index.html from the given directory.
 */
export function readInspectorIndexHtml(dir) {
    const indexPath = path.join(dir, "index.html");
    try {
        return fs.readFileSync(indexPath, "utf-8");
    }
    catch {
        return null;
    }
}
/**
 * Fingerprint for `vite build --watch` output used by live-reload polling.
 *
 * Using only `index.html` mtime misses lazy-route / code-split rebuilds:
 * those updates often touch only `assets/*.js` while the HTML shell stays
 * byte-identical, so the stamp must consider built assets too.
 */
/**
 * Normalize the pathname used for Inspector routing checks (SPA vs Vite assets).
 * Collapses duplicate slashes and strips query/hash fragments; decodes
 * percent-escapes so `/inspector//assets/x` and encoded variants still match.
 */
export function normalizeInspectorUrlPathname(pathname) {
    let p = pathname.split("?")[0]?.split("#")[0] ?? pathname;
    try {
        p = decodeURIComponent(p);
    }
    catch {
        /* keep undecoded fragment */
    }
    return p.replace(/\/{2,}/g, "/");
}
/**
 * True when the URL is a Vite-emitted static file under the Inspector mount
 * (`/inspector/assets/...` by default). The SPA shell handler must not respond
 * with index.html for these paths: missing chunks would be served as
 * text/html and break strict MIME checks (blank shell + console errors).
 */
export function isInspectorViteAssetUrlPath(basePath, pathname) {
    const path = normalizeInspectorUrlPathname(pathname);
    const normalizedBase = normalizeInspectorUrlPathname(basePath.replace(/\/$/, "") || "");
    return path.startsWith(`${normalizedBase}/assets/`);
}
export function computeInspectorBuildOutputStamp(staticDir) {
    let max = 0;
    const indexPath = path.join(staticDir, "index.html");
    try {
        max = Math.max(max, fs.statSync(indexPath).mtimeMs);
    }
    catch {
        return 0;
    }
    const assetsDir = path.join(staticDir, "assets");
    let entries;
    try {
        entries = fs.readdirSync(assetsDir, { withFileTypes: true });
    }
    catch {
        return max;
    }
    for (const ent of entries) {
        if (!ent.isFile())
            continue;
        const fp = path.join(assetsDir, ent.name);
        try {
            const st = fs.statSync(fp);
            if (st.mtimeMs > max)
                max = st.mtimeMs;
        }
        catch {
            /* skip race / deleted between readdir and stat */
        }
    }
    return max;
}
/**
 * Install the Inspector SPA mount on an Express app.
 *
 * Registered before auth middleware so the SPA shell and assets are reachable
 * without a bearer token — the API calls the Inspector makes still flow
 * through the normal auth stack.
 */
function isInspectorLiveBuildEnabled(env) {
    return env.NEOTOMA_INSPECTOR_LIVE_BUILD === "1";
}
function resolveLiveInspectorStaticDir(currentDir) {
    const root = resolvePackageRoot();
    const sourceFallbackDir = path.join(root, "inspector", "dist");
    const primaryBundledDir = path.join(root, "dist", "inspector");
    if (path.resolve(currentDir) === path.resolve(sourceFallbackDir) &&
        fs.existsSync(path.join(primaryBundledDir, "index.html"))) {
        return primaryBundledDir;
    }
    return currentDir;
}
export function installInspectorMount(app, env = process.env, logger) {
    const cfg = resolveInspectorMount(env);
    const inspectorLiveBuild = isInspectorLiveBuildEnabled(env);
    if (cfg.kind === "disabled") {
        logger.info("[Inspector] Disabled or no built SPA found; /inspector will not be served.");
        return;
    }
    if (cfg.kind === "external") {
        logger.info(`[Inspector] External Inspector configured at ${cfg.externalUrl}; no local mount.`);
        return;
    }
    const { basePath } = cfg;
    let { staticDir } = cfg;
    if (!staticDir)
        return;
    if (inspectorLiveBuild) {
        staticDir = resolveLiveInspectorStaticDir(staticDir);
    }
    try {
        const rawHtml = readInspectorIndexHtml(staticDir);
        if (!rawHtml) {
            logger.warn(`[Inspector] index.html not found in ${staticDir}; skipping mount.`);
            return;
        }
        // Align with Vite non-root base: `/inspector` → `/inspector/` so asset URLs resolve.
        app.use((req, res, next) => {
            if (req.method !== "GET" && req.method !== "HEAD")
                return next();
            if (req.path !== basePath)
                return next();
            const suffix = req.url.includes("?") ? req.url.slice(req.url.indexOf("?")) : "";
            res.redirect(308, suffix ? `${basePath}/${suffix}` : `${basePath}/`);
        });
        // Live-build mode re-reads index.html on every SPA request and injects a
        // stamp poller. Without it, we still re-read index.html on each SPA shell
        // request so `vite build --watch` (new hashed assets) is visible after a
        // hard refresh; caching the shell in memory would serve stale script hrefs
        // until the API restarts (broken chunks → HTML MIME errors).
        const buildStampPath = `${basePath}/__live/build_stamp`.replace(/\/{2,}/g, "/");
        if (inspectorLiveBuild) {
            app.get(buildStampPath, (_req, res) => {
                const activeStaticDir = inspectorLiveBuild
                    ? resolveLiveInspectorStaticDir(staticDir)
                    : staticDir;
                const stamp = computeInspectorBuildOutputStamp(activeStaticDir);
                if (!stamp) {
                    res.status(404).type("application/json").json({ error: "no_index" });
                    return;
                }
                res.set("Cache-Control", "no-store");
                res.type("application/json");
                res.json({ stamp, dir: activeStaticDir });
            });
            app.use(basePath, (req, res, next) => {
                if (req.method !== "GET" && req.method !== "HEAD")
                    return next();
                if (req.path === "/" || req.path === "" || req.path.startsWith("/__live")) {
                    return next();
                }
                const activeStaticDir = resolveLiveInspectorStaticDir(staticDir);
                const requested = decodeURIComponent(req.path.replace(/^\/+/, ""));
                const candidate = path.resolve(activeStaticDir, requested);
                const relative = path.relative(activeStaticDir, candidate);
                if (relative.startsWith("..") || path.isAbsolute(relative))
                    return next();
                try {
                    if (!fs.statSync(candidate).isFile())
                        return next();
                }
                catch {
                    return next();
                }
                res.set("Cache-Control", "no-store");
                res.sendFile(candidate);
            });
        }
        const staticOptions = {
            index: false,
            fallthrough: true,
            maxAge: inspectorLiveBuild ? 0 : "1h",
            setHeaders: inspectorLiveBuild
                ? (res) => {
                    res.setHeader("Cache-Control", "no-store");
                }
                : undefined,
        };
        if (inspectorLiveBuild) {
            const staticHandlerByDir = new Map();
            app.use(basePath, (req, res, next) => {
                const active = resolveLiveInspectorStaticDir(staticDir);
                let handler = staticHandlerByDir.get(active);
                if (!handler) {
                    handler = expressStatic.static(active, staticOptions);
                    staticHandlerByDir.set(active, handler);
                }
                handler(req, res, next);
            });
        }
        else {
            app.use(basePath, expressStatic.static(staticDir, staticOptions));
        }
        app.get([basePath, `${basePath}/*`], (req, res, next) => {
            if (req.method !== "GET")
                return next();
            const pathnameForChecks = normalizeInspectorUrlPathname((req.originalUrl || req.url || "").split("?")[0]?.split("#")[0] || req.path || "");
            if (pathnameForChecks.startsWith(`${basePath.replace(/\/$/, "")}/__live`)) {
                return next();
            }
            if (isInspectorViteAssetUrlPath(basePath, pathnameForChecks)) {
                res.status(404).type("text/plain; charset=utf-8").send("Not found");
                return;
            }
            const proto = req.protocol;
            const host = req.get("host") || "localhost";
            const origin = `${proto}://${host}`;
            if (inspectorLiveBuild) {
                const activeStaticDir = resolveLiveInspectorStaticDir(staticDir);
                const latest = readInspectorIndexHtml(activeStaticDir);
                if (!latest)
                    return next();
                const html = appendInspectorLiveReloadScript(injectInspectorApiBaseMeta(latest, origin), buildStampPath);
                res.set("Content-Type", "text/html; charset=utf-8");
                res.set("Cache-Control", "no-store");
                res.send(html);
                return;
            }
            const latestShell = readInspectorIndexHtml(staticDir);
            if (!latestShell)
                return next();
            const html = injectInspectorApiBaseMeta(latestShell, origin);
            res.set("Content-Type", "text/html; charset=utf-8");
            res.set("Cache-Control", "no-store");
            res.send(html);
        });
        logger.info(`[Inspector] Serving SPA from ${staticDir} at ${basePath}${inspectorLiveBuild
            ? " (live rebuild: no HTML/asset cache; auto-reload when build output changes)"
            : ""}`);
    }
    catch (err) {
        logger.warn(`[Inspector] Failed to mount SPA: ${err.message}`);
    }
}
/**
 * Resolve the Inspector URL for the root landing page.
 *
 * Returns null when the Inspector is disabled or unavailable; an absolute
 * external URL when PUBLIC_INSPECTOR_URL is set; or a same-origin path
 * when a local mount is active.
 */
export function resolveInspectorLandingUrl(base, env = process.env) {
    const cfg = resolveInspectorMount(env);
    if (cfg.kind === "disabled")
        return null;
    if (cfg.kind === "external")
        return cfg.externalUrl ?? null;
    return `${base}${cfg.basePath}`;
}
//# sourceMappingURL=inspector_mount.js.map