import { normalizeLabel, uniqueStrings } from '../util/text.js';
import { extractAstSymbols, pickAstSourceFromReferences, relationsFromAst } from './ast-extractor.js';
import { getRetrievalPolicy } from '../retrieval/policy.js';
export class KnowledgeRelationInference {
    infer(item) {
        const seeds = [
            ...this.fromLabels(item),
            ...this.fromReferences(item.references),
            ...this.fromMetadata(item.metadata),
            ...this.fromSourceUri(item.sourceUri),
        ];
        const deduped = dedupeRelationSeeds(seeds);
        // Phase 6c — stamp every inferred relation with metadata.validFrom so
        // graph-expansion can later filter expired edges via validUntil. validFrom
        // anchors at inference time (effectively the createdAt timestamp the store
        // will write). validUntil is left unset; it is filled in by createKnowledgeRelation
        // when a supersedes relation is created or by recordFeedback for stale signals.
        const inferredAt = new Date().toISOString();
        const baseRelations = deduped.map((seed) => ({
            project: item.project,
            fromKnowledgeId: item.id,
            relationType: seed.relationType,
            targetKind: seed.targetKind,
            targetValue: seed.targetValue,
            confidence: seed.confidence,
            inferred: true,
            metadata: { validFrom: inferredAt, ...(seed.metadata ?? {}) },
        }));
        const astRelations = this.fromAst(item);
        if (astRelations.length === 0) {
            return baseRelations;
        }
        return mergeRelations(baseRelations, astRelations).map((relation) => ({
            ...relation,
            metadata: ensureValidFromMetadata(relation.metadata, inferredAt),
        }));
    }
    fromAst(item) {
        let useAst = true;
        try {
            useAst = getRetrievalPolicy().useAstExtractor;
        }
        catch {
            useAst = true;
        }
        if (!useAst)
            return [];
        const filename = pickAstSourceFromReferences(item.references) ?? item.sourceUri;
        if (!filename)
            return [];
        const result = extractAstSymbols(item.content, { filename });
        return relationsFromAst(item, result);
    }
    fromLabels(item) {
        const seeds = [];
        for (const label of item.labels) {
            if (label.type === 'file') {
                seeds.push({
                    relationType: 'mentions_file',
                    targetKind: 'file',
                    targetValue: label.value,
                    confidence: label.weight ?? 0.9,
                    metadata: { source: 'label:file' },
                });
            }
            if (label.type === 'symbol') {
                seeds.push({
                    relationType: 'mentions_symbol',
                    targetKind: 'symbol',
                    targetValue: label.value,
                    confidence: label.weight ?? 0.85,
                    metadata: { source: 'label:symbol' },
                });
            }
            if (label.type === 'error') {
                seeds.push({
                    relationType: 'resolves_error',
                    targetKind: 'error',
                    targetValue: label.value,
                    confidence: label.weight ?? 0.85,
                    metadata: { source: 'label:error' },
                });
            }
        }
        return seeds;
    }
    fromReferences(references) {
        return references.flatMap((reference) => {
            const base = {
                relationType: reference.type === 'conversation' ? 'derived_from_session' : 'references',
                targetKind: reference.type === 'conversation' ? 'session' : referenceTargetKind(reference),
                targetValue: reference.uri,
                confidence: 0.8,
                metadata: {
                    source: `reference:${reference.type}`,
                    lineStart: reference.lineStart,
                    lineEnd: reference.lineEnd,
                },
            };
            if (reference.type !== 'file') {
                return [base];
            }
            return [
                base,
                {
                    relationType: 'mentions_file',
                    targetKind: 'file',
                    targetValue: reference.uri,
                    confidence: 0.9,
                    metadata: {
                        source: 'reference:file',
                        lineStart: reference.lineStart,
                        lineEnd: reference.lineEnd,
                    },
                },
            ];
        });
    }
    fromMetadata(metadata) {
        const seeds = [];
        const sourcePath = typeof metadata.sourcePath === 'string' ? metadata.sourcePath : undefined;
        if (sourcePath) {
            seeds.push({
                relationType: 'contains',
                targetKind: 'file',
                targetValue: sourcePath,
                confidence: 0.95,
                metadata: { source: 'metadata:sourcePath' },
            });
        }
        const sectionPath = Array.isArray(metadata.sectionPath)
            ? metadata.sectionPath.filter((section) => typeof section === 'string')
            : [];
        sectionPath.forEach((section, index) => {
            seeds.push({
                relationType: 'contains',
                targetKind: 'reference',
                targetValue: sectionPath.slice(0, index + 1).join(' > '),
                confidence: 0.7,
                metadata: { source: 'metadata:sectionPath' },
            });
        });
        const agentSessionId = typeof metadata.agentSessionId === 'string' ? metadata.agentSessionId : undefined;
        if (agentSessionId) {
            seeds.push({
                relationType: 'derived_from_session',
                targetKind: 'session',
                targetValue: agentSessionId,
                confidence: 0.9,
                metadata: { source: 'metadata:agentSessionId' },
            });
        }
        return seeds;
    }
    fromSourceUri(sourceUri) {
        if (!sourceUri) {
            return [];
        }
        return [{
                relationType: 'references',
                targetKind: sourceUri.includes('://') ? 'reference' : 'file',
                targetValue: sourceUri,
                confidence: 0.75,
                metadata: { source: 'sourceUri' },
            }];
    }
}
function referenceTargetKind(reference) {
    if (reference.type === 'file') {
        return 'file';
    }
    return 'reference';
}
function mergeRelations(base, additional) {
    const byKey = new Map();
    for (const relation of [...base, ...additional]) {
        const key = `${relation.relationType}:${relation.targetKind}:${normalizeLabel(relation.targetValue ?? '')}`;
        const existing = byKey.get(key);
        if (!existing || (relation.confidence ?? 0) > (existing.confidence ?? 0)) {
            byKey.set(key, relation);
        }
    }
    return [...byKey.values()];
}
function ensureValidFromMetadata(metadata, inferredAt) {
    if (metadata && typeof metadata.validFrom === 'string') {
        return metadata;
    }
    return { validFrom: inferredAt, ...(metadata ?? {}) };
}
function dedupeRelationSeeds(seeds) {
    const byKey = new Map();
    for (const seed of seeds) {
        const key = [
            seed.relationType,
            seed.targetKind,
            normalizeLabel(seed.targetValue),
        ].join(':');
        const existing = byKey.get(key);
        if (!existing || seed.confidence > existing.confidence) {
            byKey.set(key, {
                ...seed,
                targetValue: seed.targetValue.trim(),
            });
        }
    }
    return uniqueStrings([...byKey.keys()]).map((key) => byKey.get(key)).filter((seed) => Boolean(seed));
}
