// engine/handoff-packager.ts — Assembles handoff context package for AI agents (SPEC-039)
import { createHash } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { stripFrontmatter } from './frontmatter-parser.js';
import { hashProjectPath, projectDataDir } from '../storage/base-store.js';
// ── Parsing helpers ──────────────────────────────────────────────────────────
async function safeReadFile(path) {
    if (!path) {
        return '';
    }
    try {
        const raw = await readFile(path, 'utf-8');
        return stripFrontmatter(raw);
    }
    catch {
        return '';
    }
}
function extractObjective(huContent, spec) {
    if (!huContent) {
        return spec.title;
    }
    // Look for "quiero" pattern (Spanish HU format)
    const quieroMatch = /quiero[^,.\n]+/i.exec(huContent);
    if (quieroMatch?.[0]) {
        return quieroMatch[0].trim().replace(/^quiero\s+/i, '');
    }
    // Fallback: first non-empty line after "Historia de Usuario" heading
    const lines = huContent.split('\n');
    const huIdx = lines.findIndex((l) => /historia de usuario/i.test(l));
    if (huIdx !== -1) {
        for (let i = huIdx + 1; i < lines.length && i < huIdx + 10; i++) {
            /* v8 ignore next 1 */
            const line = lines[i]?.trim() ?? '';
            if (line.length > 20 && !line.startsWith('#') && !line.startsWith('|')) {
                return line.replace(/\*\*/g, '').slice(0, 200);
            }
        }
    }
    return spec.title;
}
function extractCriteria(huContent) {
    if (!huContent) {
        return [];
    }
    const bddBlocks = extractBddCriteria(huContent);
    if (bddBlocks.length > 0) {
        return bddBlocks;
    }
    return huContent
        .split('\n')
        .filter((line) => /^- \[[ x]\]/.test(line.trim()))
        .map((line) => line.trim().replace(/^- \[[ x]\]\s*/, ''));
}
function extractBddCriteria(content) {
    const criteria = [];
    let current = [];
    for (const rawLine of content.split('\n')) {
        const trimmed = rawLine.trim().replace(/^-+\s*/, '');
        if (/^(GIVEN|WHEN|THEN|AND)\b/i.test(trimmed)) {
            if (/^GIVEN\b/i.test(trimmed) && current.length > 0) {
                criteria.push(current.join(' / '));
                current = [];
            }
            current.push(trimmed);
        }
        else if (current.length > 0 && trimmed === '') {
            criteria.push(current.join(' / '));
            current = [];
        }
    }
    if (current.length > 0) {
        criteria.push(current.join(' / '));
    }
    return criteria.filter((criterion) => /GIVEN\b/i.test(criterion) && /WHEN\b/i.test(criterion) && /THEN\b/i.test(criterion));
}
function extractFilesFromFicha(fichaContent) {
    if (!fichaContent) {
        return { toModify: [], toCreate: [] };
    }
    const toModify = [];
    const toCreate = [];
    const lines = fichaContent.split('\n');
    let inFilesSection = false;
    for (const line of lines) {
        const trimmed = line.trim();
        if (/archivos?\s*(clave|key|impactados?|modificar|crear)/i.test(trimmed)) {
            inFilesSection = true;
            continue;
        }
        if (inFilesSection && trimmed.startsWith('##')) {
            inFilesSection = false;
        }
        if (!inFilesSection) {
            continue;
        }
        // Parse table rows: | path/to/file.ts | role |
        const tableMatch = /\|\s*(`?)([./\w-]+\.[a-z]+)\1\s*\|(.+)\|/.exec(trimmed);
        if (!tableMatch) {
            continue;
        }
        /* v8 ignore next 2 */
        const filePath = tableMatch[2] ?? '';
        const role = (tableMatch[3] ?? '').toLowerCase();
        /* v8 ignore start */
        if (!filePath) {
            continue;
        }
        /* v8 ignore stop */
        if (role.includes('nuevo') || role.includes('new') || role.includes('crear')) {
            toCreate.push(filePath);
        }
        else {
            toModify.push(filePath);
        }
    }
    return { toModify, toCreate };
}
function extractBacktickedFiles(content) {
    const files = new Set();
    const matches = content.matchAll(/`([^`\n]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|yml|yaml|sh|css|scss|html))`/g);
    for (const match of matches) {
        const filePath = match[1]?.trim();
        if (filePath && !filePath.includes(' ')) {
            files.add(filePath);
        }
    }
    return [...files];
}
function extractLinesByKeywords(content, keywords) {
    return content
        .split('\n')
        .map((line) => line.trim().replace(/^[-*]\s*/, ''))
        .filter((line) => line.length > 0 && keywords.test(line))
        .slice(0, 12);
}
function extractOperationalSections(content, spec) {
    return {
        testPlan: extractLinesByKeywords(content, /\b(test|typecheck|lint|vitest|playwright|verification|validate|pnpm|npm)\b/i),
        risks: extractLinesByKeywords(content, /\b(risk|edge|failure|security|drift|migration|breaking|compatibility|rollback)\b/i),
        ownership: extractLinesByKeywords(content, /\b(owner|ownership|wave|agent|reviewer|arbiter|files?|responsible)\b/i),
        currentState: `Spec ${spec.id} is ${spec.status}; implementation must follow the approved spec artifact, not chat history.`,
        nextAction: spec.status === 'approved'
            ? 'Move to implementing with this handoff, then implement only the listed scope.'
            : 'Review the current spec status and move through the required SDD gates before implementation.',
    };
}
function extractOutOfScope(huContent, spec) {
    if (!huContent) {
        return [
            `Anything beyond the ${spec.target} layer`,
            'Performance optimizations not specified in criteria',
        ];
    }
    const lines = huContent.split('\n');
    const outOfScopeItems = [];
    let inOosSection = false;
    for (const line of lines) {
        const trimmed = line.trim();
        if (/out of scope|fuera del scope|no incluye/i.test(trimmed)) {
            inOosSection = true;
            continue;
        }
        if (inOosSection && trimmed.startsWith('##')) {
            inOosSection = false;
        }
        if (inOosSection && /^[-*]/.test(trimmed)) {
            const item = trimmed.replace(/^[-*]\s*/, '').trim();
            if (item.length > 0) {
                outOfScopeItems.push(item);
            }
        }
    }
    if (outOfScopeItems.length === 0) {
        return [
            `Changes beyond the ${spec.target} target layer`,
            'Refactors not explicitly requested in acceptance criteria',
            'New dependencies not listed in spec.md ## Technical',
        ];
    }
    return outOfScopeItems;
}
function buildConstraints(knowledge) {
    const constraints = [];
    const conventions = knowledge.conventions;
    const conventionEntries = Object.entries(conventions);
    for (const [key, value] of conventionEntries.slice(0, 5)) {
        constraints.push({
            type: 'interface',
            description: `Convention — ${key}: ${value}`,
            affectedItems: [],
        });
    }
    // Architecture constraints
    constraints.push({
        type: 'interface',
        description: `Architecture pattern: ${knowledge.architecture.primary} — do not break layer boundaries`,
        affectedItems: knowledge.layers,
    });
    return constraints;
}
// ── Public API ───────────────────────────────────────────────────────────────
export async function packageHandoff(spec, knowledge) {
    const warnings = [];
    const huContent = await safeReadFile(spec.specPath);
    const fichaContent = huContent;
    if (!huContent) {
        warnings.push('spec.md not found — using minimal package. Run create_spec to generate it.');
    }
    const objective = extractObjective(huContent, spec);
    const criteria = extractCriteria(huContent);
    const parsedFiles = extractFilesFromFicha(fichaContent);
    const discoveredFiles = extractBacktickedFiles(`${huContent}\n${fichaContent}`);
    const toModify = parsedFiles.toModify.length > 0 ? parsedFiles.toModify : discoveredFiles;
    const toCreate = parsedFiles.toCreate;
    const outOfScope = extractOutOfScope(huContent, spec);
    const constraints = buildConstraints(knowledge);
    const operational = extractOperationalSections(`${huContent}\n${fichaContent}`, spec);
    if (criteria.length === 0) {
        warnings.push('No acceptance criteria found with complete BDD structure — implementation handoff is blocked');
    }
    const blockers = [
        ...(criteria.length === 0 ? ['Missing BDD acceptance criteria'] : []),
        ...(toModify.length + toCreate.length === 0 ? ['Missing file ownership / target files'] : []),
        ...(operational.testPlan.length === 0 ? ['Missing test plan'] : []),
        ...(operational.risks.length === 0 ? ['Missing risk analysis'] : []),
        ...(operational.ownership.length === 0 ? ['Missing ownership'] : []),
    ];
    const pkg = {
        specId: spec.id,
        generatedAt: new Date().toISOString(),
        readinessScore: 0, // caller can populate with checkSpecReadiness result
        objective,
        criteria,
        filesToModify: toModify,
        filesToCreate: toCreate,
        ownership: operational.ownership,
        testPlan: operational.testPlan,
        risks: operational.risks,
        outOfScope,
        currentState: operational.currentState,
        nextAction: operational.nextAction,
        constraints,
        warnings,
        blocked: blockers.length > 0,
        blockers,
    };
    return persistHandoffIfPossible(pkg, knowledge);
}
function renderPersistedHandoff(pkg) {
    return [
        `# Implementation Handoff Package — ${pkg.specId}`,
        '',
        `Generated: ${pkg.generatedAt}`,
        `Blocked: ${pkg.blocked ? 'yes' : 'no'}`,
        '',
        '## Objective',
        pkg.objective,
        '',
        '## Acceptance Criteria (BDD)',
        ...pkg.criteria.map((criterion) => `- ${criterion}`),
        '',
        '## Files To Modify',
        ...pkg.filesToModify.map((filePath) => `- ${filePath}`),
        '',
        '## Files To Create',
        ...pkg.filesToCreate.map((filePath) => `- ${filePath}`),
        '',
        '## Ownership',
        ...pkg.ownership.map((item) => `- ${item}`),
        '',
        '## Test Plan',
        ...pkg.testPlan.map((item) => `- ${item}`),
        '',
        '## Risks',
        ...pkg.risks.map((item) => `- ${item}`),
        '',
        '## Out Of Scope',
        ...pkg.outOfScope.map((item) => `- ${item}`),
        '',
        '## Current State',
        pkg.currentState,
        '',
        '## Next Action',
        pkg.nextAction,
        '',
        '## Blockers',
        ...(pkg.blockers.length > 0 ? pkg.blockers.map((item) => `- ${item}`) : ['- None']),
        '',
    ].join('\n');
}
async function persistHandoffIfPossible(pkg, knowledge) {
    if (!knowledge.projectPath) {
        return pkg;
    }
    const handoffDir = join(projectDataDir(hashProjectPath(knowledge.projectPath)), 'handoffs');
    const handoffPath = join(handoffDir, `${pkg.specId}.md`);
    const sessionContextPath = join(knowledge.projectPath, 'planu', 'session-context.md');
    const markdown = renderPersistedHandoff(pkg);
    try {
        await mkdir(handoffDir, { recursive: true });
        await writeFile(handoffPath, markdown, 'utf-8');
    }
    catch {
        return {
            ...pkg,
            warnings: [
                ...pkg.warnings,
                'Could not persist handoff artifact — context gate will block implementing.',
            ],
            blocked: true,
            blockers: [...pkg.blockers, 'Handoff artifact could not be persisted'],
        };
    }
    const contextContent = await readFile(sessionContextPath, 'utf-8').catch(() => '');
    const contextHash = createHash('sha256')
        .update(`${contextContent}\n---HANDOFF---\n${markdown}`, 'utf8')
        .digest('hex');
    return { ...pkg, handoffPath, contextHash };
}
//# sourceMappingURL=handoff-packager.js.map