// Copyright (c) 2026 Modelware. All rights reserved.

import type { TemplateBindResult, TemplateDefinition, TemplateInvocationContext, TemplateParameterDefinition, TemplateValue } from './types.js';

export function bindTemplateParameters(
    template: TemplateDefinition,
    context: TemplateInvocationContext,
    explicitArgs: Readonly<Record<string, TemplateValue>> = {}
): TemplateBindResult {
    const values: Record<string, TemplateValue> = {};
    const parameters = template.parameters ?? [];
    const declaredIds = new Set(parameters.map((p) => p.id));
    for (const argId of Object.keys(explicitArgs)) {
        if (!declaredIds.has(argId)) {
            throw new Error(`Unknown template parameter: ${argId}`);
        }
    }
    for (const parameter of parameters) {
        const explicit = explicitArgs[parameter.id];
        if (explicit !== undefined) {
            values[parameter.id] = coerceParameterValue(parameter, explicit);
            continue;
        }
        const defaultValue = resolveTemplateDefaultValue(parameter.defaultValue, context);
        if (defaultValue !== undefined) {
            values[parameter.id] = coerceParameterValue(parameter, defaultValue);
            continue;
        }
        if (parameter.required) {
            throw new Error(`Missing required template parameter: ${parameter.id}`);
        }
        values[parameter.id] = naturalDefaultValue(parameter.type);
    }
    return { values };
}

function resolveTemplateDefaultValue(
    value: TemplateValue | undefined,
    context: TemplateInvocationContext
): TemplateValue | undefined {
    if (value === undefined) {
        return undefined;
    }
    if (typeof value !== 'string') {
        return value;
    }
    return interpolateContextExpression(value, context);
}

function interpolateContextExpression(source: string, context: TemplateInvocationContext): string | undefined {
    let unresolved = false;
    const output = source.replace(/\$\{context\.([^}]+)\}/g, (_match, token: string) => {
        const resolved = resolveContextToken(token.trim(), context);
        if (resolved === undefined) {
            unresolved = true;
            return '';
        }
        return resolved;
    });
    return unresolved ? undefined : output;
}

function resolveContextToken(token: string, context: TemplateInvocationContext): string | undefined {
    const indexedSelection = /^selection\[(\d+)]$/.exec(token);
    if (indexedSelection) {
        const index = Number.parseInt(indexedSelection[1] ?? '', 10);
        if (!Number.isFinite(index) || index < 0) {
            return undefined;
        }
        const selection = context.selection?.iris ?? [];
        const value = selection[index];
        return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
    }
    const key = token;
    const vars = context.vars ?? {};
    const direct = vars[key];
    if (typeof direct === 'string') {
        return direct;
    }
    if (typeof direct === 'number' || typeof direct === 'boolean') {
        return String(direct);
    }
    if (key === 'ontology') {
        return context.model?.ontologyIri?.trim() || undefined;
    }
    if (key === 'member') {
        return context.focus?.memberIri?.trim() || undefined;
    }
    if (key === 'selection') {
        const selection = context.selection?.iris ?? [];
        return selection.join(', ');
    }
    return undefined;
}

function coerceParameterValue(parameter: TemplateParameterDefinition, value: TemplateValue): TemplateValue {
    switch (parameter.type) {
        case 'string':
            return coerceString(parameter, value);
        case 'iri':
            return coerceIri(parameter, value);
        case 'number':
            return coerceNumber(parameter, value);
        case 'boolean':
            return coerceBoolean(parameter, value);
        case 'iri[]':
            return coerceIriArray(parameter, value);
        case 'json':
            return coerceJson(parameter, value);
    }
}

function coerceString(parameter: TemplateParameterDefinition, value: TemplateValue): string {
    if (typeof value === 'string') {
        return value;
    }
    if (typeof value === 'number' || typeof value === 'boolean') {
        return String(value);
    }
    throw new Error(`Template parameter '${parameter.id}' must be a string.`);
}

function coerceIri(parameter: TemplateParameterDefinition, value: TemplateValue): string {
    if (typeof value !== 'string') {
        throw new Error(`Template parameter '${parameter.id}' must be a valid IRI.`);
    }
    try {
        new URL(value);
    } catch {
        throw new Error(`Template parameter '${parameter.id}' must be a valid IRI, got: ${value}`);
    }
    return value;
}

function coerceNumber(parameter: TemplateParameterDefinition, value: TemplateValue): number {
    if (typeof value === 'number' && Number.isFinite(value)) {
        return value;
    }
    if (typeof value === 'string') {
        const parsed = Number(value.trim());
        if (Number.isFinite(parsed)) {
            return parsed;
        }
    }
    throw new Error(`Template parameter '${parameter.id}' must be a number.`);
}

function coerceBoolean(parameter: TemplateParameterDefinition, value: TemplateValue): boolean {
    if (typeof value === 'boolean') {
        return value;
    }
    if (typeof value === 'string') {
        const normalized = value.trim().toLowerCase();
        if (normalized === 'true') {
            return true;
        }
        if (normalized === 'false') {
            return false;
        }
    }
    throw new Error(`Template parameter '${parameter.id}' must be a boolean.`);
}

function validateIriArrayEntries(parameter: TemplateParameterDefinition, entries: string[]): string[] {
    for (const entry of entries) {
        try {
            new URL(entry);
        } catch {
            throw new Error(`Template parameter '${parameter.id}' contains an invalid IRI: ${entry}`);
        }
    }
    return entries;
}

function coerceIriArray(parameter: TemplateParameterDefinition, value: TemplateValue): string[] {
    if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
        return validateIriArrayEntries(parameter, value as string[]);
    }
    if (typeof value === 'string') {
        const normalized = value.trim();
        if (!normalized) {
            return [];
        }
        const entries = normalized.split(',').map((entry) => entry.trim()).filter((entry) => entry.length > 0);
        return validateIriArrayEntries(parameter, entries);
    }
    throw new Error(`Template parameter '${parameter.id}' must be an array of IRIs.`);
}

function coerceJson(parameter: TemplateParameterDefinition, value: TemplateValue): TemplateValue {
    if (value === null || typeof value === 'object') {
        return value;
    }
    if (typeof value === 'string') {
        try {
            const parsed = JSON.parse(value) as unknown;
            if (
                parsed === null
                || typeof parsed === 'string'
                || typeof parsed === 'number'
                || typeof parsed === 'boolean'
                || (Array.isArray(parsed) && parsed.every((entry) => typeof entry === 'string'))
                || (typeof parsed === 'object' && parsed !== null)
            ) {
                return parsed as TemplateValue;
            }
            throw new Error('unsupported-json');
        } catch {
            throw new Error(`Template parameter '${parameter.id}' must be valid JSON.`);
        }
    }
    throw new Error(`Template parameter '${parameter.id}' must be JSON.`);
}

function naturalDefaultValue(type: TemplateParameterDefinition['type']): TemplateValue {
    switch (type) {
        case 'number':
            return 0;
        case 'boolean':
            return false;
        case 'iri[]':
            return [];
        case 'json':
            return {};
        case 'string':
        case 'iri':
            return '';
    }
}
