import fsp from 'node:fs/promises';
import path from 'node:path';
import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
import { codexSchemaPath, runCodexExecResumeWithOutputSchema, structuredOutputBlocker } from '../codex-exec-output-schema.js';
import { runOpenAIStructuredOutput } from '../structured-output-adapter.js';
import { validateJsonSchemaRecursive } from '../json-schema-validator.js';
import { sha256File } from '../wiki-image/image-hash.js';
import { PPT_SLIDE_CALLOUT_LEDGER_ARTIFACT } from './slide-imagegen-review.js';
export const PPT_SLIDE_ISSUE_LEDGER_ARTIFACT = 'ppt-slide-issue-ledger.json';
export const PPT_DECK_ISSUE_LEDGER_ARTIFACT = 'ppt-deck-issue-ledger.json';
export const PPT_SLIDE_EXTRACTION_REPORT_ARTIFACT = 'ppt-slide-extraction-report.json';
export async function extractSlideIssues(input = {}) {
    const root = String(input.root || process.cwd());
    const dir = String(input.dir);
    const mock = input.mock === true;
    const calloutLedger = input.calloutLedger || await readJson(path.join(dir, PPT_SLIDE_CALLOUT_LEDGER_ARTIFACT), {});
    const schemaPath = await codexSchemaPath('ppt-slide-issue-ledger');
    const jsonSchema = await readJson(schemaPath);
    let issues = [];
    const blockers = [];
    const reports = [];
    const images = Array.isArray(calloutLedger.generated_slide_callout_images) ? calloutLedger.generated_slide_callout_images : [];
    if (mock) {
        issues = images.flatMap((image) => (image.callouts || []).map((callout, index) => normalizePptIssue(callout, image, index, 'mock_fixture')));
        for (const image of images)
            reports.push(await buildSlideExtractionReport(root, image, { provider: 'mock_fixture', issues, schemaPath, ok: true, blockers: [] }));
    }
    else if (input.generatedSlidePath) {
        const extraction = await extractOneGeneratedSlide(root, schemaPath, jsonSchema, input.generatedSlidePath, input.sessionId || null);
        if (extraction.ok)
            issues = extraction.issues;
        else
            blockers.push(extraction.blocker?.reason || 'ppt_slide_issue_extraction_missing');
        reports.push(await buildSlideExtractionReport(root, { path: input.generatedSlidePath }, { ...extraction, schemaPath }));
    }
    else {
        for (const image of images) {
            if (!image.path)
                continue;
            const extraction = await extractOneGeneratedSlide(root, schemaPath, jsonSchema, image.path, input.sessionId || null, image);
            if (extraction.ok)
                issues.push(...extraction.issues);
            else
                blockers.push(extraction.blocker?.reason || 'ppt_slide_issue_extraction_missing');
            reports.push(await buildSlideExtractionReport(root, image, { ...extraction, schemaPath }));
        }
    }
    if (!issues.length)
        blockers.push('ppt_slide_issue_extraction_missing');
    const ledgerBase = {
        schema: 'sks.ppt-slide-issue-ledger.v1',
        schema_version: 1,
        created_at: nowIso(),
        issues,
        issue_count: issues.length,
        blocking_issue_count: issues.filter((issue) => ['P0', 'P1'].includes(issue.severity) && !['fixed', 'accepted_not_applicable'].includes(issue.status)).length,
        extraction_source: mock ? 'mock_fixture' : input.sessionId ? 'codex_exec_resume_output_schema' : 'structured_outputs_callout_extraction',
        mock_fixture: mock,
        blockers: [...new Set(blockers)],
        validation: { ok: true, issues: [] },
        passed: blockers.length === 0
    };
    const ledger = {
        ...ledgerBase,
        validation: validateJsonSchemaRecursive(ledgerBase, jsonSchema)
    };
    await writeJsonAtomic(path.join(dir, PPT_SLIDE_ISSUE_LEDGER_ARTIFACT), ledger);
    const extractionReport = {
        schema: 'sks.ppt-slide-extraction-report.v1',
        schema_version: 1,
        created_at: nowIso(),
        provider: mock ? 'mock_fixture' : input.sessionId ? 'codex_exec_resume_output_schema' : 'structured_outputs_callout_extraction',
        output_schema_path: schemaPath,
        slide_reports: reports,
        issue_count: issues.length,
        blockers: [...new Set(blockers)],
        fallback_used: !mock && !input.sessionId,
        validation_status: ledger.validation.ok ? 'valid' : 'blocked',
        passed: blockers.length === 0 && reports.every((report) => report.passed !== false)
    };
    await writeJsonAtomic(path.join(dir, PPT_SLIDE_EXTRACTION_REPORT_ARTIFACT), extractionReport);
    const deckLedger = buildDeckIssueLedger(ledger, calloutLedger);
    await writeJsonAtomic(path.join(dir, PPT_DECK_ISSUE_LEDGER_ARTIFACT), deckLedger);
    return { slide_issue_ledger: ledger, deck_issue_ledger: deckLedger, extraction_report: extractionReport };
}
export function buildDeckIssueLedger(slideLedger = {}, calloutLedger = {}) {
    const issues = Array.isArray(slideLedger.issues) ? slideLedger.issues : [];
    const p0p1 = issues.filter((issue) => ['P0', 'P1'].includes(issue.severity) && !['fixed', 'accepted_not_applicable'].includes(issue.status));
    const bySlide = new Map();
    for (const issue of issues) {
        const key = String(issue.slide_index || issue.slide_id || 'unknown');
        bySlide.set(key, [...(bySlide.get(key) || []), issue]);
    }
    return {
        schema: 'sks.ppt-deck-issue-ledger.v1',
        created_at: nowIso(),
        slide_count: Number(calloutLedger.slide_count || bySlide.size || 0),
        issue_count: issues.length,
        p0_p1_count: p0p1.length,
        narrative_flow_issue_count: issues.filter((issue) => issue.category === 'narrative').length,
        duplicate_issue_count: duplicateIssueCount(issues),
        slide_priorities: Array.from(bySlide.entries()).map(([slide, rows]) => ({ slide, max_severity: maxSeverity(rows), count: rows.length })),
        scorecard: {
            p0_p1_zero: p0p1.length === 0,
            extraction_complete: slideLedger.blockers?.length === 0,
            deck_recheck_required: p0p1.length > 0
        },
        blockers: slideLedger.blockers || [],
        passed: p0p1.length === 0 && (slideLedger.blockers || []).length === 0,
        mock_fixture: slideLedger.mock_fixture === true
    };
}
async function extractOneGeneratedSlide(root, schemaPath, jsonSchema, generatedSlidePath, sessionId, image = {}) {
    if (process.env.SKS_TEST_FAKE_IMAGEGEN === '1' || process.env.SKS_TEST_FAKE_EXTRACTOR === '1') {
        const issue = normalizePptIssue({
            id: 'fake-ppt-slide-issue-1',
            callout_id: 'fake-slide-callout-1',
            severity: 'P2',
            bbox: [0, 0, Math.max(1, Number(image.width || 1)), Math.max(1, Number(image.height || 1))],
            category: 'visual_hierarchy',
            title: 'Fake slide fixture callout',
            detail: 'Hermetic fake extractor issue from generated PPT slide callout fixture.',
            fix_action: 'No-op fixture recheck',
            target_element: 'fake slide canvas',
            confidence: 0.5,
            status: 'fixed'
        }, image, 0, 'mock_fixture');
        return {
            ok: true,
            issues: [issue],
            provider: 'fake_structured_extractor',
            blocker: null
        };
    }
    const prompt = [
        'Extract only visible numbered PPT slide review callouts from the generated review image.',
        'Return schema-valid JSON for sks.ppt-slide-issue-ledger.v1.',
        'Do not invent slide issues or infer hidden business requirements.',
        'Use bbox coordinates in generated image pixels as [x,y,width,height].',
        `Generated slide review image: ${generatedSlidePath}.`
    ].join('\n');
    const provider = sessionId
        ? await runCodexExecResumeWithOutputSchema({
            sessionId,
            prompt,
            outputSchemaPath: schemaPath,
            outputFile: path.join(root, '.sneakoscope', 'tmp', `ppt-slide-issues-${Date.now()}.json`)
        }, { cwd: root })
        : await runOpenAIStructuredOutput({
            prompt,
            schemaName: 'ppt_slide_issue_ledger',
            jsonSchema,
            imagePath: path.resolve(root, generatedSlidePath)
        });
    if (!provider.ok || !provider.parsed_json) {
        return { ok: false, issues: [], blocker: provider.blocker || structuredOutputBlocker('ppt_slide_issue_extraction_missing', 'PPT slide issue extraction did not return schema-valid JSON.') };
    }
    const parsed = provider.parsed_json;
    const rows = Array.isArray(parsed.issues) ? parsed.issues : [];
    return {
        ok: rows.length > 0,
        issues: rows.map((row, index) => normalizePptIssue(row, image, index, sessionId ? 'real_gpt_image_2_callout' : 'structured_outputs_callout_extraction')),
        provider: sessionId ? 'codex_exec_resume_output_schema' : 'structured_outputs_callout_extraction',
        blocker: rows.length ? null : structuredOutputBlocker('ppt_slide_issue_extraction_missing', 'Generated slide review image yielded no visible callouts.')
    };
}
async function buildSlideExtractionReport(root, image = {}, extraction = {}) {
    const imagePath = image.path || image.image_path || null;
    const sha = image.sha256 || (imagePath ? await sha256File(path.resolve(root, imagePath)).catch(() => null) : null);
    const issues = Array.isArray(extraction.issues) ? extraction.issues : [];
    const bboxIssues = issues.flatMap((issue) => bboxOutOfBounds(issue, image));
    const blocker = extraction.blocker?.reason || extraction.blocker || (extraction.ok ? null : 'ppt_slide_issue_extraction_missing');
    return {
        schema: 'sks.ppt-slide-extraction-report-item.v1',
        slide_id: image.slide_id || issues[0]?.slide_id || null,
        slide_index: image.slide_index || issues[0]?.slide_index || null,
        generated_image_path: imagePath,
        generated_image_sha256: sha,
        provider: extraction.provider || 'unknown',
        schema_validation: extraction.ok ? 'valid' : 'blocked',
        issue_count: issues.length,
        bbox_validation_issues: bboxIssues,
        blocker,
        passed: extraction.ok === true && issues.length > 0 && bboxIssues.length === 0
    };
}
function bboxOutOfBounds(issue = {}, image = {}) {
    const bbox = Array.isArray(issue.bbox) ? issue.bbox : [];
    const width = Number(image.width || 0);
    const height = Number(image.height || 0);
    if (bbox.length !== 4)
        return [`ppt_slide_bbox_invalid:${issue.id || 'unknown'}`];
    const [x, y, w, h] = bbox.map(Number);
    if ([x, y, w, h].some((value) => !Number.isFinite(value)) || w <= 0 || h <= 0)
        return [`ppt_slide_bbox_invalid:${issue.id || 'unknown'}`];
    if (width > 0 && height > 0 && (x < 0 || y < 0 || x + w > width || y + h > height))
        return [`ppt_slide_bbox_out_of_bounds:${issue.id || 'unknown'}`];
    return [];
}
function normalizePptIssue(issue = {}, image = {}, index = 0, source = 'real_gpt_image_2_callout') {
    const slideIndex = Number(issue.slide_index || image.slide_index || 1);
    return {
        id: issue.id || `ppt-slide-${slideIndex}-issue-${index + 1}`,
        slide_id: issue.slide_id || image.slide_id || `slide-${slideIndex}`,
        slide_index: slideIndex,
        generated_review_image_id: issue.generated_review_image_id || image.id || `ppt-generated-review-${slideIndex}`,
        callout_id: issue.callout_id || issue.id || `callout-${slideIndex}-${index + 1}`,
        bbox: Array.isArray(issue.bbox) ? issue.bbox : [0, 0, Math.max(1, Number(image.width || 1)), Math.max(1, Number(image.height || 1))],
        severity: ['P0', 'P1', 'P2', 'P3'].includes(issue.severity) ? issue.severity : 'P2',
        category: normalizeCategory(issue.category),
        title: issue.title || 'Slide visual issue',
        detail: issue.detail || 'Issue extracted from a generated PPT slide review image.',
        fix_action: issue.fix_action || 'Apply a targeted slide edit, export the slide, and re-review.',
        target_element: issue.target_element || issue.region || 'slide region',
        confidence: clamp(Number(issue.confidence ?? 0.5), 0, 1),
        source,
        status: issue.status || (issue.severity === 'P3' ? 'suggestion_only' : 'open')
    };
}
function normalizeCategory(value) {
    const allowed = new Set(['layout', 'typography', 'contrast', 'content_density', 'narrative', 'accessibility', 'visual_hierarchy', 'data_viz', 'brand', 'other']);
    return allowed.has(String(value)) ? String(value) : 'other';
}
function maxSeverity(rows) {
    const order = ['P0', 'P1', 'P2', 'P3'];
    return rows.map((row) => row.severity).sort((a, b) => order.indexOf(a) - order.indexOf(b))[0] || 'P3';
}
function duplicateIssueCount(issues) {
    const seen = new Set();
    let duplicates = 0;
    for (const issue of issues) {
        const key = `${issue.slide_index}:${issue.title}:${issue.target_element}`;
        if (seen.has(key))
            duplicates += 1;
        seen.add(key);
    }
    return duplicates;
}
function clamp(value, min, max) {
    if (!Number.isFinite(value))
        return min;
    return Math.max(min, Math.min(max, value));
}
//# sourceMappingURL=slide-issue-extraction.js.map