import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { ToolcraftBugError, UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
import { writeErrorReport } from "./error-report.js";
import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
import { invokeWithHumanInLoop } from "./human-in-loop/index.js";
import { hasMcpProxyGroups, resolveMcpProxies } from "./mcp-proxy.js";
import { getExpectedNumberDescription, isValidNumberSchemaValue } from "./number-schema.js";
import { filterSchemaForScope } from "./schema-scope.js";
import { enableSourceMaps } from "./stack-trim.js";
import { suggest } from "./suggest.js";
import { throwValidationErrors } from "./validation-errors.js";
const RESERVED_SERVICE_NAMES = new Set([
    "params",
    "secrets",
    "fetch",
    "fs",
    "env",
    "progress",
    "runtimeOptions",
    "root"
]);
const RESERVED_SERVICE_NAMES_MESSAGE = "Available reserved names: params, secrets, fetch, fs, env, progress, runtimeOptions, root.";
function splitWords(value) {
    const words = [];
    let current = "";
    for (let index = 0; index < value.length; index += 1) {
        const char = value[index] ?? "";
        const lower = char.toLowerCase();
        const upper = char.toUpperCase();
        const isSeparator = char === "-" || char === "_" || char === " " || char === ".";
        if (isSeparator) {
            if (current.length > 0) {
                words.push(current.toLowerCase());
                current = "";
            }
            continue;
        }
        const isUppercase = char !== lower && char === upper;
        const previous = value[index - 1];
        const next = value[index + 1];
        const previousIsLowercase = previous !== undefined &&
            previous === previous.toLowerCase() &&
            previous !== previous.toUpperCase();
        const nextIsLowercase = next !== undefined && next === next.toLowerCase() && next !== next.toUpperCase();
        if (isUppercase && current.length > 0 && (previousIsLowercase || nextIsLowercase)) {
            words.push(current.toLowerCase());
            current = char;
            continue;
        }
        current += char;
    }
    if (current.length > 0) {
        words.push(current.toLowerCase());
    }
    return words;
}
function formatSegment(segment) {
    return splitWords(segment)
        .map((word, index) => (index === 0 ? word : `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`))
        .join("");
}
function unwrapOptional(schema) {
    if (schema.kind === "optional") {
        return unwrapOptional(schema.inner);
    }
    return schema;
}
function isOptional(schema) {
    return schema.kind === "optional";
}
function isPlainObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function createFs() {
    return {
        readFile: async (path, encoding = "utf8") => readFile(path, { encoding }),
        writeFile: async (path, contents, options) => {
            await writeFile(path, contents, options);
        },
        exists: async (path) => {
            try {
                await access(path);
                return true;
            }
            catch {
                return false;
            }
        },
        lstat: async (path) => lstat(path),
        rename: async (fromPath, toPath) => rename(fromPath, toPath),
        unlink: async (path) => unlink(path)
    };
}
function createEnv(values = process.env) {
    return {
        get(key) {
            return values[key];
        }
    };
}
function validateServices(services) {
    for (const name of Object.keys(services)) {
        if (RESERVED_SERVICE_NAMES.has(name)) {
            throw new Error(`Service name "${name}" is reserved. Choose a different name. ${RESERVED_SERVICE_NAMES_MESSAGE}`);
        }
    }
}
function formatAvailableList(values) {
    return `Available: ${[...values].sort().join(", ")}.`;
}
function formatEnumError(value, schema, label) {
    const suggestionLine = typeof value === "string"
        ? formatEnumSuggestionLine(value, schema.values.map((candidate) => String(candidate)))
        : " ";
    return `Invalid value for "${label}".${suggestionLine}Expected one of: ${schema.values.map((candidate) => String(candidate)).join(", ")}, got ${describeReceived(value)}.`;
}
function formatEnumSuggestionLine(value, values) {
    const suggestions = suggest(value, values);
    return suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?\n` : " ";
}
function describeReceived(value) {
    if (value === null)
        return "null";
    if (value === undefined)
        return "missing";
    if (Array.isArray(value))
        return `array(${value.length})`;
    if (typeof value === "object")
        return "object";
    if (typeof value === "string") {
        const s = value.length > 40 ? `${value.slice(0, 40)}…` : value;
        return `${JSON.stringify(s)}`;
    }
    return JSON.stringify(value);
}
function validateSchemaValue(schema, value, label, errors) {
    const unwrappedSchema = unwrapOptional(schema);
    if (value === null && unwrappedSchema.nullable === true) {
        return null;
    }
    switch (unwrappedSchema.kind) {
        case "string":
            if (typeof value !== "string") {
                errors.push({
                    path: label,
                    message: `Invalid value for "${label}". Expected a string, got ${describeReceived(value)}.`
                });
            }
            else {
                validateStringConstraints(unwrappedSchema, value, label, errors);
            }
            return value;
        case "number":
            if (!isValidNumberSchemaValue(value, unwrappedSchema)) {
                errors.push({
                    path: label,
                    message: `Invalid value for "${label}". Expected ${getExpectedNumberDescription(unwrappedSchema)}, got ${describeReceived(value)}.`
                });
            }
            return value;
        case "boolean":
            if (typeof value !== "boolean") {
                errors.push({
                    path: label,
                    message: `Invalid value for "${label}". Expected a boolean, got ${describeReceived(value)}.`
                });
            }
            return value;
        case "enum":
            if (!unwrappedSchema.values.includes(value)) {
                errors.push({ path: label, message: formatEnumError(value, unwrappedSchema, label) });
            }
            return value;
        case "array":
            if (!Array.isArray(value)) {
                errors.push({
                    path: label,
                    message: `Invalid value for "${label}". Expected an array, got ${describeReceived(value)}.`
                });
                return value;
            }
            validateArrayConstraints(unwrappedSchema, value, label, errors);
            return value.map((item, index) => validateSchemaValue(unwrappedSchema.item, item, `${label}[${index}]`, errors));
        case "object":
            return validateObjectSchema(unwrappedSchema, value, label, errors);
        case "json":
            return value;
        case "record": {
            if (!isPlainObject(value)) {
                errors.push({
                    path: label,
                    message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
                });
                return value;
            }
            return Object.fromEntries(Object.entries(value).map(([key, item]) => [
                key,
                validateSchemaValue(unwrappedSchema.value, item, `${label}.${key}`, errors)
            ]));
        }
        case "oneOf": {
            if (!isPlainObject(value)) {
                return value;
            }
            const discriminator = value[unwrappedSchema.discriminator];
            const branch = typeof discriminator === "string" ? unwrappedSchema.branches[discriminator] : undefined;
            if (branch === undefined) {
                return value;
            }
            const { [unwrappedSchema.discriminator]: ignoredDiscriminator, ...branchValue } = value;
            void ignoredDiscriminator;
            return {
                [unwrappedSchema.discriminator]: discriminator,
                ...validateObjectSchema(branch, branchValue, label, errors)
            };
        }
        case "union": {
            if (!isPlainObject(value)) {
                return value;
            }
            const branch = unwrappedSchema.branches.find((candidate) => Object.keys(candidate.shape).every((key) => candidate.shape[key]?.kind === "optional" ||
                Object.prototype.hasOwnProperty.call(value, formatSegment(key))));
            return branch === undefined ? value : validateObjectSchema(branch, value, label, errors);
        }
    }
}
function validateStringConstraints(schema, value, label, errors) {
    if (schema.minLength !== undefined && value.length < schema.minLength) {
        errors.push({
            path: label,
            message: `Invalid value for "${label}". Expected a string with length at least ${schema.minLength}, got string with length ${value.length}.`
        });
    }
    if (schema.maxLength !== undefined && value.length > schema.maxLength) {
        errors.push({
            path: label,
            message: `Invalid value for "${label}". Expected a string with length at most ${schema.maxLength}, got string with length ${value.length}.`
        });
    }
    if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
        errors.push({
            path: label,
            message: `Invalid value for "${label}": "${value}" does not match pattern "${schema.pattern}".`
        });
    }
}
function validateArrayConstraints(schema, value, label, errors) {
    if (schema.minItems !== undefined && value.length < schema.minItems) {
        errors.push({
            path: label,
            message: `Invalid value for "${label}". Expected an array with at least ${schema.minItems} items, got array(${value.length}).`
        });
    }
    if (schema.maxItems !== undefined && value.length > schema.maxItems) {
        errors.push({
            path: label,
            message: `Invalid value for "${label}". Expected an array with at most ${schema.maxItems} items, got array(${value.length}).`
        });
    }
}
function validateObjectSchema(schema, value, label, errors) {
    if (!isPlainObject(value)) {
        errors.push({
            path: label,
            message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
        });
        return {};
    }
    const result = {};
    const expectedKeys = new Map();
    for (const [key, childSchema] of Object.entries(schema.shape)) {
        expectedKeys.set(formatSegment(key), [key, childSchema]);
    }
    for (const key of Object.keys(value)) {
        if (!expectedKeys.has(key)) {
            if (schema.additionalProperties === true) {
                Object.defineProperty(result, key, {
                    value: value[key],
                    enumerable: true,
                    configurable: true,
                    writable: true
                });
                continue;
            }
            const fieldLabel = label.length === 0 ? key : `${label}.${key}`;
            errors.push({
                path: fieldLabel,
                message: `Unexpected parameter "${fieldLabel}". ${formatAvailableList([...expectedKeys.keys()].map((expectedKey) => label.length === 0 ? expectedKey : `${label}.${expectedKey}`))}`
            });
        }
    }
    for (const [inputKey, [outputKey, rawChildSchema]] of expectedKeys.entries()) {
        const childSchema = unwrapOptional(rawChildSchema);
        const hasValue = Object.prototype.hasOwnProperty.call(value, inputKey);
        const fieldLabel = label.length === 0 ? inputKey : `${label}.${inputKey}`;
        if (!hasValue) {
            if (childSchema.default !== undefined) {
                Object.defineProperty(result, outputKey, {
                    value: childSchema.default,
                    enumerable: true,
                    configurable: true,
                    writable: true
                });
                continue;
            }
            if (isOptional(rawChildSchema)) {
                continue;
            }
            errors.push({ path: fieldLabel, message: `Missing required parameter "${fieldLabel}".` });
            continue;
        }
        Object.defineProperty(result, outputKey, {
            value: validateSchemaValue(rawChildSchema, value[inputKey], fieldLabel, errors),
            enumerable: true,
            configurable: true,
            writable: true
        });
    }
    return result;
}
function validateUniqueSDKParameterMembers(schema) {
    const sourceKeysByMember = new Map();
    for (const [key, rawChildSchema] of Object.entries(schema.shape)) {
        const member = formatSegment(key);
        const existingKey = sourceKeysByMember.get(member);
        if (existingKey !== undefined) {
            throw new UserError(`Parameters "${existingKey}" and "${key}" use conflicting SDK member "${member}".`);
        }
        sourceKeysByMember.set(member, key);
        const childSchema = unwrapOptional(rawChildSchema);
        if (childSchema.kind === "object") {
            validateUniqueSDKParameterMembers(childSchema);
        }
    }
}
function validateSDKArguments(schema, argumentsValue) {
    const errors = [];
    const result = validateObjectSchema(schema, argumentsValue ?? {}, "", errors);
    throwValidationErrors(errors);
    return result;
}
function defineMember(target, key, value) {
    if (Object.prototype.hasOwnProperty.call(target, key)) {
        throw new Error(`Duplicate SDK member "${key}".`);
    }
    Object.defineProperty(target, key, {
        value,
        enumerable: true,
        configurable: false,
        writable: false
    });
}
export function createSDK(root, options = {}) {
    enableSourceMaps();
    const mergedRoot = options.approvals === true ? mergeApprovalsGroup(root) : root;
    if (!hasMcpProxyGroups(mergedRoot)) {
        return createResolvedSDK(mergedRoot, options);
    }
    return createDeferredSDK(mergedRoot, options);
}
function createResolvedSDK(root, options = {}) {
    const services = options.services ?? {};
    const runtimeOptions = options.humanInLoop ?? {};
    const runtimeFetch = options.fetch ?? globalThis.fetch;
    void options.casing;
    validateServices(services);
    function build(node, path) {
        if (node.kind === "command") {
            const sdkParamsSchema = filterSchemaForScope(node.params, "sdk");
            if (sdkParamsSchema?.kind === "object") {
                validateUniqueSDKParameterMembers(sdkParamsSchema);
            }
            return async (params) => {
                const commandPath = [...path, node.name].join(".");
                let secrets;
                let validatedParams;
                try {
                    secrets = resolveCommandSecrets(node);
                    const baseContext = {
                        ...services,
                        runtimeOptions,
                        root,
                        secrets,
                        fetch: runtimeFetch,
                        fs: createFs(),
                        env: createEnv(),
                        progress() {
                            return undefined;
                        }
                    };
                    await assertCommandRequirements(node, { ...baseContext, params: undefined }, {
                        apiVersion: options.apiVersion
                    });
                    const paramsSchema = filterSchemaForScope(node.params, "sdk");
                    if (paramsSchema === undefined || paramsSchema.kind !== "object") {
                        throw new ToolcraftBugError(`command "${node.name}" must define an object params schema for SDK.`);
                    }
                    validatedParams = validateSDKArguments(paramsSchema, params);
                    return await invokeWithHumanInLoop(node, {
                        ...baseContext,
                        params: validatedParams
                    }, runtimeOptions, commandPath);
                }
                catch (error) {
                    await writeErrorReport({
                        command: node,
                        commandPath,
                        env: process.env,
                        error,
                        errorReports: options.errorReports,
                        params: validatedParams,
                        projectRoot: options.projectRoot,
                        secrets
                    });
                    throw error;
                }
            };
        }
        const output = {};
        const sourceNamesByMember = new Map();
        const nextPath = node === root ? path : [...path, node.name];
        for (const child of node.children) {
            let childValue;
            if (child.kind === "command") {
                if (!child.scope.includes("sdk")) {
                    continue;
                }
                childValue = build(child, nextPath);
            }
            else {
                childValue = build(child, nextPath);
                if (!isPlainObject(childValue) || Object.keys(childValue).length === 0) {
                    continue;
                }
            }
            const member = formatSegment(child.name);
            if (member === "then") {
                throw new UserError(`SDK member "${child.name}" uses reserved member "then".`);
            }
            const existingName = sourceNamesByMember.get(member);
            if (existingName !== undefined) {
                throw new UserError(`SDK members "${existingName}" and "${child.name}" use conflicting member "${member}".`);
            }
            sourceNamesByMember.set(member, child.name);
            defineMember(output, member, childValue);
        }
        return output;
    }
    return build(root, []);
}
function createDeferredSDK(root, options) {
    let sdkPromise;
    const resolveSDK = () => {
        sdkPromise ??= (async () => {
            await resolveMcpProxies(root, { projectRoot: options.projectRoot });
            return createResolvedSDK(root, options);
        })();
        return sdkPromise;
    };
    const resolvePath = async (path) => {
        let current = await resolveSDK();
        for (const segment of path) {
            if (typeof segment !== "string" && typeof segment !== "number") {
                return undefined;
            }
            current = current[segment];
        }
        return current;
    };
    const createPathProxy = (path) => new Proxy(() => undefined, {
        apply(_target, _thisArg, argumentsList) {
            return resolvePath(path).then((value) => {
                if (typeof value !== "function") {
                    throw new TypeError(`SDK member "${path.map(String).join(".")}" is not callable.`);
                }
                return value(...argumentsList);
            });
        },
        get(_target, property) {
            if (property === "then") {
                return path.length === 0 ? resolveSDK().then.bind(resolveSDK()) : undefined;
            }
            return createPathProxy([...path, property]);
        }
    });
    return createPathProxy([]);
}
