import { z } from "zod";
import { stripBoundParams } from "./stripBoundParams.js";
import { approve } from "./interrupts.js";
import { agencyStore, withPushedHandler } from "./asyncContext.js";
export const UNSET = Symbol("UNSET");
export class AgencyFunction {
    __agencyFunction = true;
    name;
    module;
    params;
    toolDefinition;
    _fn;
    _unboundParams;
    _nonVariadicUnbound;
    _hasVariadic;
    _isBound;
    exported;
    safe;
    _isPreapproved;
    constructor(opts) {
        this.name = opts.name;
        this.module = opts.module;
        this._fn = opts.fn;
        this.params = opts.params;
        this.toolDefinition = opts.toolDefinition;
        this.exported = opts.exported ?? false;
        this.safe = opts.safe ?? false;
        this._isPreapproved = opts.isPreapproved ?? false;
        this._unboundParams = opts.params.filter(p => !p.isBound);
        this._nonVariadicUnbound = this._unboundParams.filter(p => !p.variadic);
        this._hasVariadic = this._unboundParams.length > 0 && this._unboundParams[this._unboundParams.length - 1].variadic;
        this._isBound = opts.params.some(p => p.isBound);
    }
    get isPreapproved() {
        return this._isPreapproved;
    }
    get boundArgs() {
        if (!this._isBound)
            return null;
        const indices = [];
        const values = [];
        for (let i = 0; i < this.params.length; i++) {
            if (this.params[i].isBound) {
                indices.push(i);
                values.push(this.params[i].boundValue);
            }
        }
        return { indices, values, originalParams: this.params };
    }
    withToolDefinition(toolDefinition) {
        return new AgencyFunction({
            name: this.name,
            module: this.module,
            fn: this._fn,
            params: this.params,
            toolDefinition,
            exported: this.exported,
            safe: this.safe,
            isPreapproved: this._isPreapproved,
        });
    }
    getOriginalParams() {
        return this.params;
    }
    getUnboundParams() {
        return this._unboundParams;
    }
    async invoke(descriptor) {
        const args = this._isBound
            ? this.mergeWithBound(this.resolveArgs(descriptor))
            : this.resolveArgs(descriptor);
        return this._fn(...args);
    }
    partial(bindings) {
        if (Object.keys(bindings).length === 0)
            return this;
        // Single pass: validate bindings against full param list
        for (const name of Object.keys(bindings)) {
            const param = this.params.find(p => p.name === name);
            if (!param) {
                throw new Error(`Unknown parameter '${name}' in .partial() call`);
            }
            if (param.isBound) {
                throw new Error(`Parameter '${name}' is already bound`);
            }
            if (param.variadic) {
                throw new Error(`Variadic parameter '${name}' cannot be bound`);
            }
        }
        // Build new params with bound values set
        const newParams = this.params.map(p => {
            if (p.name in bindings) {
                return { ...p, isBound: true, boundValue: bindings[p.name] };
            }
            return p;
        });
        const unboundParams = newParams.filter(p => !p.isBound);
        // Build reduced tool definition if one exists
        const boundNames = Object.keys(bindings);
        const newToolDef = this.toolDefinition
            ? {
                ...this.toolDefinition,
                description: stripBoundParams(this.toolDefinition.description, boundNames),
                schema: buildReducedSchema(this.toolDefinition.schema, unboundParams),
            }
            : null;
        return new AgencyFunction({
            name: this.name,
            module: this.module,
            fn: this._fn,
            params: newParams,
            toolDefinition: newToolDef,
            exported: this.exported,
            safe: this.safe,
            isPreapproved: this._isPreapproved,
        });
    }
    preapprove() {
        if (this._isPreapproved)
            return this;
        // Wrap `_fn` in a `withPushedHandler` that installs an
        // auto-approve handler for the duration of every call. The handler
        // intercepts any `request_approval` interrupt the body raises so
        // preapproved tools never bubble approval prompts up to the user.
        // Doing the wrap here — rather than branching inside `invoke()` —
        // keeps the per-call hot path branch-free.
        const original = this._fn;
        const wrapped = (...args) => {
            const ctx = agencyStore.getStore()?.ctx;
            if (!ctx)
                return original(...args);
            return withPushedHandler(ctx, async () => approve(), () => Promise.resolve(original(...args)));
        };
        return new AgencyFunction({
            name: this.name,
            module: this.module,
            fn: wrapped,
            params: this.params,
            toolDefinition: this.toolDefinition,
            exported: this.exported,
            safe: this.safe,
            isPreapproved: true,
        });
    }
    describe(description) {
        const newToolDef = this.toolDefinition
            ? { ...this.toolDefinition, description }
            : { name: this.name, description, schema: null };
        return this.withToolDefinition(newToolDef);
    }
    mergeWithBound(unboundArgs) {
        const fullArgs = [];
        let unboundIdx = 0;
        for (const param of this.params) {
            if (param.isBound) {
                fullArgs.push(param.boundValue);
            }
            else {
                fullArgs.push(unboundArgs[unboundIdx++]);
            }
        }
        return fullArgs;
    }
    resolveArgs(descriptor) {
        if (descriptor.type === "positional") {
            return this.resolvePositional(descriptor.args);
        }
        return this.resolveNamed(descriptor.positionalArgs, descriptor.namedArgs, descriptor.blockArg);
    }
    resolvePositional(args) {
        // Fast path: exact arg count, no variadics — no work needed
        if (args.length === this._nonVariadicUnbound.length && !this._hasVariadic) {
            return args;
        }
        // Pad missing optional args with UNSET
        let result = args;
        if (args.length < this._nonVariadicUnbound.length) {
            result = [...args];
            for (let i = args.length; i < this._nonVariadicUnbound.length; i++) {
                if (!this._nonVariadicUnbound[i].hasDefault)
                    break;
                result.push(UNSET);
            }
        }
        // Wrap trailing args for variadic param
        if (this._hasVariadic) {
            const nonVariadicCount = this._nonVariadicUnbound.length;
            const regularArgs = result.slice(0, nonVariadicCount);
            const variadicArgs = result.slice(nonVariadicCount);
            regularArgs.push(variadicArgs);
            return regularArgs;
        }
        return result;
    }
    resolveNamed(positionalArgs, namedArgs, blockArg) {
        // Validate named args: no unknowns, no conflicts with positional
        for (const name of Object.keys(namedArgs)) {
            const paramIdx = this._nonVariadicUnbound.findIndex(p => p.name === name);
            if (paramIdx === -1) {
                throw new Error(`Unknown named argument '${name}' in call to '${this.name}'`);
            }
            if (paramIdx < positionalArgs.length) {
                throw new Error(`Named argument '${name}' conflicts with positional argument at position ${paramIdx + 1} in call to '${this.name}'`);
            }
        }
        // A trailing block argument (from `f(name: val) as { ... }` syntax)
        // binds to the LAST non-variadic unbound parameter. By convention
        // that's the block-typed param. We synthesize it into namedArgs
        // here so the rest of the fill logic treats it uniformly.
        const hasBlock = blockArg !== undefined;
        if (hasBlock) {
            const lastParam = this._nonVariadicUnbound[this._nonVariadicUnbound.length - 1];
            if (!lastParam) {
                throw new Error(`Call to '${this.name}' passed a trailing block but the function takes no parameters`);
            }
            if (Object.hasOwn(namedArgs, lastParam.name)) {
                throw new Error(`Trailing block conflicts with named argument '${lastParam.name}' in call to '${this.name}'`);
            }
            namedArgs = { ...namedArgs, [lastParam.name]: blockArg };
        }
        // Build result: positional args first, then fill from named args in parameter order
        const result = [...positionalArgs];
        for (let i = positionalArgs.length; i < this._nonVariadicUnbound.length; i++) {
            const param = this._nonVariadicUnbound[i];
            if (Object.hasOwn(namedArgs, param.name)) {
                result.push(namedArgs[param.name]);
            }
            else if (param.hasDefault) {
                const hasLaterNamedArg = this._nonVariadicUnbound
                    .slice(i + 1)
                    .some(p => p.name in namedArgs);
                if (hasLaterNamedArg) {
                    result.push(UNSET);
                }
                else {
                    break;
                }
            }
            else {
                throw new Error(`Missing required argument '${param.name}' in call to '${this.name}'`);
            }
        }
        // Apply variadic wrapping and default padding
        return this.resolvePositional(result);
    }
    // Serialization handled by FunctionRefReviver — toJSON() would conflict with the replacer pattern.
    static isAgencyFunction(value) {
        return typeof value === "object" && value !== null
            && value.__agencyFunction === true;
    }
    static create(opts, registry) {
        const fn = new AgencyFunction(opts);
        // Composite `${module}:${name}` key so two helpers with the same
        // name in different modules can coexist in the shared global
        // registry that `FunctionRefReviver` reads from.
        registry[`${opts.module}:${opts.name}`] = fn;
        return fn;
    }
}
function buildReducedSchema(originalSchema, unboundParams) {
    if (!originalSchema || !originalSchema.shape)
        return originalSchema;
    const unboundNames = unboundParams.map(p => p.name);
    const shape = originalSchema.shape;
    const reducedShape = {};
    for (const [key, value] of Object.entries(shape)) {
        if (unboundNames.includes(key)) {
            reducedShape[key] = value;
        }
    }
    return z.object(reducedShape);
}
