import { logger } from '../utils/logger.js';
import { mapWithConcurrency } from '../utils/concurrency.js';
import { isGeneratedKnowledgeSource } from './generated-projections.js';
import { isActiveKnowledgeEdge } from './projection-utils.js';
import { getKnowledgeSpaceId, isHomeAssistantKnowledgeSpace, normalizeKnowledgeSpaceId } from './spaces.js';
const DEFAULT_RELATED_GENERATED_PAGES = 24;
const GENERATED_PAGE_MARKDOWN_READ_CONCURRENCY = 8;
export async function listGeneratedKnowledgePages(input) {
    const scoped = scopeGeneratedPageRecords(input.spaceId, input.sources, input.nodes, input.edges);
    const graph = buildGeneratedKnowledgePageGraph(scoped.sources, scoped.nodes, scoped.edges, input.isGeneratedSource);
    const projectNode = input.projectNode ?? defaultPageGraphNode;
    const resolveSubject = input.resolvePageSubject ?? defaultResolvePageSubject;
    const isSubject = input.isPageSubjectNode ?? defaultIsPageSubjectNode;
    const relatedPageLimit = input.relatedPageLimit ?? DEFAULT_RELATED_GENERATED_PAGES;
    const sources = [...graph.generatedSources]
        .sort((left, right) => input.compareGeneratedPages?.(left, right, graph) ?? compareGeneratedPages(left, right, graph))
        .slice(0, input.limit);
    const pages = await mapWithConcurrency(sources, GENERATED_PAGE_MARKDOWN_READ_CONCURRENCY, async (source) => {
        const artifact = typeof source.artifactId === 'string' ? input.artifactStore.get(source.artifactId) : undefined;
        const markdown = input.includeMarkdown && artifact
            ? await readMarkdown(input.artifactStore, artifact.id)
            : undefined;
        const target = graph.targetBySourceId.get(source.id);
        const subject = target ? resolveSubject(target, graph) : undefined;
        return {
            source,
            ...(artifact ? {
                artifact: {
                    id: artifact.id,
                    mimeType: artifact.mimeType,
                    filename: artifact.filename,
                    createdAt: artifact.createdAt,
                    metadata: artifact.metadata,
                },
            } : {}),
            ...(markdown ? { markdown } : {}),
            ...(target ? { target: projectNode(target) } : {}),
            ...(subject ? { subject: projectNode(subject) } : {}),
            ...(subject ? { neighbors: pageNeighbors(subject, graph, isSubject, projectNode) } : {}),
            ...(subject ? { relatedPages: relatedPages(source.id, subject, graph, resolveSubject, isSubject, projectNode, relatedPageLimit) } : {}),
        };
    });
    return { ok: true, spaceId: input.spaceId, pages };
}
function scopeGeneratedPageRecords(spaceId, sources, nodes, edges) {
    const normalized = normalizeKnowledgeSpaceId(spaceId);
    const scopedSources = sources.filter((source) => recordBelongsToGeneratedPageSpace(source, normalized));
    const scopedNodes = nodes.filter((node) => recordBelongsToGeneratedPageSpace(node, normalized));
    const sourceIds = new Set(scopedSources.map((source) => source.id));
    const nodeIds = new Set(scopedNodes.map((node) => node.id));
    const scopedEdges = edges.filter((edge) => edgeReferencesGeneratedPageSpace(edge, sourceIds, nodeIds)
        && (recordBelongsToGeneratedPageSpace(edge, normalized) || edgeHasGeneratedPageSpaceEndpoint(edge, sourceIds, nodeIds)));
    return { sources: scopedSources, nodes: scopedNodes, edges: scopedEdges };
}
function recordBelongsToGeneratedPageSpace(record, normalizedSpaceId) {
    const recordSpaceId = normalizeKnowledgeSpaceId(getKnowledgeSpaceId(record));
    if (normalizedSpaceId === 'homeassistant')
        return isHomeAssistantKnowledgeSpace(recordSpaceId);
    return recordSpaceId === normalizedSpaceId;
}
function edgeReferencesGeneratedPageSpace(edge, sourceIds, nodeIds) {
    return (edge.fromKind !== 'source' || sourceIds.has(edge.fromId))
        && (edge.toKind !== 'source' || sourceIds.has(edge.toId))
        && (edge.fromKind !== 'node' || nodeIds.has(edge.fromId))
        && (edge.toKind !== 'node' || nodeIds.has(edge.toId));
}
function edgeHasGeneratedPageSpaceEndpoint(edge, sourceIds, nodeIds) {
    return (edge.fromKind === 'source' && sourceIds.has(edge.fromId))
        || (edge.toKind === 'source' && sourceIds.has(edge.toId))
        || (edge.fromKind === 'node' && nodeIds.has(edge.fromId))
        || (edge.toKind === 'node' && nodeIds.has(edge.toId));
}
export function buildGeneratedKnowledgePageGraph(sources, nodes, edges, isGeneratedSource = isGeneratedKnowledgeSource) {
    const nodesById = new Map(nodes.map((node) => [node.id, node]));
    const activeEdges = edges.filter(edgeIsActive);
    const targetBySourceId = new Map();
    const generatedSources = sources.filter((source) => source.status === 'indexed' && isGeneratedSource(source));
    for (const source of generatedSources) {
        const targetId = generatedPageTargetNodeId(source, activeEdges, nodesById);
        const target = targetId ? nodesById.get(targetId) : undefined;
        if (target && target.status !== 'stale')
            targetBySourceId.set(source.id, target);
    }
    return {
        nodesById,
        edges: activeEdges,
        generatedSources: generatedSources.filter((source) => {
            const targetId = generatedPageTargetNodeId(source, activeEdges, nodesById);
            if (!targetId)
                return true;
            const target = nodesById.get(targetId);
            return Boolean(target && target.status !== 'stale');
        }),
        targetBySourceId,
    };
}
function generatedPageTargetNodeId(source, edges, nodesById) {
    const metadataTargetId = typeof source.metadata.generatedTargetNodeId === 'string'
        ? source.metadata.generatedTargetNodeId
        : undefined;
    if (metadataTargetId)
        return metadataTargetId;
    const targetEdges = edges.filter((edge) => (edge.fromKind === 'source'
        && edge.fromId === source.id
        && edge.toKind === 'node'
        && isGeneratedPageTargetEdge(edge)));
    const activeTarget = targetEdges.find((edge) => {
        const target = nodesById.get(edge.toId);
        return Boolean(target && target.status !== 'stale');
    });
    return activeTarget?.toId ?? targetEdges[0]?.toId;
}
function isGeneratedPageTargetEdge(edge) {
    return edge.relation === 'source_for'
        || edge.metadata.generatedKnowledgePage === true
        || edge.metadata.generatedProjection === true;
}
function defaultResolvePageSubject(target, graph) {
    if (defaultIsPageSubjectNode(target))
        return target;
    const related = graph.edges.find((edge) => (edge.fromKind === 'node'
        && edge.fromId === target.id
        && edge.toKind === 'node'
        && edge.relation === 'source_for'));
    return related ? graph.nodesById.get(related.toId) : undefined;
}
function relatedPages(currentSourceId, subject, graph, resolveSubject, isSubject, projectNode, limit) {
    const neighborIds = new Set(pageNeighbors(subject, graph, isSubject, projectNode).map((neighbor) => neighbor.id));
    neighborIds.add(subject.id);
    const result = [];
    for (const source of graph.generatedSources) {
        if (source.id === currentSourceId)
            continue;
        const target = graph.targetBySourceId.get(source.id);
        if (!target)
            continue;
        const pageSubject = resolveSubject(target, graph);
        if (!pageSubject || !neighborIds.has(pageSubject.id))
            continue;
        result.push({
            sourceId: source.id,
            title: source.title ?? source.id,
            ...(typeof source.metadata.projectionKind === 'string' ? { projectionKind: source.metadata.projectionKind } : {}),
            subject: projectNode(pageSubject),
        });
    }
    return result.slice(0, limit);
}
function pageNeighbors(subject, graph, isSubject, projectNode) {
    const neighbors = [];
    const seen = new Set();
    for (const edge of graph.edges) {
        if (edge.fromKind === 'node' && edge.fromId === subject.id && edge.toKind === 'node') {
            const node = graph.nodesById.get(edge.toId);
            if (node && isSubject(node))
                pushNeighbor(neighbors, seen, projectNode(node), edge.relation, 'outgoing');
        }
        else if (edge.toKind === 'node' && edge.toId === subject.id && edge.fromKind === 'node') {
            const node = graph.nodesById.get(edge.fromId);
            if (node && isSubject(node))
                pushNeighbor(neighbors, seen, projectNode(node), edge.relation, 'incoming');
        }
    }
    return neighbors.slice(0, 48);
}
function pushNeighbor(neighbors, seen, node, relation, direction) {
    const key = `${node.id}:${relation}:${direction}`;
    if (seen.has(key))
        return;
    seen.add(key);
    neighbors.push({ ...node, relation, direction });
}
function defaultIsPageSubjectNode(node) {
    if (node.status === 'stale')
        return false;
    if (node.metadata.semanticKind)
        return false;
    return !['fact', 'knowledge_gap', 'wiki_page'].includes(node.kind);
}
function defaultPageGraphNode(node) {
    return {
        id: node.id,
        kind: node.kind,
        title: node.title,
    };
}
function compareGeneratedPages(left, right, graph) {
    const leftKind = typeof left.metadata.projectionKind === 'string' ? left.metadata.projectionKind : '';
    const rightKind = typeof right.metadata.projectionKind === 'string' ? right.metadata.projectionKind : '';
    return generatedPagePriority(right, graph) - generatedPagePriority(left, graph)
        || leftKind.localeCompare(rightKind)
        || (left.title ?? left.id).localeCompare(right.title ?? right.id)
        || left.id.localeCompare(right.id);
}
function generatedPagePriority(source, graph) {
    const target = graph.targetBySourceId.get(source.id);
    const subject = target ? defaultResolvePageSubject(target, graph) : undefined;
    const projectionKind = typeof source.metadata.projectionKind === 'string' ? source.metadata.projectionKind : '';
    let score = 0;
    if (projectionKind)
        score += 10;
    if (subject)
        score += 10;
    return score;
}
async function readMarkdown(artifactStore, artifactId) {
    try {
        const { buffer } = await artifactStore.readContent(artifactId);
        return buffer.toString('utf-8');
    }
    catch (error) {
        logger.warn('Generated knowledge page markdown read failed', {
            artifactId,
            error: error instanceof Error ? error.message : String(error),
        });
        return undefined;
    }
}
function edgeIsActive(edge) {
    return isActiveKnowledgeEdge(edge);
}
