import { yieldEvery, yieldToEventLoop } from '../cooperative.js';
import { isGeneratedPageSource, readHomeAssistantMetadataString, uniqueStrings } from './helpers.js';
import { resolveReadableHomeGraphSpace } from './space-selection.js';
import { readHomeGraphSearchState } from './search.js';
import { readHomeGraphState } from './state.js';
import { homeGraphExtractionNeedsRepair } from './search.js';
import { HOME_GRAPH_PAGE_POLICY_VERSION, refreshHomeGraphDevicePassport, } from './generated-pages.js';
export function coalescedHomeGraphReindexResult(store, input) {
    const { spaceId } = resolveReadableHomeGraphSpace(store, input);
    return {
        ok: true,
        spaceId,
        scanned: 0,
        reparsed: 0,
        skipped: 0,
        failed: 0,
        sources: [],
        failures: [],
        coalesced: true,
        truncated: true,
        budgetExhausted: true,
    };
}
export async function runHomeGraphReindex(context, input = {}) {
    await context.store.init();
    const { spaceId, installationId } = resolveReadableHomeGraphSpace(context.store, input);
    const state = readHomeGraphSearchState(context.store, spaceId);
    const allSourceCount = readHomeGraphState(context.store, spaceId).sources.length;
    const startedAt = Date.now();
    const maxRunMs = clampPositive(input.maxRunMs, 90_000, 15_000, 180_000);
    const sources = state.sources.filter((source) => !isGeneratedPageSource(source));
    const skippedGeneratedPageArtifactCount = Math.max(0, allSourceCount - sources.length);
    const reindex = await reindexHomeGraphSources({
        spaceId,
        sources,
        extractionBySourceId: state.extractionBySourceId,
        artifactStore: context.artifactStore,
        maxRunMs: Math.min(maxRunMs, 30_000),
        limit: input.limit,
        extract: (source, artifact) => context.extract(source, artifact, spaceId, installationId),
    });
    await yieldToEventLoop();
    const relinkedSourceIds = reindex.sources.map((source) => source.id);
    const linked = relinkedSourceIds.length > 0 || input.force === true
        ? await context.autoLinkExistingSources(spaceId, installationId, input.force === true ? undefined : relinkedSourceIds)
        : [];
    await yieldToEventLoop();
    const changedSourceIds = uniqueStrings([
        ...reindex.sources.map((source) => source.id),
        ...linked.map((item) => item.edge.fromKind === 'source' ? item.edge.fromId : undefined),
    ]);
    const forcedSourceIds = input.force === true && changedSourceIds.length === 0
        ? sources.slice(0, clampPositive(input.semanticLimit, 8, 1, 24)).map((source) => source.id)
        : [];
    const semanticSourceIds = changedSourceIds.length > 0 ? changedSourceIds : forcedSourceIds;
    const remainingMs = Math.max(5_000, maxRunMs - (Date.now() - startedAt));
    const semantic = semanticSourceIds.length > 0
        ? await context.semanticService?.reindex({
            knowledgeSpaceId: spaceId,
            sourceIds: semanticSourceIds,
            limit: semanticSourceIds.length,
            maxRunMs: Math.min(remainingMs, clampPositive(input.semanticMaxRunMs, 20_000, 5_000, 45_000)),
            force: input.force === true,
        })
        : undefined;
    await yieldToEventLoop();
    const qualityIssues = await context.refreshQualityIssues(spaceId, installationId);
    await yieldToEventLoop();
    const currentState = readHomeGraphState(context.store, spaceId);
    const generated = input.refreshPages === false
        ? emptyGeneratedPagesSummary()
        : await refreshHomeGraphPagesForSources({
            store: context.store,
            artifactStore: context.artifactStore,
            spaceId,
            installationId,
        }, currentState.sources, changedSourceIds, clampPositive(input.generatedPageLimit, 256, 1, 512));
    const refreshedGeneratedPageCount = generated.devicePassports + generated.roomPages;
    return {
        ...reindex,
        changedSourceCount: changedSourceIds.length,
        forcedSourceCount: forcedSourceIds.length,
        skippedGeneratedPageArtifactCount,
        refreshedGeneratedPageCount,
        generatedPagePolicyVersion: HOME_GRAPH_PAGE_POLICY_VERSION,
        ...(linked.length > 0 ? { linked } : {}),
        ...(semantic ? { semantic } : {}),
        qualityIssues,
        generated,
    };
}
export async function reindexHomeGraphSources(input) {
    const sources = [];
    const failures = [];
    const maxRunMs = clampPositive(input.maxRunMs, 30_000, 1_000, 120_000);
    const startedAt = Date.now();
    const candidates = input.sources.slice(0, clampPositive(input.limit, input.sources.length, 1, 10_000));
    let scanned = 0;
    let reparsed = 0;
    let skipped = 0;
    let failed = 0;
    let truncated = input.sources.length > candidates.length;
    let budgetExhausted = false;
    for (const [index, source] of candidates.entries()) {
        await yieldEvery(index);
        if (Date.now() - startedAt >= maxRunMs) {
            truncated = true;
            budgetExhausted = true;
            break;
        }
        const artifactId = typeof source.artifactId === 'string' ? source.artifactId : undefined;
        if (!artifactId)
            continue;
        scanned += 1;
        const current = input.extractionBySourceId.get(source.id);
        if (!homeGraphExtractionNeedsRepair(current)) {
            skipped += 1;
            continue;
        }
        const artifact = input.artifactStore.get(artifactId);
        if (!artifact) {
            if (isGeneratedPageSource(source)) {
                skipped += 1;
                continue;
            }
            failed += 1;
            failures.push({ sourceId: source.id, error: `Unknown artifact: ${artifactId}` });
            continue;
        }
        const extraction = await input.extract(source, artifact);
        if (!extraction) {
            failed += 1;
            failures.push({ sourceId: source.id, error: 'Artifact extraction failed.' });
            continue;
        }
        reparsed += 1;
        sources.push(source);
        await yieldToEventLoop();
    }
    return { ok: true, spaceId: input.spaceId, scanned, reparsed, skipped, failed, sources, failures, truncated, budgetExhausted };
}
async function refreshHomeGraphPagesForSources(context, sources, sourceIds, limit) {
    const summary = emptyGeneratedPagesSummary();
    const state = readHomeGraphState(context.store, context.spaceId);
    const devices = uniqueStrings([
        ...devicesLinkedToSources(state, sourceIds),
        ...devicesWithStaleGeneratedPages(state, sources),
    ]).slice(0, limit);
    if (devices.length === 0)
        return summary;
    for (const [index, deviceId] of devices.entries()) {
        await yieldEvery(index, 2);
        try {
            const page = await refreshHomeGraphDevicePassport({
                ...context,
                input: {
                    knowledgeSpaceId: context.spaceId,
                    deviceId,
                    metadata: { automation: 'reindex' },
                },
            });
            summary.devicePassports += 1;
            if (page.artifactCreated)
                summary.artifacts += 1;
            if (page.source)
                summary.sources += 1;
        }
        catch (error) {
            summary.errors.push({
                kind: 'device-passport',
                targetId: deviceId,
                error: error instanceof Error ? error.message : String(error),
            });
        }
        await yieldToEventLoop();
    }
    return summary;
}
function devicesLinkedToSources(state, sourceIds) {
    const wantedSources = new Set(sourceIds);
    if (wantedSources.size === 0)
        return [];
    const nodesById = new Map(state.nodes.map((node) => [node.id, node]));
    const factIds = new Set();
    const deviceIds = new Set();
    for (const edge of state.edges) {
        if (edge.fromKind !== 'source' || !wantedSources.has(edge.fromId) || edge.toKind !== 'node')
            continue;
        const node = nodesById.get(edge.toId);
        if ((edge.relation === 'source_for' || edge.relation === 'has_manual' || edge.relation === 'repairs_gap') && node?.kind === 'ha_device') {
            const deviceId = readHomeAssistantMetadataString(node, 'objectId', 'deviceId') ?? node.id;
            deviceIds.add(deviceId);
        }
        if (edge.relation === 'supports_fact' && node?.kind === 'fact')
            factIds.add(node.id);
    }
    for (const edge of state.edges) {
        if (edge.fromKind !== 'node' || !factIds.has(edge.fromId) || edge.toKind !== 'node' || edge.relation !== 'describes')
            continue;
        const device = nodesById.get(edge.toId);
        if (device?.kind === 'ha_device')
            deviceIds.add(readHomeAssistantMetadataString(device, 'objectId', 'deviceId') ?? device.id);
    }
    return [...deviceIds];
}
function devicesWithStaleGeneratedPages(state, sources) {
    const stalePageIds = new Set(sources
        .filter(isGeneratedPageSource)
        .filter((source) => source.metadata.projectionKind === 'device-passport')
        .filter((source) => source.metadata.pagePolicyVersion !== HOME_GRAPH_PAGE_POLICY_VERSION)
        .map((source) => source.id));
    if (stalePageIds.size === 0)
        return [];
    return state.edges
        .filter((edge) => edge.fromKind === 'source' && stalePageIds.has(edge.fromId) && edge.toKind === 'node')
        .filter((edge) => edge.relation === 'source_for')
        .map((edge) => state.nodes.find((node) => node.id === edge.toId && node.kind === 'ha_device_passport'))
        .filter((passport) => Boolean(passport))
        .map((passport) => readHomeAssistantMetadataString(passport, 'objectId', 'deviceId'))
        .filter((deviceId) => Boolean(deviceId));
}
function emptyGeneratedPagesSummary() {
    return { devicePassports: 0, roomPages: 0, artifacts: 0, sources: 0, errors: [] };
}
function clampPositive(value, fallback, min, max) {
    const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
    const effective = Number.isFinite(parsed) ? parsed : fallback;
    return Math.min(max, Math.max(min, Math.trunc(effective)));
}
