"use strict";
/**
 * Rewrite free `Hot.*` references inside a preamble body to equivalent
 * `hotCtx.*` accesses so the compiled preamble runs in the v0.9.0 runtime
 * without relying on the global Hot singleton. Also collects literal
 * Hot.include() targets for build-time partial resolution (HS090-15).
 *
 * Uses ts-morph (already a HotStaq dependency) for a scope-aware pass —
 * this is what lets us skip rewrites when an app locally rebinds `Hot`
 * via `let foo = Hot` or `const { Cookies } = Hot`.
 *
 * See design/HS090-parser-split.md "Preamble rewrite".
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.rewritePreamble = rewritePreamble;
exports.partialIdFromPath = partialIdFromPath;
const crypto = __importStar(require("crypto"));
const ts_morph_1 = require("ts-morph");
const build_expand_1 = require("./build-expand");
const SIMPLE_REWRITES = {
    // API / fetch helpers.
    getJSON: (ctx) => `${ctx}.getJSON`,
    jsonRequest: (ctx) => `${ctx}.jsonRequest`,
    echoUnsafe: (ctx) => `${ctx}.echo`,
    echo: (ctx) => `${ctx}.echo`,
    import: (ctx) => `${ctx}.import`,
    includeJS: (ctx) => `${ctx}.includeJS`,
    // URL helpers — forward to the ctx-scoped URLSearchParams.
    getUrlParam: (ctx) => `((n) => ${ctx}.search.get(n))`,
    // Routing context.
    pathname: (ctx) => `${ctx}.pathname`,
    params: (ctx) => `${ctx}.params`,
    // API client (HS090-8). Bundled client is exposed as ctx.api.
    API: (ctx) => `${ctx}.api`,
    // Cookie namespace — Hot.Cookies.get(x) → hotCtx.cookies.get(x).
    Cookies: (ctx) => `${ctx}.cookies`,
    // Left alone deliberately — too broad or runtime-only.
    CurrentPage: null,
    Output: null,
    echoHtmlInput: null,
    sanitizeHTML: null,
    addHtmlUnsafe: null,
    include: null, // Special-cased: we rewrite call-expressions, not identifier access.
    import_: null,
};
/**
 * Rewrite a preamble source string.
 */
function rewritePreamble(source, opts = {}) {
    const ctx = opts.ctxParam || "hotCtx";
    const warnings = [];
    const partials = [];
    const partialCalls = [];
    const seenPartials = new Set();
    // Fast path: if the preamble never mentions Hot at all, nothing to do.
    if (!/\bHot\b/.test(source))
        return ({ source, partials, partialCalls, warnings });
    const project = new ts_morph_1.Project({
        useInMemoryFileSystem: true,
        compilerOptions: {
            allowJs: true,
            noEmit: true,
            target: 99 // ESNext — we don't emit, we only parse.
        }
    });
    const sf = project.createSourceFile(opts.filename || "__hott_preamble__.ts", source, { overwrite: true });
    // Track local bindings that shadow the free `Hot` identifier so we
    // leave those untouched. Conservative: any `Hot` declared in scope
    // disables rewriting for the entire preamble (matches the design
    // doc's stated limitation and emits a warning).
    if (hasLocalHotBinding(sf)) {
        warnings.push({
            code: "hott/hot-local-binding",
            message: "preamble declares a local `Hot` identifier; Hot.* references will not be rewritten. " +
                "Rename the local to keep --static builds working."
        });
        return ({ source: sf.getFullText(), partials, partialCalls, warnings });
    }
    sf.forEachDescendant((node) => {
        if (node.getKind() !== ts_morph_1.SyntaxKind.PropertyAccessExpression)
            return;
        const access = node.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAccessExpression);
        const expr = access.getExpression();
        if (expr.getKind() !== ts_morph_1.SyntaxKind.Identifier || expr.getText() !== "Hot")
            return;
        const name = access.getName();
        // Hot.include(...) — rewrite the whole call expression so we can
        // inspect the argument and collect literal partial targets.
        if (name === "include") {
            const parent = access.getParent();
            if (parent && parent.getKind() === ts_morph_1.SyntaxKind.CallExpression) {
                rewriteIncludeCall(parent, ctx, partials, seenPartials, partialCalls, warnings);
                return;
            }
            warnings.push({
                code: "hott/hot-include-non-call",
                message: "Hot.include referenced but not called; cannot resolve at build time.",
                start: access.getStart(),
                end: access.getEnd()
            });
            return;
        }
        const rewriter = SIMPLE_REWRITES[name];
        if (rewriter != null) {
            access.replaceWithText(rewriter(ctx));
            return;
        }
        if (rewriter === null) {
            // Deliberately left alone (Hot.Cookies, Hot.Output, etc.).
            return;
        }
        // Unknown member — leave it but warn so the migration guide can
        // catch uncovered surface area.
        warnings.push({
            code: "hott/hot-unknown-member",
            message: `Unknown Hot.${name} reference; not rewritten. Consider porting this call site.`,
            start: access.getStart(),
            end: access.getEnd()
        });
    });
    return ({ source: sf.getFullText(), partials, partialCalls, warnings });
}
function rewriteIncludeCall(callNode, ctx, partials, seen, partialCalls, warnings) {
    const call = callNode.asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression);
    const args = call.getArguments();
    if (args.length === 0) {
        warnings.push({
            code: "hott/hot-include-no-arg",
            message: "Hot.include() called with no arguments; skipped.",
            start: call.getStart(),
            end: call.getEnd()
        });
        return;
    }
    const first = args[0];
    const stringLiteral = asStringLiteralValue(first);
    if (stringLiteral !== null) {
        if (!seen.has(stringLiteral)) {
            seen.add(stringLiteral);
            partials.push(stringLiteral);
        }
        // Capture the second-arg expression. If it's literal we also
        // materialise the values so the builder can try build-time
        // expansion; if that fails (or args were non-literal), the
        // builder swaps in a runtime-inline partial call whose args
        // use the verbatim source text so caller-scope variables flow
        // into the partial naturally.
        let argsValue = null;
        let argsExprText = null;
        if (args.length >= 2) {
            const second = args[1];
            argsExprText = second.getText();
            if ((0, build_expand_1.isLiteralAst)(second)) {
                argsValue = (0, build_expand_1.materialiseLiteral)(second);
            }
        }
        const stashId = stashIdFor(stringLiteral, argsValue);
        partialCalls.push({ path: stringLiteral, args: argsValue, argsExprText, stashId });
        // Emit a placeholder the builder rewrites during entry code-gen:
        //   → hotCtx.echo(hotCtx.includeStash('<id>'))          (argless)
        //   → await hotCtx.__includePartial('<id>', <argsExpr>) (with args)
        // The __includePartial placeholder carries the live args
        // expression text so runtime-inline can invoke a local fn. When
        // the original call is already inside an `await` expression we
        // replace that parent so the final placeholder has exactly one
        // leading `await`, not two.
        if (argsExprText !== null) {
            const parent = call.getParent();
            const targetNode = (parent && parent.getKind() === ts_morph_1.SyntaxKind.AwaitExpression)
                ? parent
                : call;
            targetNode.replaceWithText(`await ${ctx}.__includePartial(${JSON.stringify(stashId)}, ${argsExprText})`);
        }
        else {
            call.replaceWithText(`${ctx}.echo(${ctx}.includeStash(${JSON.stringify(stashId)}))`);
        }
        return;
    }
    warnings.push({
        code: "hott/hot-include-dynamic",
        message: "Hot.include() called with a non-literal argument; cannot resolve at build time. " +
            "Refactor to explicit literal branches before enabling --strict.",
        start: call.getStart(),
        end: call.getEnd()
    });
    // Rewrite to ctx.includeStash so at least it doesn't reference the
    // global Hot at runtime. The ctx implementation can fall back to a
    // runtime lookup for dynamic ids. Still wrap in echo so it mounts.
    const argText = first.getText();
    call.replaceWithText(`${ctx}.echo(${ctx}.includeStash(${argText}))`);
}
/**
 * Derive a stash id for a (path, args) pair. When args is null, the id is
 * the plain path-based form (stable across calls) so unchanged call sites
 * keep the same key. When args is present, we append a short content hash
 * so call sites with different args get distinct stash entries.
 */
function stashIdFor(path, args) {
    const base = partialIdFromPath(path);
    if (args == null)
        return (base);
    const hash = crypto
        .createHash("sha256")
        .update(JSON.stringify(args))
        .digest("hex")
        .substring(0, 10);
    return (`${base}#${hash}`);
}
function asStringLiteralValue(node) {
    const kind = node.getKind();
    if (kind === ts_morph_1.SyntaxKind.StringLiteral)
        return (node.asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral).getLiteralValue());
    if (kind === ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral)
        return (node.asKindOrThrow(ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral).getLiteralValue());
    return (null);
}
/**
 * Derive a stable stash id from a .hott partial path. The full path is
 * preserved (minus the .hott extension) so two different components named
 * "card.hott" in different directories don't collide.
 */
function partialIdFromPath(path) {
    let p = path.replace(/^\.\/+/, "").replace(/^\/+/, "");
    if (p.toLowerCase().endsWith(".hott"))
        p = p.substring(0, p.length - 5);
    return (p.replace(/\\/g, "/"));
}
/**
 * True if the preamble declares a local binding named `Hot` at any scope.
 * Conservative — we bail out of rewriting entirely when this is true.
 */
function hasLocalHotBinding(sf) {
    let found = false;
    sf.forEachDescendant((node) => {
        var _a, _b;
        if (found)
            return;
        switch (node.getKind()) {
            case ts_morph_1.SyntaxKind.VariableDeclaration:
            case ts_morph_1.SyntaxKind.Parameter:
            case ts_morph_1.SyntaxKind.BindingElement:
                {
                    const name = (_b = (_a = node).getNameNode) === null || _b === void 0 ? void 0 : _b.call(_a);
                    if (name && name.getText && name.getText() === "Hot")
                        found = true;
                    break;
                }
            default:
                break;
        }
    });
    return (found);
}
//# sourceMappingURL=rewrite-preamble.js.map