/**
 * ADR DB Sync (ADR-017)
 *
 * Syncs ADR markdown frontmatter into the architecture_decisions DB table
 * AND regenerates .cleo/adrs/MANIFEST.jsonl in one pass.
 *
 * Single command: ct adr sync (or admin.adr.sync)
 * - Updates architecture_decisions + adr_task_links in SQLite (runtime search)
 * - Rewrites MANIFEST.jsonl (portable export, includes archive/ ADRs)
 *
 * @task T4792
 * @task T4942 — MANIFEST generation folded in so one command keeps both in sync
 */
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { adrTaskLinks, architectureDecisions, tasks } from '../store/tasks-schema.js';
import { parseAdrFile } from './parse.js';
/** Extract a bare ADR-NNN id from a frontmatter value like "ADR-006 (§3 ...)" */
function extractAdrIdFromRef(ref) {
    const m = ref.match(/^(ADR-\d+)/);
    return m ? m[1] : null;
}
/** Parse comma-separated task IDs from a Related Tasks value */
function parseTaskIds(raw) {
    return raw
        .split(',')
        .map((t) => t.trim())
        .filter((t) => /^T\d{1,5}$/.test(t));
}
/** Collect all ADR .md files recursively (top-level + archive/) */
function collectAdrFiles(dir) {
    const results = [];
    let entries;
    try {
        entries = readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return results;
    }
    for (const entry of entries) {
        if (entry.isDirectory()) {
            const sub = join(dir, entry.name);
            let subFiles;
            try {
                subFiles = readdirSync(sub);
            }
            catch {
                continue;
            }
            for (const f of subFiles) {
                if (f.endsWith('.md') && /^ADR-\d+/.test(f)) {
                    results.push({ file: f, relPath: `${entry.name}/${f}` });
                }
            }
        }
        else if (entry.name.endsWith('.md') && /^ADR-\d+/.test(entry.name)) {
            results.push({ file: entry.name, relPath: entry.name });
        }
    }
    return results.sort((a, b) => a.file.localeCompare(b.file));
}
/**
 * Sync all ADR markdown files into the architecture_decisions table
 * AND regenerate MANIFEST.jsonl in one pass.
 */
export async function syncAdrsToDb(projectRoot) {
    const adrsDir = join(projectRoot, '.cleo', 'adrs');
    const result = { inserted: 0, updated: 0, skipped: 0, errors: [], warnings: [] };
    if (!existsSync(adrsDir)) {
        return result;
    }
    const { getDb } = await import('../store/sqlite.js');
    const { eq } = await import('drizzle-orm');
    const db = await getDb(projectRoot);
    const now = new Date().toISOString();
    const allFiles = collectAdrFiles(adrsDir);
    // Only sync top-level files to DB (archive/ ADRs are superseded)
    const activeFiles = allFiles.filter((f) => !f.relPath.includes('/'));
    const manifestEntries = [];
    // --- DB sync (active ADRs only) ---
    for (const { file, relPath } of activeFiles) {
        try {
            const filePath = join(adrsDir, relPath);
            const record = parseAdrFile(filePath, projectRoot);
            const fm = record.frontmatter;
            const dbRelPath = `.cleo/adrs/${relPath}`;
            const content = readFileSync(filePath, 'utf-8');
            let supersedesId = fm.Supersedes ? extractAdrIdFromRef(fm.Supersedes) : null;
            let supersededById = fm['Superseded By'] ? extractAdrIdFromRef(fm['Superseded By']) : null;
            let amendsId = fm.Amends ? extractAdrIdFromRef(fm.Amends) : null;
            // Validate FK references exist before insert/update to avoid constraint violations
            if (supersedesId) {
                const exists = await db
                    .select({ id: architectureDecisions.id })
                    .from(architectureDecisions)
                    .where(eq(architectureDecisions.id, supersedesId))
                    .all();
                if (exists.length === 0) {
                    result.warnings.push(`${record.id}: supersedes target ${supersedesId} not found in DB, setting to null`);
                    supersedesId = null;
                }
            }
            if (supersededById) {
                const exists = await db
                    .select({ id: architectureDecisions.id })
                    .from(architectureDecisions)
                    .where(eq(architectureDecisions.id, supersededById))
                    .all();
                if (exists.length === 0) {
                    result.warnings.push(`${record.id}: supersededBy target ${supersededById} not found in DB, setting to null`);
                    supersededById = null;
                }
            }
            if (amendsId) {
                const exists = await db
                    .select({ id: architectureDecisions.id })
                    .from(architectureDecisions)
                    .where(eq(architectureDecisions.id, amendsId))
                    .all();
                if (exists.length === 0) {
                    result.warnings.push(`${record.id}: amends target ${amendsId} not found in DB, setting to null`);
                    amendsId = null;
                }
            }
            const rowBase = {
                id: record.id,
                title: record.title,
                status: fm.Status ?? 'proposed',
                content,
                filePath: dbRelPath,
                date: fm.Date ?? '',
                acceptedAt: fm.Accepted ?? null,
                gate: (fm.Gate ?? null),
                gateStatus: (fm['Gate Status'] ?? null),
                amendsId,
                supersedesId,
                supersededById,
                summary: fm.Summary ?? null,
                keywords: fm.Keywords ?? null,
                topics: fm.Topics ?? null,
                updatedAt: now,
            };
            const existing = await db
                .select({ id: architectureDecisions.id })
                .from(architectureDecisions)
                .where(eq(architectureDecisions.id, record.id))
                .all();
            if (existing.length > 0) {
                await db
                    .update(architectureDecisions)
                    .set(rowBase)
                    .where(eq(architectureDecisions.id, record.id));
                result.updated++;
            }
            else {
                await db.insert(architectureDecisions).values({ ...rowBase, createdAt: now });
                result.inserted++;
            }
            await db.delete(adrTaskLinks).where(eq(adrTaskLinks.adrId, record.id));
            if (fm['Related Tasks']) {
                for (const taskId of parseTaskIds(fm['Related Tasks'])) {
                    // Validate task exists before inserting link to avoid FK constraint violation
                    const taskExists = await db
                        .select({ id: tasks.id })
                        .from(tasks)
                        .where(eq(tasks.id, taskId))
                        .all();
                    if (taskExists.length === 0) {
                        result.warnings.push(`${record.id}: related task ${taskId} not found in DB, skipping link`);
                        continue;
                    }
                    await db.insert(adrTaskLinks).values({ adrId: record.id, taskId, linkType: 'related' });
                }
            }
        }
        catch (err) {
            result.errors.push({ file, error: String(err) });
        }
    }
    // --- MANIFEST.jsonl (all ADRs including archive/) ---
    for (const { relPath } of allFiles) {
        try {
            const filePath = join(adrsDir, relPath);
            const record = parseAdrFile(filePath, projectRoot);
            const fm = record.frontmatter;
            const entry = {
                id: record.id,
                file: `.cleo/adrs/${relPath}`,
                title: record.title,
                status: fm.Status ?? 'unknown',
                date: fm.Date ?? '',
            };
            if (fm.Accepted)
                entry['accepted'] = fm.Accepted;
            if (fm.Supersedes)
                entry['supersedes'] = fm.Supersedes;
            if (fm['Superseded By'])
                entry['supersededBy'] = fm['Superseded By'];
            if (fm.Amends)
                entry['amends'] = fm.Amends;
            if (fm['Amended By'])
                entry['amendedBy'] = fm['Amended By'];
            if (fm['Related Tasks']) {
                entry['relatedTasks'] = fm['Related Tasks']
                    .split(',')
                    .map((s) => s.trim())
                    .filter(Boolean);
            }
            if (fm.Gate)
                entry['gate'] = fm.Gate;
            if (fm['Gate Status'])
                entry['gateStatus'] = fm['Gate Status'];
            if (fm.Summary)
                entry['summary'] = fm.Summary;
            if (fm.Keywords)
                entry['keywords'] = fm.Keywords.split(',')
                    .map((s) => s.trim())
                    .filter(Boolean);
            if (fm.Topics)
                entry['topics'] = fm.Topics.split(',')
                    .map((s) => s.trim())
                    .filter(Boolean);
            manifestEntries.push(entry);
        }
        catch {
            // Non-fatal: a parse failure in one ADR doesn't block the manifest
        }
    }
    writeFileSync(join(adrsDir, 'MANIFEST.jsonl'), manifestEntries.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
    return result;
}
//# sourceMappingURL=sync.js.map