import { logger } from '../../utils/logger.js';
import { scheduleBackground, sleep, yieldEvery, yieldToEventLoop } from '../cooperative.js';
import { getKnowledgeSpaceId, isHomeAssistantKnowledgeSpace, normalizeKnowledgeSpaceId } from '../spaces.js';
import { answerKnowledgeQuery } from './answer.js';
import { enrichKnowledgeSource } from './enrichment.js';
import { readRecord, readString, readStringArray, sourceSemanticHash, sourceSemanticText } from './utils.js';
import { uniqueStrings } from './utils.js';
import { runKnowledgeSemanticSelfImprovement } from './self-improvement.js';
import { isGeneratedKnowledgeSource } from '../generated-projections.js';
export class KnowledgeSemanticService {
    store;
    options;
    activeGapRepairs = new Set();
    activeSelfImprovementRuns = new Map();
    constructor(store, options = {}) {
        this.store = store;
        this.options = options;
    }
    setGapRepairer(gapRepairer) {
        this.options = {
            ...this.options,
            gapRepairer: gapRepairer ?? null,
        };
    }
    addObjectProfiles(profiles) {
        if (!profiles?.length)
            return;
        const byId = new Map((this.options.objectProfiles ?? []).map((profile) => [profile.id, profile]));
        for (const profile of profiles)
            byId.set(profile.id, profile);
        this.options = {
            ...this.options,
            objectProfiles: [...byId.values()],
        };
    }
    async enrichSource(sourceId, input = {}) {
        await this.store.init();
        const source = this.store.getSource(sourceId);
        if (!source)
            return null;
        return enrichKnowledgeSource({ store: this.store, llm: this.options.llm }, source, input);
    }
    async enrichSources(sources, input = {}) {
        const results = [];
        const selected = sources.slice(0, Math.max(1, input.limit ?? sources.length));
        for (const [index, source] of selected.entries()) {
            await yieldEvery(index);
            const result = await this.enrichSource(source.id, input);
            if (result)
                results.push(result);
            await yieldToEventLoop();
        }
        return results;
    }
    async reindex(input = {}) {
        await this.store.init();
        const allowed = input.sourceIds?.length ? new Set(input.sourceIds) : null;
        const sources = listSemanticSources(this.store, input.knowledgeSpaceId)
            .filter((source) => !allowed || allowed.has(source.id))
            .slice(0, Math.max(1, input.limit ?? 10_000));
        const maxLlmSources = Math.max(0, this.options.maxLlmSourcesPerReindex ?? 3);
        const maxRunMs = Math.max(100, input.maxRunMs ?? this.options.maxReindexRunMs ?? 45_000);
        const startedAt = Date.now();
        let llmAttempts = 0;
        let enriched = 0;
        let skipped = 0;
        let failed = 0;
        let processed = 0;
        const errors = [];
        for (const [index, source] of sources.entries()) {
            await yieldEvery(index);
            if (Date.now() - startedAt >= maxRunMs) {
                skipped += sources.length - processed;
                break;
            }
            processed += 1;
            try {
                const llm = this.options.llm && llmAttempts < maxLlmSources && sourceCanUseLlmUpgrade(this.store, source)
                    ? this.options.llm
                    : null;
                if (llm)
                    llmAttempts += 1;
                const result = await enrichKnowledgeSource({ store: this.store, llm }, source, input);
                if (result?.skipped)
                    skipped += 1;
                else if (result)
                    enriched += 1;
            }
            catch (error) {
                failed += 1;
                errors.push({ sourceId: source.id, error: error instanceof Error ? error.message : String(error) });
            }
            await yieldToEventLoop();
        }
        const selfImprovement = await this.selfImprove({
            knowledgeSpaceId: input.knowledgeSpaceId,
            sourceIds: input.sourceIds,
            force: input.force,
            reason: 'reindex',
            limit: input.limit,
            maxRunMs: input.maxRunMs,
            deferRepair: Boolean(this.options.gapRepairer),
        });
        const backgroundRepairLimit = Math.max(1, this.options.backgroundRepairLimit ?? 8);
        this.runSelfImprovementInBackground({
            knowledgeSpaceId: input.knowledgeSpaceId,
            sourceIds: input.sourceIds,
            force: true,
            reason: 'reindex',
            limit: Math.min(Math.max(1, input.limit ?? backgroundRepairLimit), backgroundRepairLimit),
            maxRunMs: 30_000,
        }, this.options.backgroundRepairDelayMs ?? 2_000);
        return { scanned: sources.length, enriched, skipped, failed, errors, selfImprovement };
    }
    async answer(input) {
        await this.store.init();
        let answer = await answerKnowledgeQuery({
            store: this.store,
            llm: this.options.llm,
            objectProfiles: this.options.objectProfiles,
        }, input);
        if (input.autoRepairGaps === false)
            return answer;
        if (this.options.gapRepairer && answer.answer.gaps.length > 0) {
            const repairSpaceId = answerRepairSpaceId(answer);
            const originalGapIds = answer.answer.gaps
                .filter((gap) => readString(readRecord(gap.metadata).gapKind) === 'answer')
                .map((gap) => gap.id);
            const foregroundBudgetMs = foregroundAnswerRepairBudget(input, answer);
            const foregroundTaskIds = [];
            const foregroundStartedAt = Date.now();
            let foregroundRepair = emptySelfImproveResult();
            if (foregroundBudgetMs > 0) {
                foregroundRepair = await this.repairAnswerGaps({
                    answer,
                    maxRunMs: foregroundBudgetMs,
                    limit: Math.min(5, answer.answer.gaps.length),
                });
                foregroundTaskIds.push(...foregroundRepair.taskIds);
                if (foregroundRepair.closedGaps > 0 || foregroundRepair.linkedRepairs > 0 || (foregroundRepair.promotedFactCount ?? 0) > 0) {
                    answer = withRefinementTaskIds(await answerKnowledgeQuery({
                        store: this.store,
                        llm: this.options.llm,
                        objectProfiles: this.options.objectProfiles,
                    }, input), foregroundTaskIds);
                    if (answerHasUsableEvidence(answer)) {
                        return withAnswerRefinement(answer, answerRefinementFromRepair(foregroundRepair, 'repaired', {
                            waitedMs: Date.now() - foregroundStartedAt,
                            answerCacheInvalidated: true,
                        }));
                    }
                }
                if (foregroundRepair.skippedGaps > 0 || foregroundRepair.queuedTasks > 0 || (foregroundRepair.acceptedSourceIds?.length ?? 0) > 0) {
                    const waited = await this.waitForActiveAnswerGapRepairs(repairSpaceId, answerGapIdsForRefinement(answer, originalGapIds), Math.min(15_000, foregroundBudgetMs));
                    if (waited) {
                        answer = withRefinementTaskIds(await answerKnowledgeQuery({
                            store: this.store,
                            llm: this.options.llm,
                            objectProfiles: this.options.objectProfiles,
                        }, input), foregroundTaskIds);
                        if (answerHasUsableEvidence(answer)) {
                            return withAnswerRefinement(answer, answerRefinementFromRepair(foregroundRepair, 'repaired', {
                                waitedMs: Date.now() - foregroundStartedAt,
                                answerCacheInvalidated: true,
                            }));
                        }
                    }
                }
            }
            const refinementGapIds = answerGapIdsForRefinement(answer, originalGapIds);
            const refinement = await this.selfImprove({
                knowledgeSpaceId: repairSpaceId,
                gapIds: refinementGapIds,
                reason: 'answer',
                limit: Math.max(1, refinementGapIds.length),
                deferRepair: true,
            });
            this.runAnswerRefinementInBackground(repairSpaceId, refinementGapIds);
            const taskIds = uniqueStrings([
                ...foregroundTaskIds,
                ...refinement.taskIds,
                ...this.taskIdsForGaps(repairSpaceId, refinementGapIds),
            ]);
            if (taskIds.length > 0) {
                return withAnswerRefinement(withRefinementTaskIds(answer, taskIds), answerRefinementFromRepair(mergeSelfImproveResults(foregroundRepair, refinement), repairAnswerStatus(answer, foregroundRepair, refinement), {
                    waitedMs: Date.now() - foregroundStartedAt,
                    answerCacheInvalidated: (foregroundRepair.linkedRepairs > 0 || (foregroundRepair.promotedFactCount ?? 0) > 0),
                }));
            }
        }
        return answer;
    }
    async repairAnswerGaps(input) {
        const gaps = input.answer.answer.gaps.filter((gap) => readString(readRecord(gap.metadata).gapKind) === 'answer');
        if (!this.options.gapRepairer || gaps.length === 0)
            return emptySelfImproveResult();
        return this.selfImprove({
            knowledgeSpaceId: answerRepairSpaceId(input.answer),
            gapIds: gaps.map((gap) => gap.id),
            reason: 'answer',
            limit: Math.max(1, input.limit ?? gaps.length),
            maxRunMs: input.maxRunMs,
            force: true,
        });
    }
    runAnswerRefinementInBackground(spaceId, gapIds) {
        if (!this.options.gapRepairer || gapIds.length === 0)
            return;
        this.runSelfImprovementInBackground({
            knowledgeSpaceId: spaceId,
            gapIds,
            reason: 'answer',
            limit: Math.max(1, gapIds.length),
            maxRunMs: 30_000,
            force: true,
        });
    }
    runSelfImprovementInBackground(input, delayMs = 0) {
        if (!this.options.gapRepairer)
            return;
        scheduleBackground(() => {
            void this.selfImprove(input).catch((error) => {
                logger.warn('Knowledge semantic background self-improvement failed', {
                    error: error instanceof Error ? error.message : String(error),
                    knowledgeSpaceId: input.knowledgeSpaceId,
                    reason: input.reason,
                });
            });
        }, Math.max(0, delayMs));
    }
    async selfImprove(input = {}) {
        await this.store.init();
        if (input.deferRepair !== true) {
            const runKey = selfImprovementRunKey(input);
            const activeRun = this.activeSelfImprovementRuns.get(runKey);
            if (activeRun && !input.gapIds?.length)
                return activeSelfImproveResult(input);
            const run = this.runSelfImproveUnlocked(input);
            if (!input.gapIds?.length)
                this.activeSelfImprovementRuns.set(runKey, run);
            try {
                return await run;
            }
            finally {
                if (this.activeSelfImprovementRuns.get(runKey) === run)
                    this.activeSelfImprovementRuns.delete(runKey);
            }
        }
        return this.runSelfImproveUnlocked(input);
    }
    async runSelfImproveUnlocked(input) {
        if (!input.knowledgeSpaceId && !input.sourceIds?.length && !input.gapIds?.length) {
            const spaces = uniqueStrings([
                ...this.store.listSources(Number.MAX_SAFE_INTEGER)
                    .filter((source) => source.status !== 'stale')
                    .map((source) => getKnowledgeSpaceId(source)),
                ...this.store.listNodes(Number.MAX_SAFE_INTEGER)
                    .filter((node) => node.status !== 'stale')
                    .map((node) => getKnowledgeSpaceId(node)),
            ]);
            let combined = emptySelfImproveResult();
            for (const [index, spaceId] of spaces.entries()) {
                await yieldEvery(index, 1);
                const result = await runKnowledgeSemanticSelfImprovement({
                    store: this.store,
                    gapRepairer: this.options.gapRepairer,
                    activeGapRepairs: this.activeGapRepairs,
                    objectProfiles: this.options.objectProfiles,
                    enrichSource: (sourceId, options) => {
                        const source = this.store.getSource(sourceId);
                        return source ? enrichKnowledgeSource({ store: this.store, llm: this.options.llm }, source, options) : Promise.resolve(null);
                    },
                }, { ...input, knowledgeSpaceId: spaceId });
                combined = mergeSelfImproveResults(combined, result);
            }
            return combined;
        }
        return runKnowledgeSemanticSelfImprovement({
            store: this.store,
            gapRepairer: this.options.gapRepairer,
            activeGapRepairs: this.activeGapRepairs,
            objectProfiles: this.options.objectProfiles,
            enrichSource: (sourceId, options) => {
                const source = this.store.getSource(sourceId);
                return source ? enrichKnowledgeSource({ store: this.store, llm: this.options.llm }, source, options) : Promise.resolve(null);
            },
        }, input);
    }
    taskIdsForGaps(spaceId, gapIds) {
        const wanted = new Set(gapIds);
        if (wanted.size === 0)
            return [];
        return this.store.listRefinementTasks(100, { spaceId })
            .filter((task) => task.gapId && wanted.has(task.gapId))
            .map((task) => task.id);
    }
    async waitForActiveAnswerGapRepairs(spaceId, gapIds, maxWaitMs) {
        if (gapIds.length === 0 || maxWaitMs <= 0)
            return false;
        const keys = gapIds.map((gapId) => `${spaceId}:${gapId}`);
        const startedAt = Date.now();
        while (Date.now() - startedAt < maxWaitMs) {
            const active = keys.some((key) => this.activeGapRepairs.has(key));
            if (!active)
                return true;
            await yieldToEventLoop();
            await sleep(100);
        }
        return false;
    }
}
function listSemanticSources(store, spaceId) {
    if (!spaceId)
        return store.listSources(Number.MAX_SAFE_INTEGER).filter(isSemanticSourceCandidate);
    const normalized = normalizeKnowledgeSpaceId(spaceId);
    if (normalized === 'homeassistant') {
        return store.listSources(Number.MAX_SAFE_INTEGER)
            .filter((source) => isSemanticSourceCandidate(source) && isHomeAssistantKnowledgeSpace(normalizeKnowledgeSpaceId(getKnowledgeSpaceId(source))));
    }
    return store.listSourcesInSpace(normalized).filter(isSemanticSourceCandidate);
}
function isSemanticSourceCandidate(source) {
    return source.status !== 'stale' && !isGeneratedKnowledgeSource(source);
}
function answerRepairSpaceId(answer) {
    for (const gap of answer.answer.gaps) {
        const spaceId = normalizeKnowledgeSpaceId(getKnowledgeSpaceId(gap));
        if (spaceId && spaceId !== 'default' && spaceId !== 'homeassistant')
            return spaceId;
    }
    for (const source of answer.answer.sources) {
        const spaceId = normalizeKnowledgeSpaceId(getKnowledgeSpaceId(source));
        if (spaceId && spaceId !== 'default' && spaceId !== 'homeassistant')
            return spaceId;
    }
    for (const node of answer.answer.linkedObjects) {
        const spaceId = normalizeKnowledgeSpaceId(getKnowledgeSpaceId(node));
        if (spaceId && spaceId !== 'default' && spaceId !== 'homeassistant')
            return spaceId;
    }
    return answer.spaceId;
}
function foregroundAnswerRepairBudget(input, answer) {
    if (!answerNeedsForegroundRepair(input, answer))
        return 0;
    const requested = typeof input.timeoutMs === 'number' && Number.isFinite(input.timeoutMs)
        ? input.timeoutMs
        : 45_000;
    if (requested < 12_000)
        return 0;
    return Math.max(3_000, Math.min(20_000, requested - 5_000));
}
function answerNeedsForegroundRepair(input, answer) {
    const subjectScoped = input.strictCandidates === true
        || (input.linkedObjects?.length ?? 0) > 0
        || answer.answer.linkedObjects.length > 0
        || answer.answer.gaps.some((gap) => readStringArray(readRecord(gap.metadata).linkedObjectIds).length > 0);
    if (!subjectScoped)
        return false;
    return answer.answer.facts.length === 0 || answer.answer.sources.length === 0 || answer.answer.confidence < 50;
}
function answerHasUsableEvidence(answer) {
    return answer.answer.gaps.length === 0
        && answer.answer.facts.length > 0
        && answer.answer.sources.length > 0
        && answer.answer.confidence >= 50;
}
function withRefinementTaskIds(answer, taskIds) {
    const ids = uniqueStrings([...(answer.answer.refinementTaskIds ?? []), ...taskIds]);
    if (ids.length === 0)
        return answer;
    return {
        ...answer,
        answer: {
            ...answer.answer,
            refinementTaskIds: ids,
        },
    };
}
function answerGapIdsForRefinement(answer, originalGapIds) {
    const currentGapIds = answer.answer.gaps
        .filter((gap) => readString(readRecord(gap.metadata).gapKind) === 'answer')
        .map((gap) => gap.id);
    return currentGapIds.length > 0 ? currentGapIds : originalGapIds;
}
function withAnswerRefinement(answer, refinement) {
    return {
        ...answer,
        answer: {
            ...answer.answer,
            refinement,
        },
    };
}
function repairAnswerStatus(answer, foreground, queued) {
    if (answerHasUsableEvidence(answer))
        return 'repaired';
    if ((foreground.acceptedSourceIds?.length ?? 0) > 0 || (foreground.promotedFactCount ?? 0) > 0)
        return 'incomplete';
    if (queued.queuedTasks > 0 || foreground.queuedTasks > 0 || foreground.skippedGaps > 0)
        return 'active';
    return 'deferred';
}
function answerRefinementFromRepair(repair, status, options = {}) {
    const acceptedSourceIds = repair.acceptedSourceIds ?? [];
    const promotedFactCount = repair.promotedFactCount ?? 0;
    const reason = repair.errors[0]?.error
        ?? (repair.budgetExhausted ? 'Repair did not finish within the current run budget.' : undefined);
    return {
        status,
        ...(reason ? { reason } : {}),
        repairStatus: status === 'repaired' ? 'repaired' : status === 'active' ? 'active' : 'deferred',
        refinementTaskIds: repair.taskIds,
        acceptedSourceIds,
        promotedFactCount,
        ...(repair.nextRepairAttemptAt ? { nextRepairAttemptAt: repair.nextRepairAttemptAt } : {}),
        ...(typeof options.waitedMs === 'number' ? { waitedMs: options.waitedMs } : {}),
        ...(options.answerCacheInvalidated ? { answerCacheInvalidated: true } : {}),
    };
}
function activeSelfImproveResult(input) {
    const requestedLimit = Math.max(1, input.limit ?? 1);
    return {
        ...emptySelfImproveResult(),
        skippedGaps: 1,
        requestedLimit,
        effectiveLimit: 0,
        coalesced: true,
        truncated: true,
        budgetExhausted: true,
    };
}
function selfImprovementRunKey(input) {
    if (input.knowledgeSpaceId)
        return `space:${normalizeKnowledgeSpaceId(input.knowledgeSpaceId)}`;
    if (input.sourceIds?.length)
        return `sources:${uniqueStrings(input.sourceIds).sort().join(',')}`;
    return 'global';
}
function sourceCanUseLlmUpgrade(store, source) {
    const extraction = store.getExtractionBySourceId(source.id);
    const text = sourceSemanticText(source, extraction);
    if (text.length < 40)
        return false;
    const existingSemantic = readRecord(source.metadata.semanticEnrichment);
    return existingSemantic.textHash !== sourceSemanticHash(source, extraction)
        || readString(existingSemantic.extractor) !== 'llm';
}
function emptySelfImproveResult() {
    return {
        scannedGaps: 0,
        candidateGaps: 0,
        processedGaps: 0,
        createdGaps: 0,
        repairableGaps: 0,
        suppressedGaps: 0,
        skippedGaps: 0,
        searched: 0,
        ingestedSources: 0,
        linkedRepairs: 0,
        blockedGaps: 0,
        closedGaps: 0,
        queuedTasks: 0,
        requestedLimit: 0,
        effectiveLimit: 0,
        truncated: false,
        budgetExhausted: false,
        taskIds: [],
        ingestedSourceIds: [],
        acceptedSourceIds: [],
        promotedFactCount: 0,
        errors: [],
    };
}
function mergeSelfImproveResults(left, right) {
    return {
        scannedGaps: left.scannedGaps + right.scannedGaps,
        candidateGaps: (left.candidateGaps ?? 0) + (right.candidateGaps ?? 0),
        processedGaps: (left.processedGaps ?? 0) + (right.processedGaps ?? 0),
        createdGaps: left.createdGaps + right.createdGaps,
        repairableGaps: left.repairableGaps + right.repairableGaps,
        suppressedGaps: left.suppressedGaps + right.suppressedGaps,
        skippedGaps: left.skippedGaps + right.skippedGaps,
        searched: left.searched + right.searched,
        ingestedSources: left.ingestedSources + right.ingestedSources,
        linkedRepairs: left.linkedRepairs + right.linkedRepairs,
        blockedGaps: left.blockedGaps + right.blockedGaps,
        closedGaps: left.closedGaps + right.closedGaps,
        queuedTasks: left.queuedTasks + right.queuedTasks,
        requestedLimit: (left.requestedLimit ?? 0) + (right.requestedLimit ?? 0),
        effectiveLimit: (left.effectiveLimit ?? 0) + (right.effectiveLimit ?? 0),
        coalesced: Boolean(left.coalesced || right.coalesced),
        truncated: Boolean(left.truncated || right.truncated),
        budgetExhausted: Boolean(left.budgetExhausted || right.budgetExhausted),
        taskIds: uniqueStrings([...left.taskIds, ...right.taskIds]),
        ingestedSourceIds: uniqueStrings([...left.ingestedSourceIds, ...right.ingestedSourceIds]),
        acceptedSourceIds: uniqueStrings([...(left.acceptedSourceIds ?? []), ...(right.acceptedSourceIds ?? [])]),
        promotedFactCount: (left.promotedFactCount ?? 0) + (right.promotedFactCount ?? 0),
        nextRepairAttemptAt: maxDefined(left.nextRepairAttemptAt, right.nextRepairAttemptAt),
        errors: [...left.errors, ...right.errors],
    };
}
function maxDefined(left, right) {
    if (typeof left !== 'number')
        return right;
    if (typeof right !== 'number')
        return left;
    return Math.max(left, right);
}
