import { readFile, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { parseAtomMarkdown, toAtomInputFromParsed } from './atom-codec.js';
import { parseKnowledgeMarkdown } from './knowledge-codec.js';
import { parseEdgesJsonl } from './edges-codec.js';
import { readManifest, sha256OfBuffer } from './manifest.js';
import { assertSafeBundlePath, assertSafeChildName } from '../security/safe-paths.js';
export async function importPack(store, opts) {
    const manifest = await readManifest(join(opts.from, 'manifest.json'));
    const project = opts.project ?? manifest.project;
    const report = {
        atomsInserted: 0,
        atomsUnchanged: 0,
        conflictsQueued: 0,
        knowledgeInserted: 0,
        knowledgeUnchanged: 0,
        edgesInserted: 0,
        edgesUpdated: 0,
        userStyleInserted: 0,
        userStyleSkipped: 0,
        bundleSource: opts.from,
    };
    const onConflict = opts.onConflict ?? 'review';
    const edgesContent = await readFile(join(opts.from, 'edges.jsonl'), 'utf8');
    const expectedHash = manifest.integrity['edges.jsonl'];
    const actualHash = sha256OfBuffer(edgesContent);
    if (expectedHash && expectedHash !== actualHash) {
        process.stderr.write(`[import-pack] edges.jsonl hash mismatch: expected ${expectedHash}, got ${actualHash}\n`);
    }
    const layout = manifest.layout;
    const atomRel = layout === 'categorized-v2'
        ? await listAreaFiles(opts.from, 'atoms')
        : (await readdir(join(opts.from, 'atoms'))).filter((f) => f.endsWith('.md')).map((f) => join('atoms', f));
    for (const rel of atomRel) {
        const raw = await readFile(join(opts.from, rel), 'utf8');
        const parsed = parseAtomMarkdown(raw, { filename: rel });
        const incoming = toAtomInputFromParsed(parsed);
        incoming.project = project;
        const existing = await store.getAtom(incoming.id);
        if (!existing) {
            if (!opts.dryRun) {
                const created = await store.createAtom({
                    project: incoming.project,
                    claim: incoming.claim,
                    type: incoming.type,
                    evidence: incoming.evidence,
                    trigger: incoming.trigger,
                    verification: incoming.verification,
                    pitfalls: incoming.pitfalls,
                    links: incoming.links,
                    producedBy: 'user',
                });
                // Imported atoms always start at draft locally; the source's tier is
                // advisory and gets reviewed before promotion.
                await store.updateAtom(created.id, { tier: 'draft' });
            }
            report.atomsInserted += 1;
            continue;
        }
        if (atomsEquivalent(existing, incoming)) {
            report.atomsUnchanged += 1;
            continue;
        }
        if (onConflict === 'skip') {
            continue;
        }
        if (!opts.dryRun) {
            await store.createAtomImportConflict({
                project,
                atomId: existing.id,
                localSnapshot: existing,
                importedSnapshot: { ...parsed.frontmatter, body: parsed.body },
                bundleSource: opts.from,
            });
        }
        report.conflictsQueued += 1;
    }
    const kRel = layout === 'categorized-v2'
        ? await listAreaFiles(opts.from, 'knowledge')
        : (await readdir(join(opts.from, 'knowledge'))).filter((f) => f.endsWith('.md')).map((f) => join('knowledge', f));
    for (const rel of kRel) {
        const raw = await readFile(join(opts.from, rel), 'utf8');
        const parsed = parseKnowledgeMarkdown(raw, { filename: rel });
        const existing = await store.getKnowledge(parsed.frontmatter.id);
        if (!existing) {
            if (!opts.dryRun) {
                await store.upsertKnowledge({
                    project,
                    sourceType: 'imported',
                    sourceUri: `bundle://${opts.from}/${rel}`,
                    itemType: parsed.frontmatter.itemType,
                    title: parsed.frontmatter.title,
                    summary: '',
                    content: parsed.body.trim(),
                    labels: parsed.frontmatter.labels,
                    references: parsed.frontmatter.references,
                    trustLevel: parsed.frontmatter.trustLevel,
                    metadata: { importedFrom: opts.from },
                }, []);
            }
            report.knowledgeInserted += 1;
        }
        else {
            report.knowledgeUnchanged += 1;
        }
    }
    const edges = parseEdgesJsonl(edgesContent);
    for (const edge of edges) {
        const existing = await store.listAtomRelations({
            fromAtomId: edge.from,
            targetAtomId: edge.to,
            limit: 5,
        });
        const same = existing.find((r) => r.relationType === edge.kind && r.inferenceSource === edge.inferenceSource);
        if (!same) {
            if (!opts.dryRun) {
                await store.replaceAtomRelations(edge.from, [
                    {
                        fromAtomId: edge.from,
                        targetAtomId: edge.to,
                        relationType: edge.kind,
                        confidence: edge.confidence,
                        inferenceSource: edge.inferenceSource,
                    },
                ], { source: edge.inferenceSource });
            }
            report.edgesInserted += 1;
        }
        else if (edge.confidence > same.confidence) {
            if (!opts.dryRun) {
                await store.replaceAtomRelations(edge.from, [
                    {
                        fromAtomId: edge.from,
                        targetAtomId: edge.to,
                        relationType: edge.kind,
                        confidence: edge.confidence,
                        inferenceSource: edge.inferenceSource,
                    },
                ], { source: edge.inferenceSource });
            }
            report.edgesUpdated += 1;
        }
    }
    // Concern F — import user-style atoms under user-style/<userId>/*. The
    // resulting atoms are downgraded to tier='draft' and (by default) priority=
    // 'coding_preference' so a teammate's personal_workflow can't override the
    // local user's project conventions. The userId is rewritten to the
    // importer's `targetUserId` unless `preserveUserId` is set.
    const userStyleDirs = await safeListUserStyleDirs(opts.from);
    for (const userIdDir of userStyleDirs) {
        const sourceUserId = userIdDir;
        const effectiveUserId = opts.preserveUserId ? sourceUserId : (opts.targetUserId ?? undefined);
        if (!effectiveUserId) {
            report.userStyleSkipped += 1;
            continue;
        }
        await assertSafeBundlePath(opts.from, join('user-style', userIdDir));
        const dir = join(opts.from, 'user-style', userIdDir);
        const allFiles = await readdir(dir);
        const files = allFiles.filter((f) => {
            if (!f.endsWith('.md'))
                return false;
            try {
                assertSafeChildName(f);
                return true;
            }
            catch {
                return false;
            }
        });
        for (const file of files) {
            const raw = await readFile(join(dir, file), 'utf8');
            const parsed = parseAtomMarkdown(raw, { filename: `user-style/${userIdDir}/${file}` });
            const incoming = toAtomInputFromParsed(parsed);
            const priority = opts.preservePriority
                ? (parsed.frontmatter.priority ?? 'coding_preference')
                : 'coding_preference';
            const existing = await store.getAtom(incoming.id);
            if (existing) {
                report.userStyleSkipped += 1;
                if (onConflict === 'skip')
                    continue;
                if (!opts.dryRun) {
                    await store.createAtomImportConflict({
                        project: `__user:${effectiveUserId}`,
                        atomId: existing.id,
                        localSnapshot: existing,
                        importedSnapshot: { ...parsed.frontmatter, body: parsed.body },
                        bundleSource: opts.from,
                    });
                }
                report.conflictsQueued += 1;
                continue;
            }
            if (!opts.dryRun) {
                const created = await store.createAtom({
                    project: `__user:${effectiveUserId}`,
                    claim: incoming.claim,
                    type: incoming.type,
                    evidence: incoming.evidence,
                    trigger: incoming.trigger,
                    verification: incoming.verification,
                    pitfalls: incoming.pitfalls,
                    links: incoming.links,
                    producedBy: 'user',
                    scope: 'user',
                    userId: effectiveUserId,
                    priority,
                });
                // Imported user-style atoms always start at draft locally.
                await store.updateAtom(created.id, { tier: 'draft' });
            }
            report.userStyleInserted += 1;
        }
    }
    return report;
}
async function safeListUserStyleDirs(bundleRoot) {
    try {
        const root = join(bundleRoot, 'user-style');
        const entries = await readdir(root);
        const dirs = [];
        for (const entry of entries) {
            try {
                assertSafeChildName(entry);
            }
            catch {
                continue;
            }
            const info = await stat(join(root, entry));
            if (info.isDirectory())
                dirs.push(entry);
        }
        return dirs;
    }
    catch {
        return [];
    }
}
/** List relative paths to atom/knowledge markdown for a categorized-v2 pack: areas/<slug>/<kind>/*.md. */
async function listAreaFiles(from, kind) {
    const root = join(from, 'areas');
    const rel = [];
    let areaDirs;
    try {
        areaDirs = await readdir(root);
    }
    catch {
        return rel;
    }
    for (const area of areaDirs) {
        try {
            assertSafeChildName(area);
        }
        catch {
            continue;
        }
        const kindDir = join(root, area, kind);
        let files;
        try {
            files = await readdir(kindDir);
        }
        catch {
            continue;
        }
        for (const f of files) {
            if (!f.endsWith('.md'))
                continue;
            try {
                assertSafeChildName(f);
            }
            catch {
                continue;
            }
            rel.push(join('areas', area, kind, f));
        }
    }
    return rel.sort();
}
function atomsEquivalent(a, b) {
    return (a.claim === b.claim
        && a.type === b.type
        && JSON.stringify(a.evidence) === JSON.stringify(b.evidence)
        && JSON.stringify(a.trigger) === JSON.stringify(b.trigger));
}
