import Database from 'better-sqlite3';
import { openRepo } from '../db/index.js';
import { SqliteRepository } from '../db/repository/sqlite-repository.js';
import { findMatchingNodes } from '../domain/queries.js';
import { loadConfig } from '../infrastructure/config.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { paginateResult } from '../shared/paginate.js';
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
// ─── Alias generation ────────────────────────────────────────────────
function buildAliases(files) {
    const aliases = new Map();
    const basenames = new Map();
    // Group by basename
    for (const file of files) {
        const base = (file.split('/').pop() ?? file).replace(/\.[^.]+$/, '');
        if (!basenames.has(base))
            basenames.set(base, []);
        basenames.get(base).push(file);
    }
    for (const [base, paths] of basenames) {
        if (paths.length === 1) {
            aliases.set(paths[0], base);
        }
        else {
            // Collision — progressively add parent dirs until aliases are unique
            for (let depth = 2; depth <= 10; depth++) {
                const trial = new Map();
                let allUnique = true;
                const seen = new Set();
                for (const p of paths) {
                    const parts = p.replace(/\.[^.]+$/, '').split('/');
                    const alias = parts
                        .slice(-depth)
                        .join('_')
                        .replace(/[^a-zA-Z0-9_-]/g, '_');
                    trial.set(p, alias);
                    if (seen.has(alias))
                        allUnique = false;
                    seen.add(alias);
                }
                if (allUnique || depth === 10) {
                    for (const [p, alias] of trial) {
                        aliases.set(p, alias);
                    }
                    break;
                }
            }
        }
    }
    return aliases;
}
function findEntryNode(repo, name, opts) {
    let matchNode = findMatchingNodes(repo, name, opts)[0] ?? null;
    if (!matchNode) {
        for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
            matchNode = findMatchingNodes(repo, `${prefix}${name}`, opts)[0] ?? null;
            if (matchNode)
                break;
        }
    }
    return matchNode;
}
function processCallee(c, caller, depth, noTests, frame) {
    if (noTests && isTestFile(c.file))
        return;
    frame.fileSet.add(c.file);
    frame.messages.push({
        from: caller.file,
        to: c.file,
        label: c.name,
        type: 'call',
        depth,
    });
    if (frame.visited.has(c.id))
        return;
    frame.visited.add(c.id);
    frame.nextFrontier.push(c.id);
    frame.idToNode.set(c.id, c);
}
function bfsCallees(repo, matchNode, maxDepth, noTests) {
    const visited = new Set([matchNode.id]);
    let frontier = [matchNode.id];
    const messages = [];
    const fileSet = new Set([matchNode.file]);
    const idToNode = new Map();
    idToNode.set(matchNode.id, matchNode);
    let truncated = false;
    for (let d = 1; d <= maxDepth; d++) {
        const frame = { visited, messages, fileSet, idToNode, nextFrontier: [] };
        for (const fid of frontier) {
            const caller = idToNode.get(fid);
            for (const c of repo.findCallees(fid)) {
                processCallee(c, caller, d, noTests, frame);
            }
        }
        frontier = frame.nextFrontier;
        if (frontier.length === 0)
            break;
        if (d === maxDepth && frontier.some((fid) => repo.findCallees(fid).length > 0)) {
            truncated = true;
        }
    }
    return { messages, fileSet, idToNode, truncated };
}
function annotateDataflow(repo, messages, idToNode, dbPath) {
    const hasTable = repo.hasDataflowTable();
    if (!hasTable)
        return;
    let db;
    let ownDb = false;
    if (repo instanceof SqliteRepository) {
        db = repo.db;
    }
    else if (dbPath) {
        db = new Database(dbPath, { readonly: true });
        ownDb = true;
    }
    else {
        return;
    }
    try {
        _annotateDataflowImpl(db, messages, idToNode);
    }
    finally {
        if (ownDb)
            db.close();
    }
}
function appendReturnMessages(messages, nodeByNameFile, stmts) {
    const seenReturns = new Set();
    for (const msg of [...messages]) {
        if (msg.type !== 'call')
            continue;
        const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
        if (!targetNode)
            continue;
        const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
        if (seenReturns.has(returnKey))
            continue;
        const returns = stmts.getReturns.all(targetNode.id);
        if (returns.length === 0)
            continue;
        seenReturns.add(returnKey);
        messages.push({
            from: msg.to,
            to: msg.from,
            label: returns[0].expression || 'result',
            type: 'return',
            depth: msg.depth,
        });
    }
}
function annotateCallParams(messages, nodeByNameFile, stmts) {
    for (const msg of messages) {
        if (msg.type !== 'call')
            continue;
        const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
        if (!targetNode)
            continue;
        const params = stmts.getFlowsTo.all(targetNode.id);
        const paramNames = params
            .map((p) => p.expression)
            .filter(Boolean)
            .slice(0, 3);
        if (paramNames.length > 0) {
            msg.label = `${msg.label}(${paramNames.join(', ')})`;
        }
    }
}
function _annotateDataflowImpl(db, messages, idToNode) {
    const nodeByNameFile = new Map();
    for (const n of idToNode.values()) {
        nodeByNameFile.set(`${n.name}|${n.file}`, n);
    }
    const stmts = {
        getReturns: db.prepare(`SELECT d.expression FROM dataflow d
         WHERE d.source_id = ? AND d.kind = 'returns'`),
        getFlowsTo: db.prepare(`SELECT d.expression FROM dataflow d
         WHERE d.target_id = ? AND d.kind = 'flows_to'
         ORDER BY d.param_index`),
    };
    appendReturnMessages(messages, nodeByNameFile, stmts);
    annotateCallParams(messages, nodeByNameFile, stmts);
}
function buildParticipants(fileSet, entryFile) {
    const aliases = buildAliases([...fileSet]);
    const participants = [...fileSet].map((file) => ({
        id: aliases.get(file),
        label: file.split('/').pop() ?? file,
        file,
    }));
    participants.sort((a, b) => {
        if (a.file === entryFile)
            return -1;
        if (b.file === entryFile)
            return 1;
        return a.file.localeCompare(b.file);
    });
    return { participants, aliases };
}
export function sequenceData(name, dbPath, opts = {}) {
    const { repo, close } = openRepo(dbPath, opts);
    try {
        const config = opts.config || loadConfig();
        const maxDepth = opts.depth || config.analysis.sequenceDepth || 10;
        const noTests = opts.noTests || false;
        const matchNode = findEntryNode(repo, name, opts);
        if (!matchNode) {
            return {
                entry: null,
                participants: [],
                messages: [],
                depth: maxDepth,
                totalMessages: 0,
                truncated: false,
            };
        }
        const entry = {
            name: matchNode.name,
            file: matchNode.file,
            kind: matchNode.kind,
            line: matchNode.line,
        };
        const { messages, fileSet, idToNode, truncated } = bfsCallees(repo, matchNode, maxDepth, noTests);
        if (opts.dataflow && messages.length > 0) {
            annotateDataflow(repo, messages, idToNode, dbPath);
        }
        messages.sort((a, b) => {
            if (a.depth !== b.depth)
                return a.depth - b.depth;
            if (a.type === 'call' && b.type === 'return')
                return -1;
            if (a.type === 'return' && b.type === 'call')
                return 1;
            return 0;
        });
        const { participants, aliases } = buildParticipants(fileSet, entry.file);
        for (const msg of messages) {
            msg.from = aliases.get(msg.from);
            msg.to = aliases.get(msg.to);
        }
        const base = {
            entry,
            participants,
            messages,
            depth: maxDepth,
            totalMessages: messages.length,
            truncated,
        };
        const result = paginateResult(base, 'messages', {
            limit: opts.limit,
            offset: opts.offset,
        });
        if (opts.limit !== undefined || opts.offset !== undefined) {
            const activeFiles = new Set(result.messages.flatMap((m) => [m.from, m.to]));
            result.participants = result.participants.filter((p) => activeFiles.has(p.id));
        }
        return result;
    }
    finally {
        close();
    }
}
// Re-export Mermaid renderer from presentation layer
export { sequenceToMermaid } from '../presentation/sequence-renderer.js';
//# sourceMappingURL=sequence.js.map