import { UserError } from "toolcraft";
import { isIdentifierName, toCliFlag } from "./naming.js";
import { REQUEST_PARAM_SECTIONS } from "./request-shape.js";
const QUERY_ARRAY_SERIALIZATION_SEPARATORS = {
    comma: ",",
    pipe: "|"
};
const VALUE_REFERENCE_OPERATIONS = {
    param: {
        render: (reference) => renderParamAccess(reference.paramName),
        evaluate: (reference, context) => readOwnParam(context.params, reference.paramName)
    },
    resolved: {
        render: (reference) => reference.resolvedName,
        evaluate: (reference, context) => context.resolvedValues[reference.resolvedName]
    }
};
const VALUE_EXPRESSION_OPERATIONS = {
    emptyObject: {
        render: (_value) => "{}",
        evaluate: (_value) => ({})
    },
    reference: {
        render: (value) => renderValueReference(value.reference),
        evaluate: (value, context) => evaluateValueReference(value.reference, context)
    },
    queryArray: {
        render: (value) => renderSerializedQueryArray(renderValueReference(value.reference), value.serialization),
        evaluate: (value, context) => serializeQueryArrayValue(evaluateValueReference(value.reference, context), value.serialization)
    }
};
const PREFLIGHT_BLOCK_OPERATIONS = {
    "scalar-null": {
        render: (block) => {
            const paramAccess = renderParamAccess(block.paramName);
            const nullAccess = renderParamAccess(block.nullParamName);
            return [
                `    if (${paramAccess} !== undefined && ${paramAccess} !== null && ${nullAccess}) {`,
                `      throw new UserError(${JSON.stringify(getScalarNullConflictMessage(block))});`,
                "    }",
                `    const ${block.resolvedName} = ${nullAccess} ? null : ${paramAccess};`,
                ...(block.required
                    ? [
                        `    if (${block.resolvedName} === undefined) {`,
                        `      throw new UserError(${JSON.stringify(getMissingRequiredParameterMessage(block.paramName))});`,
                        "    }"
                    ]
                    : [])
            ];
        },
        execute: (block, context) => {
            const value = context.params[block.paramName];
            const nullRequested = context.params[block.nullParamName] === true;
            if (value !== undefined && value !== null && nullRequested) {
                throw new UserError(getScalarNullConflictMessage(block));
            }
            context.resolvedValues[block.resolvedName] = nullRequested ? null : value;
            if (block.required && context.resolvedValues[block.resolvedName] === undefined) {
                throw new UserError(getMissingRequiredParameterMessage(block.paramName));
            }
        }
    },
    array: {
        render: (block) => {
            const paramAccess = renderParamAccess(block.paramName);
            const jsonAccess = renderParamAccess(block.jsonParamName);
            return [
                ...(block.nullParamName === undefined
                    ? []
                    : [
                        `    if (${renderParamAccess(block.nullParamName)} && (${paramAccess} !== undefined || ${jsonAccess} !== undefined)) {`,
                        `      throw new UserError(${JSON.stringify(getArrayNullConflictMessage(block))});`,
                        "    }"
                    ]),
                `    if (${paramAccess} !== undefined && ${jsonAccess} !== undefined) {`,
                `      throw new UserError(${JSON.stringify(getArrayJsonConflictMessage(block))});`,
                "    }",
                `    let ${block.resolvedName} = ${paramAccess};`,
                `    if (${jsonAccess} !== undefined) {`,
                "      let parsedJson: unknown;",
                "      try {",
                `        parsedJson = JSON.parse(${jsonAccess});`,
                "      } catch (error) {",
                `        throw new UserError(${JSON.stringify(getInvalidArrayJsonMessage(block.jsonParamName, "Expected valid JSON."))});`,
                "      }",
                "      if (!Array.isArray(parsedJson)) {",
                `        throw new UserError(${JSON.stringify(getInvalidArrayJsonMessage(block.jsonParamName, "Expected a JSON array."))});`,
                "      }",
                `      validateArrayJsonHelperValue(parsedJson, ${JSON.stringify(block.definition)}, ${JSON.stringify(block.jsonParamName)});`,
                `      ${block.resolvedName} = parsedJson;`,
                "    }",
                ...(block.nullParamName === undefined
                    ? []
                    : [
                        `    if (${renderParamAccess(block.nullParamName)}) {`,
                        `      ${block.resolvedName} = null;`,
                        "    }"
                    ]),
                ...(block.required
                    ? [
                        `    if (${block.resolvedName} === undefined) {`,
                        `      throw new UserError(${JSON.stringify(getMissingRequiredParameterMessage(block.paramName))});`,
                        "    }"
                    ]
                    : [])
            ];
        },
        execute: (block, context) => {
            const directValue = context.params[block.paramName];
            const jsonValue = context.params[block.jsonParamName];
            const nullRequested = block.nullParamName !== undefined && context.params[block.nullParamName] === true;
            if (block.nullParamName !== undefined && nullRequested && (directValue !== undefined || jsonValue !== undefined)) {
                throw new UserError(getArrayNullConflictMessage(block));
            }
            if (directValue !== undefined && jsonValue !== undefined) {
                throw new UserError(getArrayJsonConflictMessage(block));
            }
            let resolved = directValue;
            if (jsonValue !== undefined) {
                let parsedJson;
                try {
                    parsedJson = JSON.parse(String(jsonValue));
                }
                catch {
                    throw new UserError(getInvalidArrayJsonMessage(block.jsonParamName, "Expected valid JSON."));
                }
                if (!Array.isArray(parsedJson)) {
                    throw new UserError(getInvalidArrayJsonMessage(block.jsonParamName, "Expected a JSON array."));
                }
                validateArrayJsonHelperValue(parsedJson, block.definition, block.jsonParamName);
                resolved = parsedJson;
            }
            if (nullRequested) {
                resolved = null;
            }
            if (block.required && resolved === undefined) {
                throw new UserError(getMissingRequiredParameterMessage(block.paramName));
            }
            context.resolvedValues[block.resolvedName] = resolved;
        }
    }
};
const REQUEST_SECTION_OPERATIONS = {
    inline: {
        render: (section, sectionFields, optional) => {
            const [field] = sectionFields;
            if (field === undefined) {
                return [];
            }
            if (!optional) {
                return [`      ${section.key}: ${renderValueExpression(field.value)},`];
            }
            return [
                `      ...(${renderOmitWhenUndefinedExpression(field.omitWhenUndefinedReference)}`,
                "        ? {}",
                "        : {",
                `            ${section.key}: ${renderValueExpression(field.value)},`,
                "          }),"
            ];
        },
        build: (_section, sectionFields, optional, context) => {
            const [field] = sectionFields;
            if (field === undefined) {
                return undefined;
            }
            if (optional && evaluateValueReference(field.omitWhenUndefinedReference, context) === undefined) {
                return undefined;
            }
            return evaluateValueExpression(field.value, context);
        }
    },
    wrapped: {
        render: (section, sectionFields, optional) => {
            if (!optional) {
                return [
                    `      ${section.key}: {`,
                    ...sectionFields.map((field) => `        ${renderWireName(field.wireName)}: ${renderValueExpression(field.value)},`),
                    "      },"
                ];
            }
            return [
                `      ...(${sectionFields.map((field) => renderOmitWhenUndefinedExpression(field.omitWhenUndefinedReference)).join(" && ")}`,
                "        ? {}",
                "        : {",
                `            ${section.key}: {`,
                ...sectionFields.map((field) => `              ${renderWireName(field.wireName)}: ${renderValueExpression(field.value)},`),
                "            },",
                "          }),"
            ];
        },
        build: (_section, sectionFields, optional, context) => {
            if (optional &&
                sectionFields.every((field) => evaluateValueReference(field.omitWhenUndefinedReference, context) === undefined)) {
                return undefined;
            }
            return Object.fromEntries(sectionFields.map((field) => [field.wireName, evaluateValueExpression(field.value, context)]));
        }
    }
};
export function renderPreflightBlock(block) {
    return PREFLIGHT_BLOCK_OPERATIONS[block.kind].render(block);
}
export function executePreflightBlocks(blocks, params) {
    const resolvedValues = {};
    for (const block of blocks) {
        PREFLIGHT_BLOCK_OPERATIONS[block.kind].execute(block, {
            params,
            resolvedValues
        });
    }
    return resolvedValues;
}
export function validateArrayJsonHelperValue(value, definition, jsonParamName) {
    const issue = findDefinitionIssue(value, definition, []);
    if (issue !== undefined) {
        throw new UserError(getInvalidArrayJsonMessage(jsonParamName, issue));
    }
}
function findDefinitionIssue(value, definition, path) {
    if (value === null) {
        return definition.nullable === true || definition.kind === "json"
            ? undefined
            : `Expected ${expectedDefinition(definition)} at ${formatJsonPath(path)}.`;
    }
    switch (definition.kind) {
        case "array":
            return findArrayDefinitionIssue(value, definition, path);
        case "boolean":
            return typeof value === "boolean"
                ? undefined
                : `Expected boolean at ${formatJsonPath(path)}.`;
        case "enum":
            return definition.enumValues.includes(value)
                ? undefined
                : `Expected one of ${definition.enumValues.join(", ")} at ${formatJsonPath(path)}.`;
        case "json":
            return undefined;
        case "number":
            return findNumberDefinitionIssue(value, definition, path);
        case "string":
            return findStringDefinitionIssue(value, definition, path);
    }
}
function findArrayDefinitionIssue(value, definition, path) {
    if (!Array.isArray(value)) {
        return `Expected array at ${formatJsonPath(path)}.`;
    }
    if (definition.minItems !== undefined && value.length < definition.minItems) {
        return `Expected array with at least ${definition.minItems} items at ${formatJsonPath(path)}.`;
    }
    if (definition.maxItems !== undefined && value.length > definition.maxItems) {
        return `Expected array with at most ${definition.maxItems} items at ${formatJsonPath(path)}.`;
    }
    for (const [index, item] of value.entries()) {
        const issue = findDefinitionIssue(item, definition.itemDefinition, [...path, String(index)]);
        if (issue !== undefined) {
            return issue;
        }
    }
    return undefined;
}
function findNumberDefinitionIssue(value, definition, path) {
    if (typeof value !== "number" || !Number.isFinite(value)) {
        return `Expected ${definition.jsonType === "integer" ? "integer" : "number"} at ${formatJsonPath(path)}.`;
    }
    if (definition.jsonType === "integer" && !Number.isInteger(value)) {
        return `Expected integer at ${formatJsonPath(path)}.`;
    }
    if (definition.minimum !== undefined && value < definition.minimum) {
        return `Expected number greater than or equal to ${definition.minimum} at ${formatJsonPath(path)}.`;
    }
    if (definition.maximum !== undefined && value > definition.maximum) {
        return `Expected number less than or equal to ${definition.maximum} at ${formatJsonPath(path)}.`;
    }
    return undefined;
}
function findStringDefinitionIssue(value, definition, path) {
    if (typeof value !== "string") {
        return `Expected string at ${formatJsonPath(path)}.`;
    }
    if (definition.minLength !== undefined && value.length < definition.minLength) {
        return `Expected string with length at least ${definition.minLength} at ${formatJsonPath(path)}.`;
    }
    if (definition.maxLength !== undefined && value.length > definition.maxLength) {
        return `Expected string with length at most ${definition.maxLength} at ${formatJsonPath(path)}.`;
    }
    if (definition.pattern !== undefined) {
        const pattern = compileDefinitionPattern(definition.pattern);
        if (pattern === undefined || !pattern.test(value)) {
            return `Expected string matching pattern ${definition.pattern} at ${formatJsonPath(path)}.`;
        }
    }
    return undefined;
}
function compileDefinitionPattern(pattern) {
    try {
        return new RegExp(pattern, "u");
    }
    catch {
        return undefined;
    }
}
function expectedDefinition(definition) {
    return definition.kind === "number" && definition.jsonType === "integer"
        ? "integer"
        : definition.kind;
}
function formatJsonPath(path) {
    if (path.length === 0) {
        return "the JSON array";
    }
    return path.map((segment) => `[${segment}]`).join("");
}
export function renderRequestShape(requestFields, sectionRenders, optionalSections) {
    const lines = [];
    for (const section of REQUEST_PARAM_SECTIONS) {
        const sectionFields = requestFields.filter((field) => field.location === section.location);
        if (sectionFields.length === 0) {
            continue;
        }
        const renderKind = sectionRenders[section.location];
        if (renderKind === undefined) {
            continue;
        }
        lines.push(...REQUEST_SECTION_OPERATIONS[renderKind].render(section, sectionFields, optionalSections.has(section.location)));
    }
    return lines;
}
export function buildRequestShape(requestFields, sectionRenders, optionalSections, params, resolvedValues) {
    const requestShape = {};
    const context = { params, resolvedValues };
    for (const section of REQUEST_PARAM_SECTIONS) {
        const sectionFields = requestFields.filter((field) => field.location === section.location);
        if (sectionFields.length === 0) {
            continue;
        }
        const renderKind = sectionRenders[section.location];
        if (renderKind === undefined) {
            continue;
        }
        const builtSection = REQUEST_SECTION_OPERATIONS[renderKind].build(section, sectionFields, optionalSections.has(section.location), context);
        if (builtSection !== undefined) {
            requestShape[section.key] = builtSection;
        }
    }
    return requestShape;
}
function getScalarNullConflictMessage(block) {
    return `Options "--${toCliFlag(block.paramName)}" and "--${toCliFlag(block.nullParamName)}" are mutually exclusive.`;
}
function getArrayNullConflictMessage(block) {
    const nullParamName = block.nullParamName;
    if (nullParamName === undefined) {
        throw new Error("Missing null helper for nullable array preflight block.");
    }
    return `Options "--${toCliFlag(nullParamName)}", "--${toCliFlag(block.paramName)}", and "--${toCliFlag(block.jsonParamName)}" cannot be combined.`;
}
function getArrayJsonConflictMessage(block) {
    return `Options "--${toCliFlag(block.paramName)}" and "--${toCliFlag(block.jsonParamName)}" are mutually exclusive.`;
}
function getInvalidArrayJsonMessage(paramName, expectation) {
    return `Invalid value for "--${toCliFlag(paramName)}". ${expectation}`;
}
function getMissingRequiredParameterMessage(paramName) {
    return `Missing required parameter "${toCliFlag(paramName)}".`;
}
function renderValueReference(reference) {
    return VALUE_REFERENCE_OPERATIONS[reference.kind].render(reference);
}
function evaluateValueReference(reference, context) {
    return VALUE_REFERENCE_OPERATIONS[reference.kind].evaluate(reference, context);
}
function renderValueExpression(value) {
    return VALUE_EXPRESSION_OPERATIONS[value.kind].render(value);
}
function evaluateValueExpression(value, context) {
    return VALUE_EXPRESSION_OPERATIONS[value.kind].evaluate(value, context);
}
function renderSerializedQueryArray(reference, serialization) {
    if (serialization === "repeat" || serialization === "brackets") {
        return reference;
    }
    return `${reference} === undefined || ${reference} === null ? ${reference} : ${reference}.join(${JSON.stringify(QUERY_ARRAY_SERIALIZATION_SEPARATORS[serialization])})`;
}
function serializeQueryArrayValue(value, serialization) {
    if (serialization === "repeat" || serialization === "brackets" || value === undefined || value === null) {
        return value;
    }
    return Array.isArray(value) ? value.join(QUERY_ARRAY_SERIALIZATION_SEPARATORS[serialization]) : value;
}
function renderOmitWhenUndefinedExpression(reference) {
    return `${renderValueReference(reference)} === undefined`;
}
function renderParamAccess(name) {
    if (Object.prototype.hasOwnProperty.call(Object.prototype, name)) {
        return `(Object.prototype.hasOwnProperty.call(params, ${JSON.stringify(name)}) ? params[${JSON.stringify(name)}] : undefined)`;
    }
    return isIdentifierName(name) ? `params.${name}` : `params[${JSON.stringify(name)}]`;
}
function renderWireName(name) {
    return name === "__proto__" ? `[${JSON.stringify(name)}]` : JSON.stringify(name);
}
function readOwnParam(params, name) {
    return Object.prototype.hasOwnProperty.call(params, name) ? params[name] : undefined;
}
