import { ts } from "../../ir/builders.js";
/**
 * Lowers `|>` pipe chains into runner steps.
 *
 * A pipe chain `a |> f |> g` is parsed left-associatively as
 * `(a |> f) |> g`. This emitter flattens that tree into ordered stages
 * (`[a, f, g]`) and produces one runner step per stage so that interrupts
 * resume between stages without replaying the earlier ones.
 *
 * Public API:
 *   - {@link tryGetChainStages} — flatten an assignment-of-pipe into stages
 *   - {@link expand}           — turn an assignment + stages into runner nodes
 *   - {@link bind}             — lower a bare `lhs |> rhs` expression
 */
export class PipeChainEmitter {
    deps;
    counter = 0;
    constructor(deps) {
        this.deps = deps;
    }
    /**
     * Walk a left-recursive `|>` tree under an assignment and return
     * [initial, stage1, stage2, ...] in evaluation order.
     * Returns null if the node is not an assignment whose value is a pipe chain.
     */
    tryGetChainStages(node) {
        if (node.type !== "assignment")
            return null;
        const expr = node.value;
        if (expr.type !== "binOpExpression" || expr.operator !== "|>")
            return null;
        const stages = [];
        let current = expr;
        while (current.type === "binOpExpression" &&
            current.operator === "|>") {
            stages.push(current.right);
            current = current.left;
        }
        stages.push(current);
        return stages.reverse();
    }
    /**
     * Lower a bare pipe expression `left |> stage` (not an assignment) into
     * `await __pipeBind(left, async (__pipeArg) => stage(__pipeArg))`.
     */
    bind(leftIR, stage) {
        return ts.await(ts.call(ts.raw("__pipeBind"), [leftIR, this.buildPipeLambda(stage)]));
    }
    /**
     * Expand a pipe-chain assignment into one runner node per stage.
     *
     * Layout (for `x = a |> f |> g`):
     *   step baseId+0:   __pipe_N = a
     *   pipe baseId+1:   __pipe_N = await __pipeBind(__pipe_N, lambda(f))
     *   pipe baseId+2:   x        = await __pipeBind(__pipe_N, lambda(g))
     *
     * If the assignment is validated (`x!: T = ...`), an extra runner step
     * wraps the final result in `__validateType`.
     */
    expand(stmt, stages, baseId) {
        const tempName = `__pipe_${this.counter++}`;
        const tempVar = ts.scopedVar(tempName, "local");
        const targetVar = this.deps.buildAssignmentLhs(stmt.scope, stmt.variableName, stmt.accessChain);
        const nodes = [];
        nodes.push(ts.runnerStep({
            id: baseId,
            body: [ts.assign(tempVar, this.deps.processNode(stages[0]))],
        }));
        for (let i = 1; i < stages.length - 1; i++) {
            nodes.push(ts.runnerPipe({
                id: baseId + i,
                target: tempVar,
                input: tempVar,
                fn: this.buildPipeLambda(stages[i]),
            }));
        }
        const lastIdx = stages.length - 1;
        nodes.push(ts.runnerPipe({
            id: baseId + lastIdx,
            target: targetVar,
            input: tempVar,
            fn: this.buildPipeLambda(stages[lastIdx]),
        }));
        if (stmt.validated && stmt.typeHint) {
            const zodSchema = this.deps.zodSchemaFor(stmt.typeHint);
            nodes.push(ts.runnerStep({
                id: baseId + stages.length,
                body: [ts.assign(targetVar, ts.validateType(targetVar, ts.raw(zodSchema)))],
            }));
        }
        return nodes;
    }
    // ── Internal: arrow-function construction per stage ──
    buildPipeLambda(stage) {
        const pipeArg = ts.raw("__pipeArg");
        if (stage.type === "valueAccess") {
            const lastElement = stage.chain[stage.chain.length - 1];
            // Method call with args (e.g. multiply.partial(a: 3)):
            // call the method first to produce a function, then invoke with piped value
            if (lastElement?.kind === "methodCall" && lastElement.functionCall.arguments.length > 0) {
                const fnExpr = this.deps.processNode(stage);
                const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
                const config = this.deps.buildStateConfig();
                const callArgs = [fnExpr, descriptor];
                if (config)
                    callArgs.push(config);
                const callExpr = ts.call(ts.id("__call"), callArgs);
                return ts.arrowFn([{ name: "__pipeArg" }], ts.await(callExpr), { async: true });
            }
            // No placeholder: bare method/property reference — use __callMethod to preserve `this`
            const receiver = this.processValueAccessPartial(stage);
            const lastEl = stage.chain[stage.chain.length - 1];
            const propName = lastEl.kind === "property" ? lastEl.name
                : lastEl.kind === "methodCall" ? lastEl.functionCall.functionName
                    : null;
            if (propName) {
                const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
                const config = this.deps.buildStateConfig();
                const callArgs = [receiver, ts.str(propName), descriptor];
                if (config)
                    callArgs.push(config);
                const callExpr = ts.call(ts.id("__callMethod"), callArgs);
                return ts.arrowFn([{ name: "__pipeArg" }], ts.await(callExpr), { async: true });
            }
            // Fallback for non-property access (e.g. index): use __call
            const callee = this.deps.processNode(stage);
            const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
            const config = this.deps.buildStateConfig();
            const callArgs = [callee, descriptor];
            if (config)
                callArgs.push(config);
            const callExpr = ts.call(ts.id("__call"), callArgs);
            return ts.arrowFn([{ name: "__pipeArg" }], ts.await(callExpr), { async: true });
        }
        if (stage.type === "variableName" || stage.type === "functionCall") {
            return ts.arrowFn([{ name: "__pipeArg" }], this.buildPipeStageBody(stage), { async: true });
        }
        if (stage.type === "binOpExpression" && stage.operator === "catch") {
            const innerBody = this.buildPipeStageBody(stage.left);
            const fallback = this.deps.processNode(stage.right);
            const wrapped = ts.await(ts.call(ts.id("__catchResult"), [
                innerBody,
                ts.arrowFn([], ts.statements([ts.return(fallback)]), { async: true }),
            ]));
            return ts.arrowFn([{ name: "__pipeArg" }], wrapped, { async: true });
        }
        throw new Error(`Invalid pipe stage type: ${stage.type}`);
    }
    /**
     * Build the body expression for a pipe stage (without the outer arrow
     * function wrapper). Returns `await __call(...)` — the caller wraps this
     * in an arrow function or `__catchResult(...)`.
     */
    buildPipeStageBody(stage) {
        const pipeArg = ts.raw("__pipeArg");
        if (stage.type === "variableName") {
            const callee = this.deps.processNode(stage);
            const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
            const config = this.deps.buildStateConfig();
            const callArgs = [callee, descriptor];
            if (config)
                callArgs.push(config);
            return ts.await(ts.call(ts.id("__call"), callArgs));
        }
        if (stage.type === "functionCall") {
            throw new Error(`Function call '${stage.functionName}(...)' cannot appear as a pipe stage. Use .partial() to bind arguments, e.g. ${stage.functionName}.partial(...)`);
        }
        if (stage.type === "valueAccess") {
            const lastElement = stage.chain[stage.chain.length - 1];
            if (lastElement?.kind === "methodCall" && lastElement.functionCall.arguments.length > 0) {
                // e.g. map.partial(func: \x -> x * 2) — call the method, then invoke result with piped value
                const fnExpr = this.deps.processNode(stage);
                const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
                const config = this.deps.buildStateConfig();
                const callArgs = [fnExpr, descriptor];
                if (config)
                    callArgs.push(config);
                return ts.await(ts.call(ts.id("__call"), callArgs));
            }
            // Bare method/property reference — use __callMethod
            const receiver = this.processValueAccessPartial(stage);
            const lastEl = stage.chain[stage.chain.length - 1];
            const propName = lastEl.kind === "property" ? lastEl.name
                : lastEl.kind === "methodCall" ? lastEl.functionCall.functionName
                    : null;
            if (propName) {
                const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
                const config = this.deps.buildStateConfig();
                const callArgs = [receiver, ts.str(propName), descriptor];
                if (config)
                    callArgs.push(config);
                return ts.await(ts.call(ts.id("__callMethod"), callArgs));
            }
            const callee = this.deps.processNode(stage);
            const descriptor = ts.obj({ type: ts.str("positional"), args: ts.arr([pipeArg]) });
            const config = this.deps.buildStateConfig();
            const callArgs = [callee, descriptor];
            if (config)
                callArgs.push(config);
            return ts.await(ts.call(ts.id("__call"), callArgs));
        }
        throw new Error(`Unsupported pipe stage type in catch: ${stage.type}`);
    }
    /**
     * Process a ValueAccess up to but not including the last chain element.
     * Used by pipe lowering to get the receiver for a method call —
     * the LAST chain element is the pipe stage's method name, which the
     * caller hands to `__callMethod(receiver, name, …)` directly.
     *
     * Delegates to the builder's full `processValueAccess` (via deps) on
     * a synthetic node with `chain.slice(0, -1)`. This keeps every
     * subtlety of receiver lowering in one place — awaiting + paren-
     * wrapping a `functionCall` base, paren-wrapping a non-trivial base,
     * propagating the `optional` flag through chain elements, handling
     * the `call` chain-element kind, etc. Previously this method
     * duplicated the chain-walk and silently dropped all of those
     * behaviours; see the "pipe receiver precedence" issue for the
     * shapes that were broken (`getObj().foo.bar |> stage`, `obj?.foo
     * |> stage`, `factories.makeOne().bar |> stage`, etc.).
     */
    processValueAccessPartial(node) {
        return this.deps.processValueAccess({
            ...node,
            chain: node.chain.slice(0, -1),
        });
    }
}
