import { DomainError } from './tags.js';
const ALLOWED_RELATIONS = new Set([
    'depends_on',
    'implements',
    'extends',
    'example_of',
    'contradicts',
    'see_also',
]);
export class DependenciesService {
    db;
    constructor(db) {
        this.db = db;
    }
    list(query = {}) {
        const where = [];
        const params = [];
        if (query.status) {
            where.push('sd.status = ?');
            params.push(query.status);
        }
        if (query.relation) {
            where.push('sd.relation = ?');
            params.push(query.relation);
        }
        if (query.pagePath) {
            where.push('(src.page_path = ? OR tgt.page_path = ?)');
            params.push(query.pagePath, query.pagePath);
        }
        const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
        const rows = this.db
            .prepare(`SELECT sd.*,
                src.page_path AS source_page_path, src.heading_text AS source_heading_text,
                tgt.page_path AS target_page_path, tgt.heading_text AS target_heading_text
           FROM section_dependency sd
           LEFT JOIN section_index src ON src.anchor = sd.source_anchor
           LEFT JOIN section_index tgt ON tgt.anchor = sd.target_anchor
           ${whereSql}
          ORDER BY sd.status, sd.updated_at DESC`)
            .all(...params);
        return rows.map((r) => this.hydrate(r));
    }
    getById(id) {
        const row = this.db
            .prepare(`SELECT sd.*,
                src.page_path AS source_page_path, src.heading_text AS source_heading_text,
                tgt.page_path AS target_page_path, tgt.heading_text AS target_heading_text
           FROM section_dependency sd
           LEFT JOIN section_index src ON src.anchor = sd.source_anchor
           LEFT JOIN section_index tgt ON tgt.anchor = sd.target_anchor
          WHERE sd.id = ?`)
            .get(id);
        return row ? this.hydrate(row) : null;
    }
    listForAnchor(anchor) {
        const asSource = this.list({}).filter((d) => d.sourceAnchor === anchor);
        const asTarget = this.list({}).filter((d) => d.targetAnchor === anchor);
        return { asSource, asTarget };
    }
    create(input) {
        if (!ALLOWED_RELATIONS.has(input.relation)) {
            throw new DomainError('VALIDATION', `invalid relation '${input.relation}'`);
        }
        if (input.sourceAnchor === input.targetAnchor) {
            throw new DomainError('VALIDATION', 'source and target anchors must differ');
        }
        const tx = this.db.transaction(() => {
            const src = this.db
                .prepare('SELECT anchor FROM section_index WHERE anchor = ?')
                .get(input.sourceAnchor);
            if (!src) {
                throw new DomainError('NOT_FOUND', `source anchor '${input.sourceAnchor}' not found`);
            }
            const tgt = this.db
                .prepare('SELECT anchor, content_hash FROM section_index WHERE anchor = ?')
                .get(input.targetAnchor);
            if (!tgt) {
                throw new DomainError('NOT_FOUND', `target anchor '${input.targetAnchor}' not found`);
            }
            const existing = this.db
                .prepare(`SELECT id FROM section_dependency
             WHERE source_anchor = ? AND target_anchor = ? AND relation = ?`)
                .get(input.sourceAnchor, input.targetAnchor, input.relation);
            if (existing) {
                this.db
                    .prepare(`UPDATE section_dependency
                SET description = COALESCE(?, description),
                    status = 'active',
                    last_verified_hash = ?,
                    updated_at = datetime('now')
              WHERE id = ?`)
                    .run(input.description ?? null, tgt.content_hash, existing.id);
                return existing.id;
            }
            const info = this.db
                .prepare(`INSERT INTO section_dependency
              (source_anchor, target_anchor, relation, description, status, last_verified_hash)
           VALUES (?, ?, ?, ?, 'active', ?)`)
                .run(input.sourceAnchor, input.targetAnchor, input.relation, input.description ?? null, tgt.content_hash);
            return Number(info.lastInsertRowid);
        });
        const id = tx();
        return this.getById(id);
    }
    remove(id) {
        const row = this.getById(id);
        if (!row)
            throw new DomainError('NOT_FOUND', `dependency ${id} not found`);
        this.db.prepare('DELETE FROM section_dependency WHERE id = ?').run(id);
    }
    verify(id) {
        const tx = this.db.transaction(() => {
            const dep = this.getById(id);
            if (!dep)
                throw new DomainError('NOT_FOUND', `dependency ${id} not found`);
            const tgt = this.db
                .prepare('SELECT content_hash FROM section_index WHERE anchor = ?')
                .get(dep.targetAnchor);
            if (!tgt)
                throw new DomainError('NOT_FOUND', `target section '${dep.targetAnchor}' not found`);
            this.db
                .prepare(`UPDATE section_dependency
              SET status = 'active', last_verified_hash = ?, updated_at = datetime('now')
            WHERE id = ?`)
                .run(tgt.content_hash, id);
        });
        tx();
        return this.getById(id);
    }
    impactAnalysis(targetAnchor) {
        const target = this.db
            .prepare('SELECT anchor, heading_text, page_path FROM section_index WHERE anchor = ?')
            .get(targetAnchor);
        if (!target)
            return null;
        const rows = this.db
            .prepare(`SELECT sd.source_anchor, sd.relation, sd.status, sd.description,
                src.heading_text AS source_heading_text, src.page_path AS source_page_path
           FROM section_dependency sd
           JOIN section_index src ON src.anchor = sd.source_anchor
          WHERE sd.target_anchor = ?
          ORDER BY sd.status, src.page_path, src.heading_text`)
            .all(targetAnchor);
        return {
            targetAnchor: target.anchor,
            targetHeadingText: target.heading_text,
            targetPagePath: target.page_path,
            dependents: rows.map((r) => ({
                sourceAnchor: r.source_anchor,
                sourceHeadingText: r.source_heading_text,
                sourcePagePath: r.source_page_path,
                relation: r.relation,
                status: r.status,
                description: r.description,
            })),
        };
    }
    counts() {
        const rows = this.db
            .prepare(`SELECT status, COUNT(*) AS c FROM section_dependency GROUP BY status`)
            .all();
        const out = { active: 0, stale: 0, broken: 0 };
        for (const r of rows) {
            if (r.status === 'active' || r.status === 'stale' || r.status === 'broken') {
                out[r.status] = r.c;
            }
        }
        return out;
    }
    countByStatus(status) {
        const row = this.db
            .prepare('SELECT COUNT(*) AS c FROM section_dependency WHERE status = ?')
            .get(status);
        return row.c;
    }
    graph(query = {}) {
        const { relation, status, pagePathPrefix, includeIsolated = false } = query;
        const sectionRows = this.db
            .prepare(pagePathPrefix
            ? `SELECT anchor, page_path, heading_text FROM section_index
               WHERE page_path LIKE ? ORDER BY page_path, line_start`
            : `SELECT anchor, page_path, heading_text FROM section_index
               ORDER BY page_path, line_start`)
            .all(...(pagePathPrefix ? [`${pagePathPrefix}%`] : []));
        const scopedAnchors = new Set(sectionRows.map((s) => s.anchor));
        let edges = this.list();
        edges = edges.filter((e) => scopedAnchors.has(e.sourceAnchor) || scopedAnchors.has(e.targetAnchor));
        if (relation && relation.length > 0) {
            const set = new Set(relation);
            edges = edges.filter((e) => set.has(e.relation));
        }
        if (status && status.length > 0) {
            const set = new Set(status);
            edges = edges.filter((e) => set.has(e.status));
        }
        const touched = new Set();
        for (const e of edges) {
            touched.add(e.sourceAnchor);
            touched.add(e.targetAnchor);
        }
        const nodes = sectionRows
            .filter((s) => includeIsolated || touched.has(s.anchor))
            .map((s) => ({
            anchor: s.anchor,
            headingText: s.heading_text,
            pagePath: s.page_path,
            isolated: !touched.has(s.anchor),
        }));
        const cycles = detectCycles(edges.map((e) => [e.sourceAnchor, e.targetAnchor]));
        return { nodes, edges, cycles };
    }
    suggest(anchor) {
        const source = this.db
            .prepare('SELECT anchor, page_path, heading_text FROM section_index WHERE anchor = ?')
            .get(anchor);
        if (!source)
            return [];
        const existing = new Set(this.db
            .prepare('SELECT target_anchor FROM section_dependency WHERE source_anchor = ?')
            .all(anchor).map((r) => r.target_anchor));
        const sharedEntities = this.db
            .prepare(`SELECT s2.anchor, s2.heading_text, s2.page_path, COUNT(*) AS overlap
           FROM section_entity_link sel1
           JOIN section_entity_link sel2
             ON sel1.entity_type = sel2.entity_type AND sel1.entity_id = sel2.entity_id
           JOIN section_index s2 ON s2.anchor = sel2.anchor
          WHERE sel1.anchor = ? AND sel2.anchor != ?
          GROUP BY s2.anchor
          ORDER BY overlap DESC
          LIMIT 20`)
            .all(anchor, anchor);
        const suggestions = sharedEntities
            .filter((s) => !existing.has(s.anchor) && s.anchor !== anchor)
            .map((s) => ({
            targetAnchor: s.anchor,
            heading: s.heading_text,
            pagePath: s.page_path,
            confidence: Math.min(1, 0.3 + s.overlap * 0.15),
            reason: `Shares ${s.overlap} entit${s.overlap === 1 ? 'y' : 'ies'} with source section`,
        }));
        return suggestions.slice(0, 10);
    }
    hydrate(row) {
        return {
            id: row.id,
            sourceAnchor: row.source_anchor,
            targetAnchor: row.target_anchor,
            relation: row.relation,
            description: row.description,
            status: row.status,
            lastVerifiedHash: row.last_verified_hash,
            createdAt: row.created_at,
            updatedAt: row.updated_at,
            sourcePagePath: row.source_page_path,
            sourceHeadingText: row.source_heading_text,
            targetPagePath: row.target_page_path,
            targetHeadingText: row.target_heading_text,
        };
    }
}
export function detectCycles(edges) {
    const adj = new Map();
    for (const [s, t] of edges) {
        if (!adj.has(s))
            adj.set(s, []);
        adj.get(s).push(t);
    }
    const WHITE = 0;
    const GRAY = 1;
    const BLACK = 2;
    const color = new Map();
    const cycles = [];
    const parent = new Map();
    const dfs = (node) => {
        color.set(node, GRAY);
        for (const next of adj.get(node) ?? []) {
            if (color.get(next) === GRAY) {
                const cycle = [next];
                let cur = node;
                while (cur && cur !== next) {
                    cycle.push(cur);
                    cur = parent.get(cur) ?? null;
                }
                cycle.push(next);
                cycles.push(cycle.reverse());
            }
            else if ((color.get(next) ?? WHITE) === WHITE) {
                parent.set(next, node);
                dfs(next);
            }
        }
        color.set(node, BLACK);
    };
    for (const node of adj.keys()) {
        if ((color.get(node) ?? WHITE) === WHITE) {
            parent.set(node, null);
            dfs(node);
        }
    }
    return cycles;
}
//# sourceMappingURL=dependencies.js.map