export { $, TsChain } from "./fluent.js";
export const ts = {
    raw(code) {
        return { kind: "raw", code };
    },
    statements(body) {
        return { kind: "statements", body };
    },
    statementsPush(statement, ...stmts) {
        return { kind: "statements", body: [...statement.body, ...stmts] };
    },
    importDecl(opts) {
        return {
            kind: "import",
            importKind: opts.importKind,
            names: opts.names ?? [],
            defaultName: opts.defaultName,
            namespaceName: opts.namespaceName,
            from: opts.from,
        };
    },
    varDecl(declKind, name, initializer, typeAnnotation) {
        return { kind: "varDecl", declKind, name, typeAnnotation, initializer };
    },
    letDecl(name, initializer, typeAnnotation) {
        return {
            kind: "varDecl",
            declKind: "let",
            name,
            typeAnnotation,
            initializer,
        };
    },
    constDecl(name, initializer, typeAnnotation) {
        return {
            kind: "varDecl",
            declKind: "const",
            name,
            typeAnnotation,
            initializer,
        };
    },
    constDeclId(name, initializer, typeAnnotation) {
        return {
            kind: "varDecl",
            declKind: "const",
            name: name.name,
            typeAnnotation,
            initializer,
        };
    },
    /** `const { key: binding, ... } = initializer` — use shorthand when key === binding */
    constDestructure(bindings, initializer, typeAnnotation) {
        const parts = Object.entries(bindings).map(([key, binding]) => key === binding ? key : `${key}: ${binding}`);
        return {
            kind: "varDecl",
            declKind: "const",
            name: `{ ${parts.join(", ")} }`,
            typeAnnotation,
            initializer,
        };
    },
    /** `let { key: binding, ... } = initializer` — use shorthand when key === binding */
    letDestructure(bindings, initializer, typeAnnotation) {
        const parts = Object.entries(bindings).map(([key, binding]) => key === binding ? key : `${key}: ${binding}`);
        return {
            kind: "varDecl",
            declKind: "let",
            name: `{ ${parts.join(", ")} }`,
            typeAnnotation,
            initializer,
        };
    },
    assign(lhs, rhs) {
        return { kind: "assign", lhs, rhs };
    },
    functionDecl(name, params, body, opts) {
        return {
            kind: "functionDecl",
            name,
            params,
            body,
            async: opts?.async ?? false,
            export: opts?.export ?? false,
            returnType: opts?.returnType,
        };
    },
    arrowFn(params, body, opts) {
        return {
            kind: "arrowFn",
            params,
            body,
            async: opts?.async ?? false,
            returnType: opts?.returnType,
        };
    },
    call(callee, args = []) {
        return { kind: "call", callee, arguments: args };
    },
    namedArgs(callee, args) {
        return ts.call(callee, [ts.obj(args)]);
    },
    await(expr) {
        return { kind: "await", expr };
    },
    /**
     * `receiver.name(args)` or `receiver?.name(args)`.
     * Replaces the verbose `$(receiver).prop(name).call(args).done()` chain
     * for the common "method call on a receiver" shape.
     */
    methodCall(receiver, name, args = [], opts) {
        return ts.call(ts.prop(receiver, name, opts), args);
    },
    /** `await callee(args)` */
    awaitCall(callee, args = []) {
        return ts.await(ts.call(callee, args));
    },
    /** `await receiver.name(args)` or `await receiver?.name(args)` */
    awaitMethodCall(receiver, name, args = [], opts) {
        return ts.await(ts.methodCall(receiver, name, args, opts));
    },
    /**
     * Immediately-invoked (arrow) function expression: `(async () => { body })()`.
     * `body` can be a `TsStatements` body or any single `TsNode`.
     * The printer adds the wrapping parens around the arrow callee automatically.
     */
    iife(opts) {
        const body = Array.isArray(opts.body) ? ts.statements(opts.body) : opts.body;
        return ts.call(ts.arrowFn(opts.params ?? [], body, { async: opts.async }), []);
    },
    return(expr) {
        return { kind: "return", expr };
    },
    obj(entries) {
        if (Array.isArray(entries)) {
            return { kind: "objectLiteral", entries };
        }
        return {
            kind: "objectLiteral",
            entries: Object.entries(entries).map(([key, value]) => ({
                spread: false,
                key,
                value,
            })),
        };
    },
    setSpread(expr) {
        return { spread: true, expr };
    },
    set(key, value) {
        return { spread: false, key, value };
    },
    setComputed(key, value) {
        return { spread: false, computed: true, key, value };
    },
    arr(items) {
        return { kind: "arrayLiteral", items };
    },
    template(parts) {
        return { kind: "templateLit", parts };
    },
    if(condition, body, opts) {
        return {
            kind: "if",
            condition,
            body,
            elseIfs: opts?.elseIfs ?? [],
            elseBody: opts?.elseBody,
        };
    },
    forOf(varName, iterable, body) {
        return { kind: "for", variant: "of", varName, iterable, body };
    },
    forC(init, condition, update, body) {
        return { kind: "for", variant: "cStyle", init, condition, update, body };
    },
    while(condition, body) {
        return { kind: "while", condition, body };
    },
    switch(discriminant, cases) {
        return { kind: "switch", discriminant, cases };
    },
    tryCatch(tryBody, catchBody, catchParam, finallyBody) {
        return { kind: "tryCatch", tryBody, catchParam, catchBody, finallyBody };
    },
    binOp(left, op, right, opts) {
        return {
            kind: "binOp",
            left,
            op,
            right,
            parenLeft: opts?.parenLeft,
            parenRight: opts?.parenRight,
        };
    },
    prop(object, property, opts) {
        return { kind: "propertyAccess", object, property, computed: false, ...(opts?.optional && { optional: true }) };
    },
    index(object, property, opts) {
        return { kind: "propertyAccess", object, property, computed: true, ...(opts?.optional && { optional: true }) };
    },
    spread(expr) {
        return { kind: "spread", expr };
    },
    id(name) {
        return { kind: "identifier", name };
    },
    str(value) {
        return { kind: "stringLiteral", value };
    },
    num(value) {
        return { kind: "numericLiteral", value };
    },
    bool(value) {
        return { kind: "booleanLiteral", value };
    },
    comment(text, block = false) {
        return { kind: "comment", text, block };
    },
    export(decl, names) {
        return { kind: "export", decl, names };
    },
    new(callee, args = []) {
        return { kind: "newExpr", callee, arguments: args };
    },
    validateType(value, zodSchema) {
        return ts.call(ts.id("__validateType"), [value, zodSchema]);
    },
    /**
     * `await __validateChainRecursive(value, <descriptor>)` — used at
     * `!` sites whose resolved type carries at least one `@validate(...)` tag
     * anywhere in the tree. The descriptor is a TS expression built via
     * `buildValidationDescriptor(...)`. Validators read `ctx` from the
     * active `agencyStore` ALS frame, so no explicit ctx arg is threaded.
     */
    validateChainRecursive(value, descriptor) {
        return ts.awaitCall(ts.id("__validateChainRecursive"), [
            value,
            descriptor,
        ]);
    },
    scopedVar(name, scope, moduleId) {
        return { kind: "scopedVar", name, scope, moduleId };
    },
    functionReturn(value) {
        return ts.statements([
            ts.assign(ts.id("__functionCompleted"), ts.bool(true)),
            ts.runnerHalt(value),
        ]);
    },
    // ── Runner-based step builders ──
    runnerStep(opts) {
        return { kind: "runnerStep", ...opts };
    },
    runnerThread(opts) {
        const res = { kind: "runnerThread", ...opts };
        return res;
    },
    runnerHandle(opts) {
        return { kind: "runnerHandle", ...opts };
    },
    /** Emit `await runner.hook(id, async () => { ...body... })` — a
     *  substep-counter-idempotent wrapper for codegen-emitted callback
     *  hook sites. See `Runner.hook`'s JSDoc. */
    runnerHookStep(opts) {
        return { kind: "runnerHookStep", ...opts };
    },
    withHandler(handler, body) {
        return { kind: "withHandler", handler, body };
    },
    runnerIfElse(opts) {
        return { kind: "runnerIfElse", ...opts };
    },
    runnerLoop(opts) {
        return { kind: "runnerLoop", ...opts };
    },
    runnerWhileLoop(opts) {
        return { kind: "runnerWhileLoop", ...opts };
    },
    runnerBranchStep(opts) {
        return { kind: "runnerBranchStep", ...opts };
    },
    runnerDebugger(opts) {
        return { kind: "runnerDebugger", ...opts };
    },
    runnerPipe(opts) {
        return { kind: "runnerPipe", ...opts };
    },
    empty() {
        return { kind: "empty" };
    },
    break() {
        return { kind: "break" };
    },
    continue() {
        return { kind: "continue" };
    },
    postfix(operand, op) {
        return { kind: "postfixOp", operand, op };
    },
    // --- Semantic convenience builders (no new IR types) ---
    /** Call runner.halt(value) and return from the current callback */
    runnerHalt(value) {
        return ts.statements([
            ts.methodCall(ts.id("runner"), "halt", [value]),
            ts.return(),
        ]);
    },
    /** Halt with { messages: __threads(), data: value } and return from a graph node callback */
    nodeResult(value) {
        return ts.runnerHalt(ts.obj({ messages: ts.runtime.threads, data: value }));
    },
    /**
     * Wrap a list of statements in
     * `await agencyStore.run({ ctx, stack, threads }, async () => { ... })`.
     *
     * Defense-in-depth: every function/node body's try block carries an
     * ALS frame so stdlib helpers and `__threads()` / `__stateStack()`
     * reads resolve correctly even for code that runs between Runner
     * steps. Today the gap is empty (every callback emission uses
     * `runner.step`/`runner.hook`/etc. which re-seed the frame
     * themselves), but the wrap makes the contract explicit and
     * removes the risk that a future refactor silently loses the
     * per-scope frame.
     *
     * NOTE: `return` statements inside the wrapped body escape only the
     * inner async callback, not the outer function. Validation guards
     * that return a non-`undefined` value (e.g. `return __vr_x` for a
     * validation failure) must stay outside the wrap so the outer
     * function actually returns the value to its caller. The bare
     * `return;` emitted by `runnerHalt` is fine: after the callback
     * returns, the outer `if (runner.halted) return runner.haltResult;`
     * check picks up the halted result.
     */
    withAlsFrame({ ctx, stack, threads, body, }) {
        // Spread the outer ALS frame first so non-overridden slots —
        // notably `moduleDir`, which is seeded once by generated code in
        // `runNode` / `runInBootstrapFrame` — are inherited here. Without
        // the spread, stdlib helpers invoked from inside this body frame
        // would see `moduleDir = undefined` and fall back to
        // `process.cwd()`, breaking the "resolve relative paths against
        // the compiled module dir" contract for `read` / `dirname()` /
        // `_readSkill` / etc.
        return ts.awaitCall(ts.prop(ts.id("agencyStore"), "run"), [
            ts.obj([
                ts.setSpread(ts.call(ts.id("getRuntimeContext"))),
                ts.set("ctx", ctx),
                ts.set("stack", stack),
                ts.set("threads", threads),
            ]),
            ts.arrowFn([], ts.statements(body), { async: true }),
        ]);
    },
    env(varName) {
        return ts.raw(`__process.env[${JSON.stringify(varName)}]`);
    },
    /**
     * Emit `await callHook({ name, data })`.
     *
     * `callHook` reads `ctx` from the active `agencyStore` frame, so the
     * codegen no longer needs to thread `__ctx` through every emission
     * site. Callback bodies cannot raise interrupts (statically forbidden
     * by the typechecker — see `checkCallbackBodyInterrupts`). `callHook`
     * returns `void`; the codegen-emitted hook sites fire-and-forget.
     */
    callHook(hookName, data) {
        const dataNode = "kind" in data ? data : ts.obj(data);
        return ts.awaitCall(ts.id("callHook"), [
            ts.obj({
                name: ts.str(hookName),
                data: dataNode,
            }),
        ]);
    },
    setupEnv({ stack, step, self, ctx, }) {
        // No `__threads` or `__stateStack` const declaration here: those
        // per-scope values are now read on demand via the `__threads()`
        // and `__stateStack()` accessors (which resolve through the active
        // `agencyStore` ALS frame). The `__graph` and `statelogClient`
        // locals that used to be declared here were dead code — no
        // template or codegen path referenced them — so they were dropped
        // entirely. See `ts.runtime.threads` / `ts.runtime.stateStack` and
        // `lib/runtime/asyncContext.ts`.
        return ts.statements([
            ts.constDeclId(ts.runtime.stack, stack),
            ts.constDeclId(ts.runtime.step, step),
            ts.constDeclId(ts.runtime.self, self),
            // `ts.runtime.ctx` is now the `__ctx()` accessor; the const-decl
            // target must be the literal identifier `__ctx`. The local stays
            // because pre-wrap code (Runner ctor, withAlsFrame seed,
            // __initializeGlobals call) needs a lexical handle to seed ALS.
            ts.constDeclId(ts.id("__ctx"), ctx),
            ts.letDecl("__forked"),
            // Track whether the function completed normally (vs pausing for a debug interrupt).
            // Used in the finally block to decide whether to fire onFunctionEnd.
            ts.letDecl("__functionCompleted", ts.bool(false)),
        ]);
    },
    time(varName) {
        return ts.letDecl(varName, ts.methodCall(ts.id("performance"), "now"), "number");
    },
    stack(varName) {
        return ts.prop(ts.runtime.stack, varName);
    },
    ctx(varName) {
        return ts.prop(ts.runtime.ctx, varName);
    },
    self(varName) {
        return ts.prop(ts.runtime.self, varName);
    },
    throw(code) {
        return ts.raw(`throw ${code}`);
    },
    and(...conditions) {
        return { kind: "and", operands: conditions };
    },
    or(...conditions) {
        return { kind: "or", operands: conditions };
    },
    not(condition) {
        return { kind: "not", operand: condition };
    },
    unaryOp(op, operand, opts) {
        return { kind: "unaryOp", op, operand, paren: opts?.paren };
    },
    functionCallConfig({ ctx, threads, stateStack, moduleId, scopeName, stepPath, }) {
        const entries = {
            ctx,
        };
        if (threads) {
            entries.threads = threads;
        }
        if (stateStack) {
            entries.stateStack = stateStack;
        }
        if (moduleId) {
            entries.moduleId = moduleId;
        }
        if (scopeName) {
            entries.scopeName = scopeName;
        }
        if (stepPath) {
            entries.stepPath = stepPath;
        }
        return ts.obj(entries);
    },
    newThreadStore() {
        return ts.new(ts.id("ThreadStore"));
    },
    smoltalkSystemMessage(args) {
        return ts.methodCall(ts.id("smoltalk"), "systemMessage", args);
    },
    smoltalkUserMessage(args) {
        return ts.methodCall(ts.id("smoltalk"), "userMessage", args);
    },
    smoltalkAssistantMessage(args) {
        return ts.methodCall(ts.id("smoltalk"), "assistantMessage", args);
    },
    goToNode(nodeName, args) {
        return ts.call(ts.id("goToNode"), [ts.str(nodeName), args]);
    },
    nodeReturn({ messages, data }) {
        return ts.statements([
            ts.methodCall(ts.id("runner"), "halt", [ts.obj({ messages, data })]),
            ts.raw("return"),
        ]);
    },
    jsonStringify(value) {
        return ts.methodCall(ts.id("JSON"), "stringify", [value]);
    },
    consoleLog(...args) {
        return ts.methodCall(ts.id("console"), "log", args);
    },
    consoleWarn(...args) {
        return ts.methodCall(ts.id("console"), "warn", args);
    },
    consoleError(...args) {
        return ts.methodCall(ts.id("console"), "error", args);
    },
    /*
            ? ts.obj([ts.spread(ts.runtime.state), ts.set("data", ts.id(tempVar))])
          : ts.obj({ data: ts.id(tempVar) }); */
    /*
  
    functionReturn(value: TsNode): TsReturn {
      return ts.return(ts.obj({ data: value }));
    }, */
    /** GlobalStore operations.
     *
     *  Receiver defaults to `getRuntimeContext().ctx` (strict accessor —
     *  throws clearly if no ALS frame). Pass `ctxRef` explicitly when the
     *  emission site has a different lexical context — most notably the
     *  `__initializeGlobals(__ctx)` helper body in sectionAssembler.ts,
     *  where `__ctx` is a parameter and ALS isn't yet installed; pass
     *  `ts.id("__ctx")` there. Routing through the strict accessor by
     *  default keeps the no-frame failure mode loud and aligns with the
     *  two-flavor rule in docs/dev/codegen-als-accessors.md. */
    globalGet(moduleId, varName, ctxRef = ts.raw("getRuntimeContext().ctx")) {
        return ts.methodCall(ts.prop(ctxRef, "globals"), "get", [ts.str(moduleId), ts.str(varName)]);
    },
    globalSet(moduleId, varName, value, ctxRef = ts.raw("getRuntimeContext().ctx")) {
        return ts.methodCall(ts.prop(ctxRef, "globals"), "set", [ts.str(moduleId), ts.str(varName), value]);
    },
    ternary(condition, trueExpr, falseExpr) {
        return { kind: "ternary", condition, trueExpr, falseExpr };
    },
    agencyFunctionWrap(fn, name, module, params) {
        return { kind: "agencyFunctionWrap", name, module, fn, params };
    },
    /** Predefined runtime identifiers. `threads` and `stateStack` are
     *  `__threads()` / `__stateStack()` accessor calls (not bare
     *  identifiers) because post-ALS migration the per-scope
     *  `ThreadStore` and `StateStack` live on the active `agencyStore`
     *  frame instead of in codegen-emitted `const __threads` / `const
     *  __stateStack` locals. Every site that referenced the old locals
     *  now emits the accessor call, which reads from ALS and returns
     *  the live store (or `undefined` outside any frame — see
     *  `runtime/asyncContext.ts`).
     *
     *  `ctx` is `getRuntimeContext().ctx` rather than a bare `__ctx()`
     *  accessor call: the per-scope `const __ctx = __state?.ctx ||
     *  __globalCtx;` local emitted by `setupEnv` would shadow a bare
     *  `__ctx` accessor identifier — and the esbuild TS transform
     *  rewrites *every* `__ctx` reference in a scope (import-bound
     *  ones included) to match the renamed local, so `__ctx()` would
     *  become `__ctx2()` and crash with `__ctx2 is not a function`.
     *  Routing through `getRuntimeContext().ctx` avoids the shadow
     *  entirely. Pre-wrap, parameter-context, and seed sites still use
     *  `ts.id("__ctx")` directly to reach the setupEnv local. */
    runtime: {
        self: { kind: "identifier", name: "__self" },
        ctx: { kind: "raw", code: "getRuntimeContext().ctx" },
        threads: { kind: "raw", code: "__threads()" },
        stateStack: { kind: "raw", code: "__stateStack()" },
        stack: { kind: "identifier", name: "__stack" },
        step: { kind: "identifier", name: "__step" },
        state: { kind: "identifier", name: "__state" },
        globalCtx: { kind: "identifier", name: "__globalCtx" },
        client: { kind: "identifier", name: "__client" },
    },
    /** Thread operations */
    threads: {
        create() {
            return ts.call(ts.prop(ts.runtime.threads, "create"));
        },
        createAndReturnThread() {
            return ts.call(ts.prop(ts.runtime.threads, "createAndReturnThread"));
        },
        createSubthread() {
            return ts.call(ts.prop(ts.runtime.threads, "createSubthread"));
        },
        createAndReturnSubthread() {
            return ts.call(ts.prop(ts.runtime.threads, "createAndReturnSubthread"));
        },
        get(id) {
            return ts.call(ts.prop(ts.runtime.threads, "get"), [id]);
        },
        active() {
            return ts.call(ts.prop(ts.runtime.threads, "active"));
        },
        getOrCreateActive() {
            return ts.call(ts.prop(ts.runtime.threads, "getOrCreateActive"));
        },
    },
};
