import fs from 'node:fs/promises';
import path from 'node:path';
import { Buffer } from 'node:buffer';
import { initCgdb, closeCgdb, executeParameterized } from '../cgdb/pool-adapter.js';
import { readRegistry } from '../../storage/repo-manager.js';
import { HttpRouteExtractor } from './extractors/http-route-extractor.js';
import { GrpcExtractor } from './extractors/grpc-extractor.js';
import { TopicExtractor } from './extractors/topic-extractor.js';
import { ManifestExtractor } from './extractors/manifest-extractor.js';
import { runExactMatch } from './matching.js';
import { detectServiceBoundaries, assignService } from './service-boundary-detector.js';
import { writeContractRegistry } from './storage.js';
export function stableRepoPoolId(entry, allEntries) {
    const base = entry.name.toLowerCase();
    const resolved = path.resolve(entry.path);
    for (const other of allEntries) {
        if (other.name.toLowerCase() === base && path.resolve(other.path) !== resolved) {
            const hash = Buffer.from(entry.path).toString('base64url').slice(0, 6);
            return `${base}-${hash}`;
        }
    }
    return base;
}
function defaultResolveHandle(allEntries) {
    return async (registryName, groupPath) => {
        const e = allEntries.find((en) => en.name === registryName);
        if (!e)
            return null;
        const poolId = stableRepoPoolId(e, allEntries);
        return {
            id: poolId,
            path: groupPath,
            repoPath: e.path,
            storagePath: e.storagePath,
        };
    };
}
/**
 * Dedupe cross-links that point from the same consumer endpoint to the same
 * provider endpoint for the same contract. Preserves first-seen order so the
 * caller controls precedence (e.g., pass manifest links first).
 */
function dedupeCrossLinks(links) {
    const seen = new Set();
    const out = [];
    for (const link of links) {
        const key = `${link.from.repo}::${link.from.symbolUid}|${link.to.repo}::${link.to.symbolUid}|${link.type}|${link.contractId}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        out.push(link);
    }
    return out;
}
export async function syncGroup(config, opts) {
    const missingRepos = [];
    const repoSnapshots = {};
    let autoContracts = [];
    let manifestCrossLinks = [];
    let dbExecutors;
    const eo = opts?.extractorOverride;
    if (eo && eo.length === 0) {
        autoContracts = await eo();
    }
    else {
        const entries = await readRegistry();
        const resolve = opts?.resolveRepoHandle ?? defaultResolveHandle(entries);
        const httpEx = new HttpRouteExtractor();
        const grpcEx = new GrpcExtractor();
        const topicEx = new TopicExtractor();
        dbExecutors = new Map();
        const openPoolIds = [];
        try {
            for (const [groupPath, regName] of Object.entries(config.repos)) {
                const handle = await resolve(regName, groupPath);
                if (!handle) {
                    missingRepos.push(groupPath);
                    continue;
                }
                const poolId = handle.id;
                const cgdbPath = path.join(handle.storagePath, 'cgdb');
                try {
                    await initCgdb(poolId, cgdbPath);
                    openPoolIds.push(poolId);
                    const executor = (query, params) => executeParameterized(poolId, query, params ?? {});
                    dbExecutors.set(groupPath, executor);
                    const boundaries = await detectServiceBoundaries(handle.repoPath);
                    if (config.detect.http) {
                        const extracted = await httpEx.extract(executor, handle.repoPath, handle);
                        for (const c of extracted) {
                            autoContracts.push({
                                ...c,
                                repo: groupPath,
                                service: assignService(c.symbolRef.filePath, boundaries),
                            });
                        }
                    }
                    if (config.detect.grpc) {
                        const extracted = await grpcEx.extract(executor, handle.repoPath, handle);
                        for (const c of extracted) {
                            autoContracts.push({
                                ...c,
                                repo: groupPath,
                                service: assignService(c.symbolRef.filePath, boundaries),
                            });
                        }
                    }
                    if (config.detect.topics) {
                        const extracted = await topicEx.extract(executor, handle.repoPath, handle);
                        for (const c of extracted) {
                            autoContracts.push({
                                ...c,
                                repo: groupPath,
                                service: assignService(c.symbolRef.filePath, boundaries),
                            });
                        }
                    }
                    const metaPath = path.join(handle.storagePath, 'meta.json');
                    try {
                        const raw = await fs.readFile(metaPath, 'utf-8');
                        const m = JSON.parse(raw);
                        repoSnapshots[groupPath] = {
                            indexedAt: m.indexedAt || '',
                            lastCommit: m.lastCommit || '',
                        };
                    }
                    catch {
                        const e = entries.find((en) => en.name === regName);
                        repoSnapshots[groupPath] = {
                            indexedAt: e?.indexedAt || '',
                            lastCommit: e?.lastCommit || '',
                        };
                    }
                }
                catch {
                    missingRepos.push(groupPath);
                }
            }
        }
        finally {
            for (const id of [...new Set(openPoolIds)]) {
                await closeCgdb(id).catch(() => { });
            }
        }
    }
    // Process manifest links declared in group.yaml.
    // ManifestExtractor is fully implemented but was never wired into this
    // pipeline — config.links were parsed and validated but silently dropped.
    // Placed after the DB try/finally: resolveSymbol falls back to synthetic
    // UIDs when dbExecutors is undefined or a pool is closed, so cross-links
    // are always generated regardless of whether real DB executors are available.
    if (config.links.length > 0) {
        // Warn about dangling links that reference repos not declared in config.repos.
        // They still generate cross-links via synthetic UIDs (determinism is preserved),
        // but the operator probably meant something that now silently does nothing useful.
        const knownRepos = new Set(Object.keys(config.repos));
        for (const link of config.links) {
            const dangling = [link.from, link.to].filter((r) => !knownRepos.has(r));
            if (dangling.length > 0) {
                console.warn(`[group/sync] manifest link ${link.type}:${link.contract} references repos not in config.repos: ${dangling.join(', ')} — cross-links will use synthetic UIDs`);
            }
        }
        const manifestEx = new ManifestExtractor();
        const manifestResult = await manifestEx.extractFromManifest(config.links, dbExecutors);
        autoContracts.push(...manifestResult.contracts);
        manifestCrossLinks = manifestResult.crossLinks;
        if (opts?.verbose) {
            console.log(`  manifest: ${manifestCrossLinks.length} cross-links from ${config.links.length} declared links`);
        }
    }
    const { matched, unmatched } = runExactMatch(autoContracts);
    // Dedupe cross-links. Manifest contracts participate in runExactMatch, so a
    // manifest-declared link can also emit a matchType:'exact' CrossLink with the
    // same endpoints. Prefer the manifest version — it reflects operator intent
    // and carries matchType:'manifest' which downstream consumers may rely on.
    const crossLinks = dedupeCrossLinks([...manifestCrossLinks, ...matched]);
    const allContracts = autoContracts;
    const registry = {
        version: 1,
        generatedAt: new Date().toISOString(),
        repoSnapshots,
        missingRepos,
        contracts: allContracts,
        crossLinks,
    };
    if (opts?.groupDir && !opts.skipWrite) {
        await writeContractRegistry(opts.groupDir, registry);
    }
    return {
        contracts: allContracts,
        crossLinks,
        unmatched,
        missingRepos,
        repoSnapshots,
    };
}
