import { mkdir, readdir, rename, stat } from "node:fs/promises";
import { resolve as resolvePath, join, dirname } from "node:path";
import { writeLlmsTxt } from "../../../../../lib/aeo-llms-txt-writer/dist/index.js";
/**
 * Publish an already-extracted static-site tree into the per-account
 * static-publish surface (<accountDir>/sites/<slug>/) and emit one
 * canonical path slug. Pure function — the MCP registration in index.ts
 * adapts the discriminated-union result onto the MCP transport response.
 *
 * Invariants (each maps to one Refusal kind):
 *   - slug segments match SAFE_SEG_RE, no leading dot, no `..`
 *   - destination must be absent or empty
 *   - source tree must contain no symlinks at any depth
 *   - source must have at least one .html at the top level
 *   - if no index.html at top level, exactly one top-level .html
 *
 * Post-move:
 *   - canonical path is `/sites/<slug>/` if index.html at top level,
 *     otherwise `/sites/<slug>/<the-one-html>.html`
 *   - llms.txt + llms-full.txt are refreshed via writeLlmsTxt
 *     (best-effort: failure does not unwind the mv)
 *
 * All operator-visible log lines preserved verbatim from the prior
 * skill-prose contract so existing grep targets keep working.
 */
const SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
const TAG = "[publish-site]";
function refusal(kind, detail, logLine) {
    process.stderr.write(`${TAG} refused reason=${kind} ${logLine}\n`);
    return { ok: false, refusal: kind, detail };
}
function validateSlug(slug) {
    const segments = slug.split("/").filter((s) => s.length > 0);
    if (segments.length === 0) {
        return {
            ok: false,
            refusal: refusal("unsafe-slug", { slug }, `slug=${slug}`),
        };
    }
    for (const seg of segments) {
        if (seg === "..") {
            return {
                ok: false,
                refusal: refusal("unsafe-slug", { slug, segment: seg }, `slug=${slug}`),
            };
        }
        if (seg.startsWith(".")) {
            return {
                ok: false,
                refusal: refusal("unsafe-slug", { slug, segment: seg }, `slug=${slug}`),
            };
        }
        if (!SAFE_SEG_RE.test(seg)) {
            return {
                ok: false,
                refusal: refusal("unsafe-slug", { slug, segment: seg }, `slug=${slug}`),
            };
        }
    }
    return { ok: true, segments };
}
async function findSymlinkAnywhere(dir) {
    const entries = await readdir(dir, { withFileTypes: true });
    for (const ent of entries) {
        if (ent.isSymbolicLink())
            return join(dir, ent.name);
        if (ent.isDirectory()) {
            const found = await findSymlinkAnywhere(join(dir, ent.name));
            if (found)
                return found;
        }
    }
    return null;
}
async function countFiles(dir) {
    let n = 0;
    const entries = await readdir(dir, { withFileTypes: true });
    for (const ent of entries) {
        if (ent.isFile())
            n += 1;
        else if (ent.isDirectory())
            n += await countFiles(join(dir, ent.name));
    }
    return n;
}
export async function publishSite(input, deps) {
    const { accountId, accountDir, source, slug } = input;
    const slugCheck = validateSlug(slug);
    if (!slugCheck.ok)
        return slugCheck.refusal;
    const destination = resolvePath(accountDir, "sites", ...slugCheck.segments);
    // Pre-flight: dest must be absent or empty.
    try {
        const destStat = await stat(destination);
        if (destStat.isDirectory()) {
            const entries = await readdir(destination);
            if (entries.length > 0) {
                return refusal("destination-occupied", { destination, entryCount: entries.length }, `dest=${destination}`);
            }
        }
        else {
            return refusal("destination-occupied", { destination, kind: "non-directory" }, `dest=${destination}`);
        }
    }
    catch (err) {
        const code = err?.code;
        if (code !== "ENOENT")
            throw err;
        // ENOENT is the happy path — destination doesn't exist yet.
    }
    // Pre-flight: source symlink scan (any depth).
    const symlink = await findSymlinkAnywhere(source);
    if (symlink) {
        return refusal("symlink-in-source", { source, entry: symlink }, `entry=${symlink}`);
    }
    // Pre-flight: top-level html inventory.
    const topLevel = await readdir(source, { withFileTypes: true });
    const topHtml = topLevel
        .filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".html"))
        .map((e) => e.name);
    if (topHtml.length === 0) {
        return refusal("zero-html", { source }, `source=${source}`);
    }
    const hasIndex = topHtml.some((n) => n.toLowerCase() === "index.html");
    if (!hasIndex && topHtml.length > 1) {
        return refusal("ambiguous-html", { source, candidates: topHtml }, `candidates=${topHtml.join(",")}`);
    }
    const movedFiles = await countFiles(source);
    // Move. Single rename — same filesystem assumed (accountDir-local).
    // Ensure the parent (`<accountDir>/sites/...` up to the leaf) exists so
    // the first publish on a fresh accountDir doesn't ENOENT. mkdir is
    // idempotent on the parent; the leaf itself was confirmed absent above.
    await mkdir(dirname(destination), { recursive: true });
    // EXDEV or EACCES propagates as an exception → MCP isError surfacing
    // via the registration layer in index.ts.
    await rename(source, destination);
    process.stderr.write(`${TAG} move from=${source} to=${destination}/ files=${movedFiles}\n`);
    const kind = hasIndex ? "index" : "file";
    const pathSlug = hasIndex
        ? `/sites/${slugCheck.segments.join("/")}/`
        : `/sites/${slugCheck.segments.join("/")}/${topHtml[0]}`;
    process.stderr.write(`${TAG} url emitted=${pathSlug} kind=${kind}\n`);
    // Post-move hook: refresh llms.txt at the site root. Best-effort.
    // Never unwinds the mv — a stale llms.txt is strictly less bad than
    // a failed publish.
    let aeo;
    try {
        const llms = await writeLlmsTxt({
            accountId,
            siteName: slugCheck.segments[slugCheck.segments.length - 1],
            siteDir: destination,
            siteDescription: input.siteDescription,
        }, { getSession: deps.getSession });
        const indexPath = llms.writtenPaths?.index ?? "";
        aeo = {
            ok: true,
            indexPath,
            indexBytes: llms.sizeBytes.index,
        };
        process.stderr.write(`${TAG} aeo-llms-txt-ok wrote=${indexPath} indexBytes=${llms.sizeBytes.index}\n`);
    }
    catch (err) {
        const reason = err instanceof Error ? err.message : String(err);
        aeo = { ok: false, reason };
        process.stderr.write(`${TAG} aeo-llms-txt-failed reason=${reason}\n`);
    }
    return {
        ok: true,
        pathSlug,
        kind,
        movedFiles,
        destination,
        aeo,
    };
}
//# sourceMappingURL=publish-site.js.map