/**
 * Server-side validation route handler factory.
 *
 * Creates a POST handler that executes validation rules against uploaded
 * documents or plain text using LLM analysis, and returns ClarificationItems
 * for any issues found.
 *
 * Usage:
 *   import { create_validation_route } from 'hazo_collab_forms/lib';
 *   export const POST = create_validation_route();
 *
 * Uses standard Web API Request/Response for portability.
 * hazo_llm_api is dynamically imported so it's only required at runtime.
 */
import 'server-only';
import { DEFAULT_CLARIFICATION_TEMPLATES } from '../config/clarification_templates.js';
import { get_config } from './config.js';
import { make_issue_id } from '../utils/validation_result.js';
import { extract_expectation } from '../utils/expectation_extractor.js';
// ============================================================================
// Helpers
// ============================================================================
function generate_clarification_id() {
    return `clr_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
function is_image_mime(mime_type) {
    return mime_type.startsWith('image/');
}
function is_document_mime(mime_type) {
    return mime_type === 'application/pdf';
}
/** Substitute {{variable}} placeholders in prompt text. */
function substitute_variables(prompt, variables) {
    return prompt.replace(/\{\{(\w+)\}\}/g, (_, key) => {
        return variables[key] ?? `{{${key}}}`;
    });
}
/** Wrap a per-rule prompt with the response envelope from hazo_prompts. */
function wrap_rule_prompt(rule_prompt, wrapper) {
    return substitute_variables(wrapper, { rule_prompt });
}
// ============================================================================
// Parser (unified — v4)
// ============================================================================
/**
 * Parse the LLM response into a ValidationRuleResult.
 *
 * Expected shape (per validation/response_wrapper in hazo_prompts):
 *   {
 *     "summary": "...",                    // optional rule-level explanation
 *     "issues": [                          // empty = passed
 *       { "description": "...", "amount": "...", "date": "...", "reason": "...",
 *         "client_comment": "...", "confidence": 0.9 }
 *     ],
 *     "confidence": 0.95                   // optional rule-level confidence
 *   }
 *
 * Tolerates: direct JSON, fenced code block, regex-extracted object, and
 * top-level arrays (treated as the issues array).
 */
export function parse_response(text, rule_id) {
    const cleaned = text.trim();
    const try_parse = (raw) => {
        try {
            return JSON.parse(raw);
        }
        catch {
            return null;
        }
    };
    const parsed = try_parse(cleaned) ??
        (() => {
            const fence = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
            return fence ? try_parse(fence[1].trim()) : null;
        })() ??
        (() => {
            const obj = cleaned.match(/\{[\s\S]*\}/);
            if (obj)
                return try_parse(obj[0]);
            const arr = cleaned.match(/\[[\s\S]*\]/);
            return arr ? try_parse(arr[0]) : null;
        })();
    if (!parsed)
        return { issues: [] };
    let raw_issues = [];
    let summary;
    let confidence;
    let extracted_data;
    if (Array.isArray(parsed)) {
        raw_issues = parsed;
    }
    else if (typeof parsed === 'object' && parsed !== null) {
        const obj = parsed;
        if (Array.isArray(obj.issues))
            raw_issues = obj.issues;
        else if (Array.isArray(obj.items))
            raw_issues = obj.items;
        if (typeof obj.summary === 'string')
            summary = obj.summary;
        if (typeof obj.confidence === 'number')
            confidence = obj.confidence;
        if (obj.extracted_data && typeof obj.extracted_data === 'object' && !Array.isArray(obj.extracted_data)) {
            extracted_data = obj.extracted_data;
        }
    }
    const issues = [];
    for (const row of raw_issues) {
        if (typeof row !== 'object' || row === null)
            continue;
        const r = row;
        const description_raw = r.description ||
            r.issue_description ||
            r.issue ||
            r.label ||
            '';
        if (!description_raw)
            continue;
        const amount = r.amount;
        const date = r.date;
        const reason = r.reason;
        const client_comment = r.client_comment;
        const issue_confidence = typeof r.confidence === 'number' ? r.confidence : undefined;
        // Compose a richer human-readable description that includes amount/date
        // so list views show useful context without reaching into sub-fields.
        const parts = [description_raw];
        if (amount !== undefined && amount !== '')
            parts.push(`$${amount}`);
        if (date)
            parts.push(String(date));
        const header = parts.join(' — ');
        const issue_description = reason && reason !== description_raw
            ? `${header}. ${reason}`
            : header;
        const id_seed = [rule_id, description_raw, amount, date];
        const fallback = `iss-${issues.length}`;
        issues.push({
            issue_id: make_issue_id(id_seed, fallback),
            issue_description,
            ...(amount !== undefined && amount !== '' ? { amount: String(amount) } : {}),
            ...(date ? { date: String(date) } : {}),
            ...(reason ? { reason } : {}),
            ...(client_comment ? { client_comment } : {}),
            ...(issue_confidence !== undefined ? { confidence: issue_confidence } : {}),
        });
    }
    return {
        issues,
        ...(summary ? { summary } : {}),
        ...(confidence !== undefined ? { confidence } : {}),
        ...(extracted_data ? { extracted_data } : {}),
    };
}
// ============================================================================
// Clarification creation
// ============================================================================
function create_clarification_from_result(rule, result, doc_info) {
    const template = rule.clarification_type !== 'none'
        ? DEFAULT_CLARIFICATION_TEMPLATES[rule.clarification_type]
        : null;
    const response_options = rule.custom_response_options ??
        template?.response_options ??
        [];
    // Derive a human-readable issue description: prefer the first issue's
    // client_comment (LLM-drafted for the client), fall back to its description,
    // then the rule-level summary, then template/default.
    const first = result.issues[0];
    const derived_issue_description = first?.client_comment ??
        first?.issue_description ??
        result.summary ??
        template?.default_issue_description ??
        'A validation issue was detected.';
    const expectation = first ? extract_expectation(first) : undefined;
    return {
        id: generate_clarification_id(),
        type: rule.clarification_type,
        status: 'pending',
        target_field_id: rule.target_field_id,
        target_label: rule.target_label,
        rule_name: rule.name,
        rule_id: rule.rule_id,
        issue_description: rule.custom_issue_description ??
            derived_issue_description,
        validation_details: result.summary ?? first?.issue_description,
        doc_references: doc_info ? [{
                file_id: '', // Client enriches with actual file_id
                file_name: doc_info.file_name,
                mime_type: doc_info.mime_type,
            }] : [],
        action_description: rule.custom_action_description ??
            template?.default_action_description,
        response_options,
        response_files: [],
        created_at: new Date().toISOString(),
        ...(expectation ? { expectation } : {}),
    };
}
// ============================================================================
// Route Handler Factory
// ============================================================================
/**
 * Creates a POST route handler for document validation.
 *
 * The handler:
 * 1. Validates the request payload
 * 2. Fetches the file (inline or from URL)
 * 3. Executes each validation rule against the document via LLM
 * 4. Creates ClarificationItems for rules that find issues
 * 5. Returns the results
 */
export function create_validation_route(options) {
    const sqlite_path = options?.sqlite_path ?? get_config('hazo_connect', 'prompt_sqlite_path') ?? 'data/prompt_library.sqlite';
    const hazo_connect_adapter = options?.hazo_connect_adapter;
    const on_error = options?.on_error ?? ((err, ctx) => console.error(`[validation] ${ctx}:`, err));
    // Cache the dynamic import
    let llm_api_module = null;
    let init_promise = null;
    // Validation wrappers live in hazo_prompts (not in code). Loaded once per
    // route-handler lifetime and cached. Updates via the Prompt Editor require a
    // server restart to pick up — same behaviour as every other prompt loaded
    // here. There is intentionally no hardcoded fallback: missing wrappers must
    // surface as a 500 with a seeding hint.
    let wrapper_cache = null;
    async function get_validation_wrappers() {
        if (wrapper_cache)
            return wrapper_cache;
        await ensure_initialized();
        const api = await get_llm_api();
        const connect = api.get_hazo_connect?.() ?? null;
        if (!connect) {
            throw new Error('hazo_llm_api hazo_connect is not available — cannot load validation wrappers from hazo_prompts');
        }
        const fetch_one = async (key) => {
            const r = await connect.get_by_area_key('validation', key);
            return r.success ? (r.data?.prompt_text_full ?? null) : null;
        };
        const [response, multi_doc, text, document_extract] = await Promise.all([
            fetch_one('response_wrapper'),
            fetch_one('multi_doc_suffix'),
            fetch_one('text_wrapper'),
            fetch_one('document_extract'),
        ]);
        const missing = [];
        if (!response)
            missing.push('validation/response_wrapper');
        if (!multi_doc)
            missing.push('validation/multi_doc_suffix');
        if (!text)
            missing.push('validation/text_wrapper');
        if (!document_extract)
            missing.push('validation/document_extract');
        if (missing.length > 0) {
            throw new Error(`Validation wrappers missing in hazo_prompts: ${missing.join(', ')}. Run test-app/scripts/seed-prompts.mjs.`);
        }
        wrapper_cache = {
            response: response,
            multi_doc: multi_doc,
            text: text,
            document_extract: document_extract,
        };
        return wrapper_cache;
    }
    async function get_llm_api() {
        if (!llm_api_module) {
            try {
                const module_id = 'hazo_llm_api/server';
                llm_api_module = await import(/* webpackIgnore: true */ module_id);
            }
            catch {
                throw new Error('hazo_llm_api is required for validation. Install it: npm install hazo_llm_api');
            }
        }
        return llm_api_module;
    }
    async function ensure_initialized() {
        const api = await get_llm_api();
        if (!api.is_initialized()) {
            if (!init_promise) {
                const init_config = hazo_connect_adapter
                    ? { hazo_connect: hazo_connect_adapter }
                    : { sqlite_path };
                init_promise = api.initialize_llm_api(init_config).then(() => { });
            }
            await init_promise;
        }
    }
    /**
     * Call the LLM for a given input. Returns the raw response text.
     * Internal: all three input kinds converge here so run_rule can stay flat.
     */
    async function call_llm_for_input(full_prompt, input, wrappers) {
        const api = await get_llm_api();
        if (input.kind === 'text') {
            const text_prompt = substitute_variables(wrappers.text, {
                full_prompt,
                text_content: input.text_content,
            });
            return api.hazo_llm_text_text({ prompt: text_prompt });
        }
        if (input.kind === 'document') {
            if (is_image_mime(input.mime_type)) {
                return api.hazo_llm_image_text({
                    prompt: full_prompt,
                    image_b64: input.file_b64,
                    image_mime_type: input.mime_type,
                });
            }
            return api.hazo_llm_document_text({
                prompt: full_prompt,
                document_b64: input.file_b64,
                document_mime_type: input.mime_type,
            });
        }
        // input.kind === 'documents' (batch)
        const [primary, ...additional] = input.files;
        const additional_documents = additional.map(f => ({
            mime_type: f.mime_type,
            data: f.file_b64,
        }));
        // If the primary is an image, fall back to extract-then-evaluate since
        // image_text doesn't support additional_documents.
        if (is_image_mime(primary.mime_type)) {
            return batch_via_extraction(full_prompt, input.files, wrappers);
        }
        try {
            return await api.hazo_llm_document_text({
                prompt: full_prompt,
                document_b64: primary.file_b64,
                document_mime_type: primary.mime_type,
                additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
            });
        }
        catch {
            // If additional_documents isn't supported, fall back.
            return batch_via_extraction(full_prompt, input.files, wrappers);
        }
    }
    /**
     * Fallback: extract each file individually, then evaluate collectively via
     * text-only LLM. Uses validation/document_extract + validation/multi_doc_suffix.
     */
    async function batch_via_extraction(full_prompt, files, wrappers) {
        const api = await get_llm_api();
        const extractions = [];
        for (const file of files) {
            const resp = is_image_mime(file.mime_type)
                ? await api.hazo_llm_image_text({ prompt: wrappers.document_extract, image_b64: file.file_b64, image_mime_type: file.mime_type })
                : await api.hazo_llm_document_text({ prompt: wrappers.document_extract, document_b64: file.file_b64, document_mime_type: file.mime_type });
            extractions.push(`FILE: ${file.file_name}\n${resp.success && resp.text ? resp.text : '[Extraction failed]'}`);
        }
        const combined_prompt = substitute_variables(wrappers.multi_doc, {
            full_prompt,
            extractions: extractions.join('\n\n---\n\n'),
        });
        return api.hazo_llm_text_text({ prompt: combined_prompt });
    }
    /**
     * Execute a single rule against a given input. This is the one place all
     * three paths (single document, multi-document batch, text) converge.
     */
    async function run_rule(rule, input, variables, wrappers) {
        const resolved_prompt = substitute_variables(rule.prompt, variables);
        const full_prompt = wrap_rule_prompt(resolved_prompt, wrappers.response);
        const llm_response = await call_llm_for_input(full_prompt, input, wrappers);
        if (!llm_response.success || !llm_response.text) {
            return {
                rule_id: rule.rule_id,
                issues: [],
                raw_response: llm_response.error ?? 'LLM call failed',
                check_type: rule.check_type,
            };
        }
        const parsed = parse_response(llm_response.text, rule.rule_id);
        return {
            rule_id: rule.rule_id,
            issues: parsed.issues,
            ...(parsed.summary ? { summary: parsed.summary } : {}),
            ...(parsed.confidence !== undefined ? { confidence: parsed.confidence } : {}),
            ...(parsed.extracted_data ? { extracted_data: parsed.extracted_data } : {}),
            raw_response: llm_response.text,
            check_type: rule.check_type,
        };
    }
    // ── File fetching helpers (route-level) ──
    async function fetch_file_b64(request_url, entry) {
        if (entry.file_b64)
            return entry.file_b64;
        if (!entry.download_url)
            throw new Error(`No file_b64 or download_url for "${entry.file_name}"`);
        const absolute = entry.download_url.startsWith('http')
            ? entry.download_url
            : `${new URL(request_url).origin}${entry.download_url}`;
        const resp = await fetch(absolute);
        if (!resp.ok)
            throw new Error(`Failed to fetch file "${entry.file_name}": ${resp.status}`);
        const buffer = Buffer.from(await resp.arrayBuffer());
        return buffer.toString('base64');
    }
    // ── Route ──
    return async function POST(request) {
        try {
            const body = await request.json();
            const { file_name, mime_type, download_url, file_b64: request_file_b64, rules, variables = {}, content_tag, } = body;
            if (!rules?.length) {
                return Response.json({
                    success: false,
                    clarifications: [],
                    rule_results: [],
                    errors: [{ rule_id: '', error: 'rules array is required and must not be empty' }],
                }, { status: 400 });
            }
            await ensure_initialized();
            const wrappers = await get_validation_wrappers();
            // ── Build the ValidationInput based on request shape ──
            let input;
            const text_content = body.text_content;
            const is_text_mode = !!text_content && !request_file_b64 && !download_url;
            const rule_results = [];
            const clarifications = [];
            const errors = [];
            if (body.mode === 'batch' && body.files?.length) {
                // Multi-file batch
                const batch_files = [];
                for (const f of body.files) {
                    try {
                        const b64 = await fetch_file_b64(request.url, f);
                        batch_files.push({ file_id: f.file_id, file_name: f.file_name, mime_type: f.mime_type, file_b64: b64 });
                    }
                    catch (err) {
                        errors.push({ rule_id: '', error: err instanceof Error ? err.message : 'Unknown file fetch error' });
                    }
                }
                if (batch_files.length === 0) {
                    return Response.json({
                        success: false,
                        clarifications: [],
                        rule_results: [],
                        errors: errors.length > 0 ? errors : [{ rule_id: '', error: 'No files could be fetched for batch validation' }],
                    }, { status: 500 });
                }
                input = { kind: 'documents', files: batch_files };
                const batch_variables = {
                    ...variables,
                    document_name: batch_files.map(f => f.file_name).join(', '),
                    ...(content_tag ? { document_type: content_tag } : {}),
                };
                for (const rule of rules) {
                    try {
                        const result = await run_rule(rule, input, batch_variables, wrappers);
                        rule_results.push(result);
                        if (result.issues.length > 0 && rule.clarification_type !== 'none') {
                            const clarification = create_clarification_from_result(rule, result, {
                                file_name: batch_files.map(f => f.file_name).join(', '),
                                mime_type: batch_files[0].mime_type,
                            });
                            clarification.doc_references = batch_files.map(f => ({
                                file_id: f.file_id, file_name: f.file_name, mime_type: f.mime_type,
                            }));
                            clarifications.push(clarification);
                        }
                    }
                    catch (err) {
                        const message = err instanceof Error ? err.message : 'Unknown error';
                        on_error(err, `batch rule ${rule.rule_id}: ${rule.name}`);
                        errors.push({ rule_id: rule.rule_id, error: message });
                        rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
                    }
                }
            }
            else if (is_text_mode) {
                // Text mode
                input = { kind: 'text', text_content: text_content, file_name };
                const text_variables = {
                    ...variables,
                    document_name: file_name || 'text input',
                    ...(content_tag ? { document_type: content_tag } : {}),
                };
                for (const rule of rules) {
                    try {
                        const result = await run_rule(rule, input, text_variables, wrappers);
                        rule_results.push(result);
                        if (result.issues.length > 0 && rule.clarification_type !== 'none') {
                            clarifications.push(create_clarification_from_result(rule, result, { file_name: file_name || 'text input', mime_type: 'text/plain' }));
                        }
                    }
                    catch (err) {
                        const message = err instanceof Error ? err.message : 'Unknown error';
                        on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
                        errors.push({ rule_id: rule.rule_id, error: message });
                        rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
                    }
                }
            }
            else {
                // Single document
                let file_b64;
                try {
                    file_b64 = await fetch_file_b64(request.url, { file_b64: request_file_b64, download_url, file_name });
                }
                catch (err) {
                    return Response.json({
                        success: false,
                        clarifications: [],
                        rule_results: [],
                        errors: [{ rule_id: '', error: err instanceof Error ? err.message : 'Unknown error' }],
                    }, { status: err instanceof Error && err.message.includes('No file_b64') ? 400 : 500 });
                }
                input = { kind: 'document', file_name, mime_type, file_b64 };
                const doc_variables = {
                    ...variables,
                    document_name: file_name || 'document',
                    ...(content_tag ? { document_type: content_tag } : {}),
                };
                for (const rule of rules) {
                    try {
                        const result = await run_rule(rule, input, doc_variables, wrappers);
                        rule_results.push(result);
                        if (result.issues.length > 0 && rule.clarification_type !== 'none') {
                            clarifications.push(create_clarification_from_result(rule, result, { file_name, mime_type }));
                        }
                    }
                    catch (err) {
                        const message = err instanceof Error ? err.message : 'Unknown error';
                        on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
                        errors.push({ rule_id: rule.rule_id, error: message });
                        rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
                    }
                }
            }
            const response = {
                success: errors.length === 0,
                clarifications,
                rule_results,
                ...(errors.length > 0 ? { errors } : {}),
            };
            return Response.json(response);
        }
        catch (error) {
            on_error(error, 'unhandled error');
            return Response.json({
                success: false,
                clarifications: [],
                rule_results: [],
                errors: [{ rule_id: '', error: error instanceof Error ? error.message : 'Unknown error' }],
            }, { status: 500 });
        }
    };
}
//# sourceMappingURL=validation_handler.js.map