import { createHash } from 'node:crypto';
import { logger } from '../utils/logger.js';
export function generatedKnowledgeSourceId(kind, value) {
    return `kg-gen-${stableHash(`${kind}:${value}`)}`;
}
export function generatedKnowledgeCanonicalUri(kind, value) {
    return `knowledge://generated/${encodeURIComponent(kind)}/${stableHash(value, 20)}`;
}
export function isGeneratedKnowledgeSource(source) {
    return source.metadata.generatedKnowledgePage === true
        || source.metadata.generatedProjection === true;
}
export async function materializeGeneratedKnowledgeProjection(input) {
    throwIfAborted(input.signal);
    const existing = input.store.getSource(input.sourceId);
    const contentHash = stableHash(input.markdown, 40);
    const existingMetadata = readRecord(existing?.metadata);
    const existingGeneratedAt = typeof existingMetadata.generatedAt === 'number' ? existingMetadata.generatedAt : undefined;
    const contentUnchanged = existingMetadata.generatedContentHash === contentHash;
    const generatedAt = contentUnchanged && existingGeneratedAt !== undefined ? existingGeneratedAt : Date.now();
    const projectionMetadata = {
        ...(input.metadata ?? {}),
        generatedKnowledgePage: true,
        generatedProjection: true,
        projectionKind: input.projectionKind,
        generatedAt,
        generatedContentHash: contentHash,
        pageEditable: true,
    };
    if (input.target) {
        assertGeneratedProjectionTarget(input.artifactStore, input.store, {
            toKind: input.target.kind,
            toId: input.target.id,
        });
    }
    throwIfAborted(input.signal);
    const reusedArtifact = await findReusableGeneratedArtifact(input.artifactStore, existing, input.markdown);
    throwIfAborted(input.signal);
    const artifact = reusedArtifact ?? await input.artifactStore.create({
        kind: 'document',
        mimeType: 'text/markdown',
        filename: input.filename,
        text: input.markdown,
        retentionMs: 0,
        metadata: {
            ...projectionMetadata,
            ...(input.artifactMetadata ?? {}),
            generatedAt,
            generatedContentHash: contentHash,
        },
    });
    const sourceInput = {
        id: input.sourceId,
        connectorId: input.connectorId,
        sourceType: input.sourceType ?? 'document',
        title: input.title,
        sourceUri: input.canonicalUri,
        canonicalUri: input.canonicalUri,
        ...(input.summary ? { summary: input.summary } : {}),
        tags: uniqueStrings(input.tags ?? []),
        status: 'indexed',
        artifactId: artifact.id,
        lastCrawledAt: contentUnchanged && typeof existing?.lastCrawledAt === 'number'
            ? existing.lastCrawledAt
            : generatedAt,
        metadata: {
            ...projectionMetadata,
            artifactId: artifact.id,
            filename: artifact.filename,
            ...(input.sourceMetadata ?? {}),
            generatedAt,
            generatedContentHash: contentHash,
        },
    };
    const edgeInput = input.target
        ? {
            fromKind: 'source',
            fromId: input.sourceId,
            toKind: input.target.kind,
            toId: input.target.id,
            relation: input.target.relation ?? 'source_for',
            metadata: {
                generatedKnowledgePage: true,
                generatedProjection: true,
                projectionKind: input.projectionKind,
                ...(input.edgeMetadata ?? {}),
            },
        }
        : undefined;
    const existingLinked = edgeInput
        ? input.store.edgesFor(edgeInput.fromKind, edgeInput.fromId).find((edge) => (edge.toKind === edgeInput.toKind
            && edge.toId === edgeInput.toId
            && edge.relation === edgeInput.relation))
        : undefined;
    let source;
    let linked;
    try {
        throwIfAborted(input.signal);
        source = await input.store.upsertSource(sourceInput);
        throwIfAborted(input.signal);
        linked = edgeInput ? await input.store.upsertEdge(edgeInput) : undefined;
        throwIfAborted(input.signal);
    }
    catch (error) {
        if (!reusedArtifact)
            input.artifactStore.delete(artifact.id);
        const currentLinked = edgeInput
            ? input.store.edgesFor(edgeInput.fromKind, edgeInput.fromId).find((edge) => (edge.toKind === edgeInput.toKind
                && edge.toId === edgeInput.toId
                && edge.relation === edgeInput.relation))
            : undefined;
        if (currentLinked && edgeInput) {
            if (existingLinked) {
                await input.store.replaceEdgeRecord(existingLinked);
            }
            else {
                await input.store.deleteEdge(currentLinked.id);
            }
        }
        const persistedSource = input.store.getSource(input.sourceId);
        if (!existing && persistedSource) {
            await input.store.deleteSource(persistedSource.id);
        }
        else if (existing && persistedSource) {
            await input.store.replaceSourceRecord(existing);
        }
        throw error;
    }
    if (!source)
        throw new Error(`Generated projection '${input.sourceId}' did not persist a source record.`);
    if (!reusedArtifact && existing?.artifactId && existing.artifactId !== artifact.id) {
        const existingArtifact = input.artifactStore.get(existing.artifactId);
        if (existingArtifact && readRecord(existingArtifact.metadata).generatedKnowledgePage === true) {
            input.artifactStore.delete(existing.artifactId);
        }
    }
    return {
        artifact,
        source,
        ...(linked ? { linked } : {}),
        artifactCreated: !reusedArtifact,
    };
}
function throwIfAborted(signal) {
    if (!signal?.aborted)
        return;
    throw new Error('Generated knowledge projection was cancelled.');
}
function assertGeneratedProjectionTarget(artifactStore, store, edge) {
    switch (edge.toKind) {
        case 'node':
            if (!store.getNode(edge.toId))
                throw new Error(`Generated projection target node does not exist: ${edge.toId}`);
            return;
        case 'source':
            if (!store.getSource(edge.toId))
                throw new Error(`Generated projection target source does not exist: ${edge.toId}`);
            return;
        case 'artifact':
            if (!artifactStore.get(edge.toId))
                throw new Error(`Generated projection target artifact does not exist: ${edge.toId}`);
            return;
        default:
            throw new Error(`Generated projection target kind is not supported: ${edge.toKind}`);
    }
}
async function findReusableGeneratedArtifact(artifactStore, source, markdown) {
    if (!source?.artifactId)
        return undefined;
    const artifact = artifactStore.get(source.artifactId);
    if (!artifact)
        return undefined;
    try {
        const { buffer } = await artifactStore.readContent(artifact.id);
        return buffer.toString('utf-8') === markdown ? artifact : undefined;
    }
    catch (error) {
        logger.warn('Generated knowledge projection artifact reuse failed', {
            sourceId: source.id,
            artifactId: artifact.id,
            error: error instanceof Error ? error.message : String(error),
        });
        return undefined;
    }
}
function stableHash(value, length = 24) {
    return createHash('sha256').update(value).digest('hex').slice(0, length);
}
function readRecord(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value)
        ? value
        : {};
}
function uniqueStrings(values) {
    const seen = new Set();
    const result = [];
    for (const value of values) {
        const trimmed = typeof value === 'string' ? value.trim() : '';
        if (!trimmed || seen.has(trimmed))
            continue;
        seen.add(trimmed);
        result.push(trimmed);
    }
    return result;
}
