import { randomUUID } from 'node:crypto';
import { SQLiteStore } from '../state/sqlite-store.js';
import { summarizeError } from '../utils/error-display.js';
import { logger } from '../utils/logger.js';
import { createSchema, issueStatusForUpsert, nowMs, stableText, uniq, } from './store-schema.js';
import { resolveKnowledgeDbPath } from './store-config.js';
import { upsertKnowledgeRefinementTask } from './store-refinement.js';
import { deleteKnowledgeSpaceRows, planKnowledgeSpaceDeleteRows, } from './store-space-delete.js';
import { deleteKnowledgeSchedule, upsertKnowledgeSchedule, } from './store-schedules.js';
import { edgesForKnowledgeStore, getKnowledgeConsolidationCandidate, getKnowledgeConsolidationCandidateBySubject, getKnowledgeConsolidationReport, getKnowledgeExtraction, getKnowledgeExtractionBySourceId, getKnowledgeIssue, getKnowledgeItem, getKnowledgeJobRun, getKnowledgeRefinementTask, getKnowledgeNode, getKnowledgeNodeByKindAndSlug, getKnowledgeSchedule, getKnowledgeSource, getKnowledgeSourceByCanonicalUri, getKnowledgeStoreStatus, getKnowledgeUsageRecord, listKnowledgeConsolidationCandidates, listKnowledgeConsolidationReports, listKnowledgeEdges, listKnowledgeExtractions, listKnowledgeExtractionsForSources, listKnowledgeExtractionsInSpace, listKnowledgeIssues, listKnowledgeIssuesInSpace, listKnowledgeJobRuns, listKnowledgeRefinementTasks, listKnowledgeNodes, listKnowledgeNodesInSpace, listKnowledgeSchedules, listKnowledgeSources, listKnowledgeSourcesInSpace, listKnowledgeUsageRecords, } from './store-read.js';
import { loadKnowledgeStoreSnapshot } from './store-load.js';
import { DEFAULT_KNOWLEDGE_SPACE_ID, ensureKnowledgeSpaceMetadata, getExplicitKnowledgeSpaceId, getKnowledgeSpaceId, } from './spaces.js';
export class KnowledgeStore {
    sqlite;
    dbPath;
    ready = false;
    initPromise = null;
    sources = new Map();
    nodes = new Map();
    edges = new Map();
    issues = new Map();
    extractions = new Map();
    jobRuns = new Map();
    refinementTasks = new Map();
    usageRecords = new Map();
    consolidationCandidates = new Map();
    consolidationReports = new Map();
    schedules = new Map();
    constructor(config) {
        this.dbPath = resolveKnowledgeDbPath(config);
        this.sqlite = new SQLiteStore(this.dbPath);
        void this.init().catch((error) => {
            logger.error('[knowledge-store] initialization failed', {
                path: this.dbPath,
                error: summarizeError(error),
            });
        });
    }
    get isReady() {
        return this.ready;
    }
    get storagePath() {
        return this.dbPath;
    }
    asReadView() {
        return this;
    }
    async init() {
        if (this.ready)
            return;
        if (this.initPromise)
            return this.initPromise;
        this.initPromise = this.initialize();
        try {
            await this.initPromise;
        }
        finally {
            this.initPromise = null;
        }
    }
    status() {
        return getKnowledgeStoreStatus(this.asReadView());
    }
    async batch(operation) { await this.init(); return this.sqlite.batch(operation); }
    listSources(limit = 100) {
        return listKnowledgeSources(this.asReadView(), limit);
    }
    listSourcesInSpace(spaceId) {
        return listKnowledgeSourcesInSpace(this.asReadView(), spaceId);
    }
    listNodes(limit = 100) {
        return listKnowledgeNodes(this.asReadView(), limit);
    }
    listNodesInSpace(spaceId) {
        return listKnowledgeNodesInSpace(this.asReadView(), spaceId);
    }
    listEdges() {
        return listKnowledgeEdges(this.asReadView());
    }
    listIssues(limit = 100) {
        return listKnowledgeIssues(this.asReadView(), limit);
    }
    listIssuesInSpace(spaceId) {
        return listKnowledgeIssuesInSpace(this.asReadView(), spaceId);
    }
    listExtractions(limit = 100) {
        return listKnowledgeExtractions(this.asReadView(), limit);
    }
    listExtractionsInSpace(spaceId) {
        return listKnowledgeExtractionsInSpace(this.asReadView(), spaceId);
    }
    listExtractionsForSources(sourceIds) {
        return listKnowledgeExtractionsForSources(this.asReadView(), sourceIds);
    }
    listJobRuns(limit = 100, jobId) {
        return listKnowledgeJobRuns(this.asReadView(), limit, jobId);
    }
    listRefinementTasks(limit = 100, input = {}) {
        return listKnowledgeRefinementTasks(this.asReadView(), limit, input);
    }
    listUsageRecords(limit = 100, input = {}) {
        return listKnowledgeUsageRecords(this.asReadView(), limit, input);
    }
    listConsolidationCandidates(limit = 100, input = {}) {
        return listKnowledgeConsolidationCandidates(this.asReadView(), limit, input);
    }
    listConsolidationReports(limit = 100) {
        return listKnowledgeConsolidationReports(this.asReadView(), limit);
    }
    listSchedules(limit = 100) {
        return listKnowledgeSchedules(this.asReadView(), limit);
    }
    getSource(id) {
        return getKnowledgeSource(this.asReadView(), id);
    }
    getNode(id) {
        return getKnowledgeNode(this.asReadView(), id);
    }
    getIssue(id) {
        return getKnowledgeIssue(this.asReadView(), id);
    }
    getExtraction(id) {
        return getKnowledgeExtraction(this.asReadView(), id);
    }
    getExtractionBySourceId(sourceId) {
        return getKnowledgeExtractionBySourceId(this.asReadView(), sourceId);
    }
    getJobRun(id) {
        return getKnowledgeJobRun(this.asReadView(), id);
    }
    getRefinementTask(id) {
        return getKnowledgeRefinementTask(this.asReadView(), id);
    }
    getUsageRecord(id) {
        return getKnowledgeUsageRecord(this.asReadView(), id);
    }
    getConsolidationCandidate(id) {
        return getKnowledgeConsolidationCandidate(this.asReadView(), id);
    }
    getConsolidationCandidateBySubject(subjectKind, subjectId, candidateType) {
        return getKnowledgeConsolidationCandidateBySubject(this.asReadView(), subjectKind, subjectId, candidateType);
    }
    getConsolidationReport(id) {
        return getKnowledgeConsolidationReport(this.asReadView(), id);
    }
    getSchedule(id) {
        return getKnowledgeSchedule(this.asReadView(), id);
    }
    getSourceByCanonicalUri(canonicalUri) {
        return getKnowledgeSourceByCanonicalUri(this.asReadView(), canonicalUri);
    }
    getNodeByKindAndSlug(kind, slug) {
        return getKnowledgeNodeByKindAndSlug(this.asReadView(), kind, slug);
    }
    edgesFor(kind, id) {
        return edgesForKnowledgeStore(this.asReadView(), kind, id);
    }
    getItem(id) {
        return getKnowledgeItem(this.asReadView(), id);
    }
    async upsertSource(input) {
        await this.init();
        const existing = input.id
            ? this.sources.get(input.id)
            : input.canonicalUri
                ? this.getSourceByCanonicalUri(input.canonicalUri)
                : null;
        const now = nowMs();
        // n1: `opt` is a local helper that collapses the 18 conditional spread expressions
        // in the record below. Returns `{ [key]: newVal }` when newVal is non-null,
        // falls back to `{ [key]: existingVal }` to preserve existing value on partial
        // update, or `{}` when neither is present.
        function opt(key, newVal, existingVal) {
            if (newVal !== null)
                return { [key]: newVal };
            if (existingVal !== undefined)
                return { [key]: existingVal };
            return {};
        }
        const _title = stableText(input.title);
        const _sourceUri = stableText(input.sourceUri);
        const _canonicalUri = stableText(input.canonicalUri);
        const _summary = stableText(input.summary);
        const _description = stableText(input.description);
        const _folderPath = stableText(input.folderPath);
        const _artifactId = stableText(input.artifactId);
        const _contentHash = stableText(input.contentHash);
        const _crawlError = stableText(input.crawlError);
        const _sessionId = stableText(input.sessionId);
        const sourceMetadata = ensureKnowledgeSpaceMetadata({
            ...(existing?.metadata ?? {}),
            ...(input.metadata ?? {}),
        });
        const record = {
            id: existing?.id ?? input.id ?? `source-${randomUUID().slice(0, 8)}`,
            connectorId: input.connectorId,
            sourceType: input.sourceType,
            ...opt('title', _title),
            ...opt('sourceUri', _sourceUri),
            ...opt('canonicalUri', _canonicalUri),
            ...opt('summary', _summary),
            ...opt('description', _description),
            tags: uniq(input.tags ?? existing?.tags),
            ...opt('folderPath', _folderPath, existing?.folderPath),
            status: input.status,
            ...opt('artifactId', _artifactId, existing?.artifactId),
            ...opt('contentHash', _contentHash, existing?.contentHash),
            ...(typeof input.lastCrawledAt === 'number' ? { lastCrawledAt: input.lastCrawledAt } : existing?.lastCrawledAt ? { lastCrawledAt: existing.lastCrawledAt } : {}),
            ...opt('crawlError', _crawlError, existing?.crawlError && input.status !== 'indexed' ? existing.crawlError : undefined),
            ...opt('sessionId', _sessionId, existing?.sessionId),
            metadata: sourceMetadata,
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_sources (
        id, connector_id, source_type, title, source_uri, canonical_uri, summary, description,
        tags, folder_path, status, artifact_id, content_hash, last_crawled_at, crawl_error,
        session_id, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.connectorId,
            record.sourceType,
            record.title ?? null,
            record.sourceUri ?? null,
            record.canonicalUri ?? null,
            record.summary ?? null,
            record.description ?? null,
            JSON.stringify([...record.tags]),
            record.folderPath ?? null,
            record.status,
            record.artifactId ?? null,
            record.contentHash ?? null,
            record.lastCrawledAt ?? null,
            record.crawlError ?? null,
            record.sessionId ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.sources.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async deleteSource(id) {
        await this.init();
        if (!this.sources.has(id))
            return false;
        for (const [edgeId, edge] of [...this.edges.entries()]) {
            if ((edge.fromKind === 'source' && edge.fromId === id) || (edge.toKind === 'source' && edge.toId === id)) {
                this.sqlite.run('DELETE FROM knowledge_edges WHERE id = ?', [edgeId]);
                this.edges.delete(edgeId);
            }
        }
        for (const [extractionId, extraction] of [...this.extractions.entries()]) {
            if (extraction.sourceId === id) {
                this.sqlite.run('DELETE FROM knowledge_extractions WHERE id = ?', [extractionId]);
                this.extractions.delete(extractionId);
            }
        }
        for (const [issueId, issue] of [...this.issues.entries()]) {
            if (issue.sourceId === id) {
                this.sqlite.run('DELETE FROM knowledge_issues WHERE id = ?', [issueId]);
                this.issues.delete(issueId);
            }
        }
        for (const [usageId, usage] of [...this.usageRecords.entries()]) {
            if (usage.targetKind === 'source' && usage.targetId === id) {
                this.sqlite.run('DELETE FROM knowledge_usage_records WHERE id = ?', [usageId]);
                this.usageRecords.delete(usageId);
            }
        }
        for (const [candidateId, candidate] of [...this.consolidationCandidates.entries()]) {
            if (candidate.subjectKind === 'source' && candidate.subjectId === id) {
                this.sqlite.run('DELETE FROM knowledge_consolidation_candidates WHERE id = ?', [candidateId]);
                this.consolidationCandidates.delete(candidateId);
            }
        }
        this.sqlite.run('DELETE FROM knowledge_sources WHERE id = ?', [id]);
        this.sources.delete(id);
        await this.sqlite.save();
        return true;
    }
    async replaceSourceRecord(record) {
        await this.init();
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_sources (
        id, connector_id, source_type, title, source_uri, canonical_uri, summary, description,
        tags, folder_path, status, artifact_id, content_hash, last_crawled_at, crawl_error,
        session_id, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.connectorId,
            record.sourceType,
            record.title ?? null,
            record.sourceUri ?? null,
            record.canonicalUri ?? null,
            record.summary ?? null,
            record.description ?? null,
            JSON.stringify([...record.tags]),
            record.folderPath ?? null,
            record.status,
            record.artifactId ?? null,
            record.contentHash ?? null,
            record.lastCrawledAt ?? null,
            record.crawlError ?? null,
            record.sessionId ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.sources.set(record.id, record);
        await this.sqlite.save();
    }
    async upsertNode(input) {
        await this.init();
        const existing = input.id
            ? this.nodes.get(input.id)
            : this.getNodeByKindAndSlug(input.kind, input.slug);
        const now = nowMs();
        const _summary = stableText(input.summary);
        const _sourceId = stableText(input.sourceId);
        const mergedNodeMetadata = {
            ...(existing?.metadata ?? {}),
            ...(input.metadata ?? {}),
        };
        const explicitNodeSpaceId = getExplicitKnowledgeSpaceId({ metadata: mergedNodeMetadata });
        const relatedNodeSpaceId = inferRecordReferenceSpaceId({
            sourceId: _sourceId ?? existing?.sourceId,
            metadata: mergedNodeMetadata,
            sources: this.sources,
            nodes: this.nodes,
        });
        const nodeSpaceId = preferRelatedNonDefaultSpace(explicitNodeSpaceId, relatedNodeSpaceId);
        const nodeMetadata = nodeSpaceId
            ? ensureKnowledgeSpaceMetadata(mergedNodeMetadata, nodeSpaceId)
            : mergedNodeMetadata;
        const record = {
            id: existing?.id ?? input.id ?? `node-${randomUUID().slice(0, 8)}`,
            kind: input.kind,
            slug: input.slug,
            title: input.title.trim(),
            ...(_summary !== null ? { summary: _summary } : existing?.summary ? { summary: existing.summary } : {}),
            aliases: uniq(input.aliases ?? existing?.aliases),
            status: input.status ?? existing?.status ?? 'active',
            confidence: Math.max(0, Math.min(100, input.confidence ?? existing?.confidence ?? 70)),
            ...(_sourceId !== null ? { sourceId: _sourceId } : existing?.sourceId ? { sourceId: existing.sourceId } : {}),
            metadata: nodeMetadata,
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_nodes (
        id, kind, slug, title, summary, aliases, status, confidence, source_id, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.kind,
            record.slug,
            record.title,
            record.summary ?? null,
            JSON.stringify([...record.aliases]),
            record.status,
            record.confidence,
            record.sourceId ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.nodes.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async deleteNode(id) {
        await this.init();
        if (!this.nodes.has(id))
            return false;
        for (const [edgeId, edge] of [...this.edges.entries()]) {
            if ((edge.fromKind === 'node' && edge.fromId === id) || (edge.toKind === 'node' && edge.toId === id)) {
                this.sqlite.run('DELETE FROM knowledge_edges WHERE id = ?', [edgeId]);
                this.edges.delete(edgeId);
            }
        }
        for (const [issueId, issue] of [...this.issues.entries()]) {
            if (issue.nodeId === id) {
                this.sqlite.run('DELETE FROM knowledge_issues WHERE id = ?', [issueId]);
                this.issues.delete(issueId);
            }
        }
        for (const [usageId, usage] of [...this.usageRecords.entries()]) {
            if (usage.targetKind === 'node' && usage.targetId === id) {
                this.sqlite.run('DELETE FROM knowledge_usage_records WHERE id = ?', [usageId]);
                this.usageRecords.delete(usageId);
            }
        }
        for (const [candidateId, candidate] of [...this.consolidationCandidates.entries()]) {
            if (candidate.subjectKind === 'node' && candidate.subjectId === id) {
                this.sqlite.run('DELETE FROM knowledge_consolidation_candidates WHERE id = ?', [candidateId]);
                this.consolidationCandidates.delete(candidateId);
            }
        }
        this.sqlite.run('DELETE FROM knowledge_nodes WHERE id = ?', [id]);
        this.nodes.delete(id);
        await this.sqlite.save();
        return true;
    }
    async replaceNodeRecord(record) {
        await this.init();
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_nodes (
        id, kind, slug, title, summary, aliases, status, confidence, source_id, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.kind,
            record.slug,
            record.title,
            record.summary ?? null,
            JSON.stringify([...record.aliases]),
            record.status,
            record.confidence,
            record.sourceId ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.nodes.set(record.id, record);
        await this.sqlite.save();
    }
    async upsertEdge(input) {
        await this.init();
        const existing = [...this.edges.values()].find((edge) => (edge.fromKind === input.fromKind
            && edge.fromId === input.fromId
            && edge.toKind === input.toKind
            && edge.toId === input.toId
            && edge.relation === input.relation));
        const now = nowMs();
        const record = {
            id: existing?.id ?? `edge-${randomUUID().slice(0, 8)}`,
            fromKind: input.fromKind,
            fromId: input.fromId,
            toKind: input.toKind,
            toId: input.toId,
            relation: input.relation,
            weight: Number.isFinite(input.weight) ? Number(input.weight) : existing?.weight ?? 1,
            metadata: {
                ...(existing?.metadata ?? {}),
                ...(input.metadata ?? {}),
            },
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_edges (
        id, from_kind, from_id, to_kind, to_id, relation, weight, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.fromKind,
            record.fromId,
            record.toKind,
            record.toId,
            record.relation,
            record.weight,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.edges.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async deleteEdge(id) {
        await this.init();
        if (!this.edges.has(id))
            return false;
        this.sqlite.run('DELETE FROM knowledge_edges WHERE id = ?', [id]);
        this.edges.delete(id);
        await this.sqlite.save();
        return true;
    }
    async replaceEdgeRecord(record) {
        await this.init();
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_edges (
        id, from_kind, from_id, to_kind, to_id, relation, weight, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.fromKind,
            record.fromId,
            record.toKind,
            record.toId,
            record.relation,
            record.weight,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.edges.set(record.id, record);
        await this.sqlite.save();
    }
    async replaceIssues(inputs, namespace) {
        await this.init();
        const created = [];
        const activeIds = new Set();
        for (const input of inputs) {
            const record = await this.upsertIssue(input);
            activeIds.add(record.id);
            created.push(record);
        }
        if (namespace) {
            for (const issue of [...this.issues.values()]) {
                if (issue.metadata.namespace !== namespace)
                    continue;
                if (activeIds.has(issue.id) || issue.status === 'resolved')
                    continue;
                this.sqlite.run('DELETE FROM knowledge_issues WHERE id = ?', [issue.id]);
                this.issues.delete(issue.id);
            }
        }
        await this.sqlite.save();
        return created;
    }
    async upsertIssue(input) {
        await this.init();
        const existing = input.id ? this.issues.get(input.id) : null;
        const now = nowMs();
        const _sourceId = stableText(input.sourceId);
        const _nodeId = stableText(input.nodeId);
        const mergedIssueMetadata = {
            ...(existing?.metadata ?? {}),
            ...(input.metadata ?? {}),
        };
        const issueSource = _sourceId !== null
            ? this.sources.get(_sourceId)
            : existing?.sourceId
                ? this.sources.get(existing.sourceId)
                : null;
        const issueNode = _nodeId !== null
            ? this.nodes.get(_nodeId)
            : existing?.nodeId
                ? this.nodes.get(existing.nodeId)
                : null;
        const issueSpaceId = preferRelatedNonDefaultSpace(getExplicitKnowledgeSpaceId({ metadata: mergedIssueMetadata }), inferRecordReferenceSpaceId({
            sourceId: _sourceId ?? existing?.sourceId,
            nodeId: _nodeId ?? existing?.nodeId,
            metadata: mergedIssueMetadata,
            sources: this.sources,
            nodes: this.nodes,
        }) ?? getExplicitKnowledgeSpaceId(issueSource) ?? getExplicitKnowledgeSpaceId(issueNode));
        const issueMetadata = issueSpaceId
            ? ensureKnowledgeSpaceMetadata(mergedIssueMetadata, issueSpaceId)
            : mergedIssueMetadata;
        const record = {
            id: existing?.id ?? input.id ?? `issue-${randomUUID().slice(0, 8)}`,
            severity: input.severity,
            code: input.code,
            message: input.message.trim(),
            status: input.status ?? issueStatusForUpsert(existing, input),
            ...(_sourceId !== null ? { sourceId: _sourceId } : {}),
            ...(_nodeId !== null ? { nodeId: _nodeId } : {}),
            metadata: issueMetadata,
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_issues (
        id, severity, code, message, status, source_id, node_id, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.severity,
            record.code,
            record.message,
            record.status,
            record.sourceId ?? null,
            record.nodeId ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.issues.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async replaceIssueRecord(record) {
        await this.init();
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_issues (
        id, severity, code, message, status, source_id, node_id, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.severity,
            record.code,
            record.message,
            record.status,
            record.sourceId ?? null,
            record.nodeId ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.issues.set(record.id, record);
        await this.sqlite.save();
    }
    async upsertExtraction(input) {
        await this.init();
        const existing = input.id
            ? this.extractions.get(input.id)
            : this.getExtractionBySourceId(input.sourceId);
        const now = nowMs();
        const _artifactId = stableText(input.artifactId);
        const _title = stableText(input.title);
        const _summary = stableText(input.summary);
        const _excerpt = stableText(input.excerpt);
        const mergedExtractionMetadata = {
            ...(existing?.metadata ?? {}),
            ...(input.metadata ?? {}),
        };
        const extractionSource = this.sources.get(input.sourceId);
        const extractionMetadata = getExplicitKnowledgeSpaceId({ metadata: mergedExtractionMetadata }) || !extractionSource
            ? mergedExtractionMetadata
            : ensureKnowledgeSpaceMetadata(mergedExtractionMetadata, getKnowledgeSpaceId(extractionSource));
        const record = {
            id: existing?.id ?? input.id ?? `extract-${randomUUID().slice(0, 8)}`,
            sourceId: input.sourceId,
            ...(_artifactId !== null ? { artifactId: _artifactId } : existing?.artifactId ? { artifactId: existing.artifactId } : {}),
            extractorId: input.extractorId,
            format: input.format,
            ...(_title !== null ? { title: _title } : existing?.title ? { title: existing.title } : {}),
            ...(_summary !== null ? { summary: _summary } : existing?.summary ? { summary: existing.summary } : {}),
            ...(_excerpt !== null ? { excerpt: _excerpt } : existing?.excerpt ? { excerpt: existing.excerpt } : {}),
            sections: uniq(input.sections ?? existing?.sections),
            links: uniq(input.links ?? existing?.links),
            estimatedTokens: Math.max(0, Number(input.estimatedTokens ?? existing?.estimatedTokens ?? 0)),
            structure: {
                ...(existing?.structure ?? {}),
                ...(input.structure ?? {}),
            },
            metadata: extractionMetadata,
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_extractions (
        id, source_id, artifact_id, extractor_id, format, title, summary, excerpt,
        sections, links, estimated_tokens, structure, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.sourceId,
            record.artifactId ?? null,
            record.extractorId,
            record.format,
            record.title ?? null,
            record.summary ?? null,
            record.excerpt ?? null,
            JSON.stringify([...record.sections]),
            JSON.stringify([...record.links]),
            record.estimatedTokens,
            JSON.stringify(record.structure),
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.extractions.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async upsertJobRun(input) {
        await this.init();
        const existing = input.id ? this.jobRuns.get(input.id) : null;
        const now = nowMs();
        const _error = stableText(input.error);
        const record = {
            id: existing?.id ?? input.id ?? `kjr-${randomUUID().slice(0, 8)}`,
            jobId: input.jobId,
            status: input.status,
            mode: input.mode,
            requestedAt: input.requestedAt ?? existing?.requestedAt ?? now,
            ...(typeof input.startedAt === 'number' ? { startedAt: input.startedAt } : existing?.startedAt ? { startedAt: existing.startedAt } : {}),
            ...(typeof input.completedAt === 'number' ? { completedAt: input.completedAt } : existing?.completedAt ? { completedAt: existing.completedAt } : {}),
            ...(_error !== null ? { error: _error } : existing?.error ? { error: existing.error } : {}),
            result: {
                ...(existing?.result ?? {}),
                ...(input.result ?? {}),
            },
            metadata: {
                ...(existing?.metadata ?? {}),
                ...(input.metadata ?? {}),
            },
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_job_runs (
        id, job_id, status, mode, requested_at, started_at, completed_at, error, result, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.jobId,
            record.status,
            record.mode,
            record.requestedAt,
            record.startedAt ?? null,
            record.completedAt ?? null,
            record.error ?? null,
            JSON.stringify(record.result),
            JSON.stringify(record.metadata),
            existing ? now : now,
            now,
        ]);
        this.jobRuns.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async upsertRefinementTask(input) {
        await this.init();
        return upsertKnowledgeRefinementTask(this.sqlite, this.refinementTasks, input, () => `kref-${randomUUID().slice(0, 8)}`);
    }
    async upsertUsageRecord(input) {
        await this.init();
        const _task = stableText(input.task);
        const _sessionId = stableText(input.sessionId);
        const record = {
            id: input.id ?? `kuse-${randomUUID().slice(0, 8)}`,
            targetKind: input.targetKind,
            targetId: input.targetId,
            usageKind: input.usageKind,
            ...(_task !== null ? { task: _task } : {}),
            ...(_sessionId !== null ? { sessionId: _sessionId } : {}),
            ...(typeof input.score === 'number' && Number.isFinite(input.score) ? { score: Number(input.score) } : {}),
            metadata: { ...(input.metadata ?? {}) },
            createdAt: nowMs(),
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_usage_records (
        id, target_kind, target_id, usage_kind, task, session_id, score, metadata, created_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.targetKind,
            record.targetId,
            record.usageKind,
            record.task ?? null,
            record.sessionId ?? null,
            record.score ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
        ]);
        this.usageRecords.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async upsertConsolidationCandidate(input) {
        await this.init();
        const existing = input.id
            ? this.consolidationCandidates.get(input.id)
            : this.getConsolidationCandidateBySubject(input.subjectKind, input.subjectId, input.candidateType);
        const now = nowMs();
        const _summary = stableText(input.summary);
        const _suggestedMemoryClass = stableText(input.suggestedMemoryClass);
        const _suggestedScope = stableText(input.suggestedScope);
        const _decidedBy = stableText(input.decidedBy);
        const record = {
            id: existing?.id ?? input.id ?? `kcand-${randomUUID().slice(0, 8)}`,
            candidateType: input.candidateType,
            status: input.status ?? existing?.status ?? 'open',
            subjectKind: input.subjectKind,
            subjectId: input.subjectId,
            title: input.title.trim(),
            ...(_summary !== null ? { summary: _summary } : existing?.summary ? { summary: existing.summary } : {}),
            score: Number(input.score),
            evidence: uniq(input.evidence ?? existing?.evidence),
            ...(_suggestedMemoryClass !== null ? { suggestedMemoryClass: _suggestedMemoryClass } : existing?.suggestedMemoryClass ? { suggestedMemoryClass: existing.suggestedMemoryClass } : {}),
            ...(_suggestedScope !== null ? { suggestedScope: _suggestedScope } : existing?.suggestedScope ? { suggestedScope: existing.suggestedScope } : {}),
            ...(typeof input.decidedAt === 'number' ? { decidedAt: input.decidedAt } : existing?.decidedAt ? { decidedAt: existing.decidedAt } : {}),
            ...(_decidedBy !== null ? { decidedBy: _decidedBy } : existing?.decidedBy ? { decidedBy: existing.decidedBy } : {}),
            metadata: {
                ...(existing?.metadata ?? {}),
                ...(input.metadata ?? {}),
            },
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_consolidation_candidates (
        id, candidate_type, status, subject_kind, subject_id, title, summary, score,
        evidence, suggested_memory_class, suggested_scope, decided_at, decided_by, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.candidateType,
            record.status,
            record.subjectKind,
            record.subjectId,
            record.title,
            record.summary ?? null,
            record.score,
            JSON.stringify([...record.evidence]),
            record.suggestedMemoryClass ?? null,
            record.suggestedScope ?? null,
            record.decidedAt ?? null,
            record.decidedBy ?? null,
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.consolidationCandidates.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async upsertConsolidationReport(input) {
        await this.init();
        const existing = input.id ? this.consolidationReports.get(input.id) : null;
        const now = nowMs();
        const record = {
            id: existing?.id ?? input.id ?? `krep-${randomUUID().slice(0, 8)}`,
            kind: input.kind,
            title: input.title.trim(),
            summary: input.summary.trim(),
            highlights: uniq(input.highlights ?? existing?.highlights),
            metrics: {
                ...(existing?.metrics ?? {}),
                ...(input.metrics ?? {}),
            },
            metadata: {
                ...(existing?.metadata ?? {}),
                ...(input.metadata ?? {}),
            },
            createdAt: existing?.createdAt ?? now,
            updatedAt: now,
        };
        this.sqlite.run(`
      INSERT OR REPLACE INTO knowledge_consolidation_reports (
        id, kind, title, summary, highlights, metrics, metadata, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
            record.id,
            record.kind,
            record.title,
            record.summary,
            JSON.stringify([...record.highlights]),
            JSON.stringify(record.metrics),
            JSON.stringify(record.metadata),
            record.createdAt,
            record.updatedAt,
        ]);
        this.consolidationReports.set(record.id, record);
        await this.sqlite.save();
        return record;
    }
    async upsertSchedule(input) {
        await this.init();
        return upsertKnowledgeSchedule(this.sqlite, this.schedules, input, () => `ksched-${randomUUID().slice(0, 8)}`);
    }
    async deleteSchedule(id) {
        await this.init();
        return deleteKnowledgeSchedule(this.sqlite, this.schedules, id);
    }
    async planKnowledgeSpaceDelete(spaceId) {
        await this.init();
        return planKnowledgeSpaceDeleteRows(this.knowledgeSpaceRowStores(), spaceId);
    }
    async deleteKnowledgeSpace(spaceId, input = {}) {
        await this.init();
        if (input.dryRun)
            return this.planKnowledgeSpaceDelete(spaceId);
        const deleted = deleteKnowledgeSpaceRows(this.sqlite, this.knowledgeSpaceRowStores(), spaceId);
        await this.sqlite.save();
        return deleted;
    }
    knowledgeSpaceRowStores() {
        return {
            sources: this.sources,
            nodes: this.nodes,
            edges: this.edges,
            issues: this.issues,
            extractions: this.extractions,
            jobRuns: this.jobRuns,
            refinementTasks: this.refinementTasks,
            usageRecords: this.usageRecords,
            consolidationCandidates: this.consolidationCandidates,
            consolidationReports: this.consolidationReports,
            schedules: this.schedules,
        };
    }
    async initialize() {
        await this.sqlite.init(createSchema);
        const snapshot = loadKnowledgeStoreSnapshot(this.sqlite);
        this.sources.clear();
        for (const record of snapshot.sources)
            this.sources.set(record.id, record);
        this.nodes.clear();
        for (const record of snapshot.nodes)
            this.nodes.set(record.id, record);
        this.edges.clear();
        for (const record of snapshot.edges)
            this.edges.set(record.id, record);
        this.issues.clear();
        for (const record of snapshot.issues)
            this.issues.set(record.id, record);
        this.extractions.clear();
        for (const record of snapshot.extractions)
            this.extractions.set(record.id, record);
        this.jobRuns.clear();
        for (const record of snapshot.jobRuns)
            this.jobRuns.set(record.id, record);
        this.refinementTasks.clear();
        for (const record of snapshot.refinementTasks)
            this.refinementTasks.set(record.id, record);
        this.usageRecords.clear();
        for (const record of snapshot.usageRecords)
            this.usageRecords.set(record.id, record);
        this.consolidationCandidates.clear();
        for (const record of snapshot.consolidationCandidates)
            this.consolidationCandidates.set(record.id, record);
        this.consolidationReports.clear();
        for (const record of snapshot.consolidationReports)
            this.consolidationReports.set(record.id, record);
        this.schedules.clear();
        for (const record of snapshot.schedules)
            this.schedules.set(record.id, record);
        this.ready = true;
    }
}
function inferRecordReferenceSpaceId(input) {
    const spaceIds = new Set();
    for (const sourceId of uniqueReferenceIds([
        input.sourceId ?? undefined,
        readMetadataString(input.metadata.sourceId),
        ...readMetadataStringArray(input.metadata.sourceIds),
    ])) {
        const spaceId = getExplicitKnowledgeSpaceId(input.sources.get(sourceId));
        if (spaceId)
            spaceIds.add(spaceId);
    }
    for (const nodeId of uniqueReferenceIds([
        input.nodeId ?? undefined,
        readMetadataString(input.metadata.nodeId),
        ...readMetadataStringArray(input.metadata.linkedObjectIds),
        ...readMetadataStringArray(input.metadata.subjectIds),
    ])) {
        const spaceId = getExplicitKnowledgeSpaceId(input.nodes.get(nodeId));
        if (spaceId)
            spaceIds.add(spaceId);
    }
    return [...spaceIds].find((spaceId) => spaceId !== DEFAULT_KNOWLEDGE_SPACE_ID) ?? [...spaceIds][0] ?? null;
}
function preferRelatedNonDefaultSpace(explicitSpaceId, relatedSpaceId) {
    if (relatedSpaceId && relatedSpaceId !== DEFAULT_KNOWLEDGE_SPACE_ID && explicitSpaceId === DEFAULT_KNOWLEDGE_SPACE_ID) {
        return relatedSpaceId;
    }
    return explicitSpaceId ?? relatedSpaceId;
}
function readMetadataString(value) {
    return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function readMetadataStringArray(value) {
    if (typeof value === 'string' && value.trim().length > 0)
        return [value.trim()];
    if (!Array.isArray(value))
        return [];
    return value.flatMap((entry) => readMetadataStringArray(entry));
}
function uniqueReferenceIds(values) {
    return [...new Set(values.filter((entry) => Boolean(entry && entry.trim().length > 0)))];
}
