import { NextResponse } from "next/server";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
import { getEntityIds } from "@open-mercato/shared/lib/encryption/entityIds";
import { sql } from "kysely";
import { readCoverageSnapshots, refreshCoverageSnapshot } from "../lib/coverage.js";
import { mapWithConcurrency } from "@open-mercato/shared/lib/query/bounded-decrypt";
import { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from "./openapi.js";
import { flattenSystemEntityIds } from "@open-mercato/shared/lib/entities/system-entities";
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
const metadata = {
  GET: { requireAuth: true, requireFeatures: ["query_index.status.view"] }
};
const STATUS_REFRESH_COOLDOWN_MS = 6e4;
function getCoverageSnapshotRefreshedAt(snapshot) {
  const value = snapshot?.refreshed_at;
  if (value instanceof Date) {
    const time = value.getTime();
    return Number.isFinite(time) ? time : null;
  }
  if (typeof value === "string") {
    const time = new Date(value).getTime();
    return Number.isFinite(time) ? time : null;
  }
  return null;
}
function hasFreshCoverageSnapshots(snapshots, entityIds, now) {
  for (const entityId of entityIds) {
    const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId));
    if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false;
  }
  return entityIds.length > 0;
}
async function GET(req) {
  const auth = await getAuthFromRequest(req);
  if (!auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  const container = await createRequestContainer();
  const em = container.resolve("em");
  const db = em.getKysely();
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
  const organizationId = scope.selectedId ?? auth.orgId ?? null;
  const tenantId = typeof scope.tenantId === "string" && scope.tenantId.trim().length > 0 ? scope.tenantId.trim() : typeof auth.tenantId === "string" && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null;
  if (!tenantId) {
    return NextResponse.json({ error: "Tenant context is required" }, { status: 400 });
  }
  const organizationFilter = scope.filterIds === null ? null : Array.isArray(scope.filterIds) && scope.filterIds.length > 0 ? scope.filterIds : organizationId ? [organizationId] : [];
  if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {
    return NextResponse.json({ error: "Organization access denied" }, { status: 403 });
  }
  const organizationScopeIds = organizationFilter === null ? null : Array.from(
    new Set(
      organizationFilter.filter(
        (value) => typeof value === "string" && value.length > 0
      )
    )
  );
  if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {
    return NextResponse.json({ error: "Organization access denied" }, { status: 403 });
  }
  const url = new URL(req.url);
  const forceRefresh = url.searchParams.has("refresh") && url.searchParams.get("refresh") !== "0";
  const generatedIds = flattenSystemEntityIds(getEntityIds());
  const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }));
  const byId = /* @__PURE__ */ new Map();
  for (const g of generated) byId.set(g.entityId, g);
  let entityIds = generatedIds.slice();
  let searchModuleConfigs = [];
  try {
    searchModuleConfigs = container.resolve("searchModuleConfigs");
  } catch {
  }
  const vectorEnabledEntities = /* @__PURE__ */ new Set();
  const fulltextEnabledEntities = /* @__PURE__ */ new Set();
  for (const moduleConfig of searchModuleConfigs) {
    for (const entity of moduleConfig.entities ?? []) {
      if (entity.enabled !== false) {
        if (typeof entity.buildSource === "function") {
          vectorEnabledEntities.add(entity.entityId);
        }
        if (entity.fieldPolicy && typeof entity.fieldPolicy === "object") {
          fulltextEnabledEntities.add(entity.entityId);
        }
      }
    }
  }
  let fulltextStrategy = null;
  try {
    const searchStrategies = container.resolve("searchStrategies");
    fulltextStrategy = searchStrategies?.find(
      (s) => s?.id === "fulltext"
    ) ?? null;
  } catch {
    fulltextStrategy = null;
  }
  let fulltextEntityCounts = null;
  if (fulltextStrategy) {
    try {
      fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId);
    } catch {
      fulltextEntityCounts = null;
    }
  }
  try {
    let cfQuery = db.selectFrom("custom_field_defs").select(["entity_id"]).distinct().where("is_active", "=", true);
    if (tenantId != null) {
      cfQuery = cfQuery.where((eb) => eb.or([
        eb("tenant_id", "=", tenantId),
        eb("tenant_id", "is", null)
      ]));
    } else {
      cfQuery = cfQuery.where("tenant_id", "is", null);
    }
    if (Array.isArray(organizationScopeIds)) {
      cfQuery = cfQuery.where((eb) => eb.or([
        eb("organization_id", "in", organizationScopeIds),
        eb("organization_id", "is", null)
      ]));
    }
    const cfRows = await cfQuery.execute();
    const enabled = new Set((cfRows || []).map((r) => String(r.entity_id)));
    entityIds = entityIds.filter((id) => enabled.has(id));
  } catch {
  }
  const HEARTBEAT_STALE_MS = 6e4;
  const COVERAGE_STALE_MS = 6e4;
  const COVERAGE_REFRESH_CONCURRENCY = 8;
  async function fetchJobSummary(entityType, tenantIdParam, organizationIdParam) {
    try {
      let jobQuery = db.selectFrom("entity_index_jobs").selectAll().where("entity_type", "=", entityType).where(sql`tenant_id is not distinct from ${tenantIdParam ?? null}`);
      if (organizationIdParam != null) {
        jobQuery = jobQuery.where((eb) => eb.or([
          eb("organization_id", "=", organizationIdParam),
          eb("organization_id", "is", null)
        ]));
      } else {
        jobQuery = jobQuery.where(sql`organization_id is not distinct from ${null}`);
      }
      const rows = await jobQuery.orderBy("started_at", "desc").execute();
      if (!rows.length) {
        return { status: "idle", partitions: [] };
      }
      const preferOrg = organizationIdParam != null && rows.some((row) => row.organization_id === organizationIdParam);
      const pickPreferred = (existing, candidate) => {
        if (!existing) return candidate;
        if (preferOrg) {
          if (candidate.orgMatch && !existing.orgMatch) return candidate;
          if (!candidate.orgMatch && existing.orgMatch) return existing;
        }
        if (candidate.tenantMatch && !existing.tenantMatch) return candidate;
        if (!candidate.tenantMatch && existing.tenantMatch) return existing;
        return candidate.startedTs > existing.startedTs ? candidate : existing;
      };
      const partitionRows = /* @__PURE__ */ new Map();
      let scopeRow = null;
      for (const row of rows) {
        const key = String(row.partition_index ?? "__null__");
        const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0;
        const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true;
        const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null;
        const candidate = { row, startedTs, tenantMatch, orgMatch };
        if (row.partition_index == null) {
          scopeRow = pickPreferred(scopeRow, candidate);
          continue;
        }
        const existing = partitionRows.get(key);
        partitionRows.set(key, pickPreferred(existing ?? null, candidate));
      }
      const partitions = Array.from(partitionRows.values()).filter((entry) => !preferOrg || entry.orgMatch).map(({ row }) => {
        const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null;
        const startedDate = row.started_at ? new Date(row.started_at) : null;
        const finishedDate = row.finished_at ? new Date(row.finished_at) : null;
        const stalled = !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS);
        const state = finishedDate ? "completed" : stalled ? "stalled" : row.status || "reindexing";
        return {
          partitionIndex: row.partition_index ?? null,
          partitionCount: row.partition_count ?? null,
          status: state,
          startedAt: startedDate ? startedDate.toISOString() : null,
          finishedAt: finishedDate ? finishedDate.toISOString() : null,
          heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,
          processedCount: row.processed_count ?? null,
          totalCount: row.total_count ?? null
        };
      }).sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0));
      const activePartitions = partitions.filter((p) => !p.finishedAt);
      const runningPartitions = activePartitions.filter(
        (p) => p.status === "reindexing" || p.status === "purging"
      );
      const stalledPartitions = activePartitions.filter((p) => p.status === "stalled");
      let status = "idle";
      if (activePartitions.length) {
        if (runningPartitions.length) {
          status = runningPartitions.some((p) => p.status === "purging") ? "purging" : "reindexing";
        } else if (stalledPartitions.length) {
          status = "stalled";
        }
      }
      const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null;
      const finishedAt = status === "idle" ? partitions.find((p) => p.finishedAt)?.finishedAt ?? null : null;
      const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null;
      const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0);
      const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0);
      const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null;
      const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null;
      return {
        status,
        startedAt,
        finishedAt,
        heartbeatAt,
        processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,
        totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,
        partitions,
        scope: scopeCandidate ? {
          status: (() => {
            const heartbeatDate = scopeCandidate.row.heartbeat_at ? new Date(scopeCandidate.row.heartbeat_at) : null;
            const finishedDate = scopeCandidate.row.finished_at ? new Date(scopeCandidate.row.finished_at) : null;
            if (finishedDate) return "completed";
            if (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS) {
              return "stalled";
            }
            return scopeCandidate.row.status || "reindexing";
          })(),
          processedCount: scopeCandidate.row.processed_count ?? null,
          totalCount: scopeCandidate.row.total_count ?? null
        } : null
      };
    } catch {
      return { status: "idle", partitions: [] };
    }
  }
  const normalizeCount = (value) => {
    if (value == null) return null;
    if (typeof value === "number") return Number.isFinite(value) ? value : null;
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  };
  const coverageScope = {
    tenantId: tenantId ?? null,
    organizationId,
    withDeleted: false
  };
  const entitiesNeedingRefresh = /* @__PURE__ */ new Set();
  const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope });
  if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {
    await mapWithConcurrency(
      entityIds,
      COVERAGE_REFRESH_CONCURRENCY,
      (entityId) => refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => void 0)
    );
    const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope });
    for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot);
  }
  const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null);
  const jobs = await Promise.all(entityIds.map((eid) => fetchJobSummary(eid, tenantId, organizationId)));
  const items = [];
  for (let idx = 0; idx < entityIds.length; idx += 1) {
    const eid = entityIds[idx];
    let coverage = coverageSnapshots[idx];
    const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null;
    const isStale = !coverage || !refreshedAt || Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS;
    if (isStale) entitiesNeedingRefresh.add(eid);
    const job = jobs[idx];
    const label = byId.get(eid)?.label || eid;
    const baseCountNumber = normalizeCount(coverage?.baseCount);
    const indexCountNumber = normalizeCount(coverage?.indexedCount);
    const vectorEnabled = vectorEnabledEntities.has(eid);
    const vectorCountNumber = vectorEnabled ? normalizeCount(coverage?.vectorIndexedCount ?? coverage?.vector_indexed_count) : null;
    const fulltextEnabled = fulltextEnabledEntities.has(eid);
    const fulltextCountNumber = fulltextEnabled ? fulltextEntityCounts?.[eid] ?? 0 : null;
    const ok = (() => {
      if (baseCountNumber == null || indexCountNumber == null) return false;
      if (baseCountNumber !== indexCountNumber) return false;
      if (!vectorEnabled) return true;
      return vectorCountNumber != null && vectorCountNumber === baseCountNumber;
    })();
    items.push({
      entityId: eid,
      label,
      baseCount: baseCountNumber,
      indexCount: indexCountNumber,
      vectorCount: vectorEnabled ? vectorCountNumber : null,
      vectorEnabled,
      fulltextCount: fulltextCountNumber,
      fulltextEnabled,
      ok,
      job,
      refreshedAt: refreshedAt ?? null
    });
  }
  if (!forceRefresh) {
    try {
      const eventBus = container.resolve("eventBus");
      if (entitiesNeedingRefresh.size > 0) {
        await Promise.all(
          Array.from(entitiesNeedingRefresh).map(
            (entityId) => eventBus.emitEvent("query_index.coverage.refresh", {
              entityType: entityId,
              tenantId: tenantId ?? null,
              organizationId,
              delayMs: 0
            }).catch(() => void 0)
          )
        );
      }
    } catch {
    }
  }
  let errorQuery = db.selectFrom("indexer_error_logs").selectAll();
  if (tenantId != null) {
    errorQuery = errorQuery.where((eb) => eb.or([
      eb("tenant_id", "=", tenantId),
      eb("tenant_id", "is", null)
    ]));
  } else {
    errorQuery = errorQuery.where("tenant_id", "is", null);
  }
  if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {
    errorQuery = errorQuery.where((eb) => eb.or([
      eb("organization_id", "is", null),
      eb("organization_id", "in", organizationScopeIds)
    ]));
  } else {
    errorQuery = errorQuery.where("organization_id", "is", null);
  }
  const errorRows = await errorQuery.orderBy("occurred_at", "desc").limit(100).execute();
  const errors = errorRows.map((row) => {
    const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null;
    return {
      id: String(row.id),
      source: String(row.source ?? ""),
      handler: String(row.handler ?? ""),
      entityType: row.entity_type ?? null,
      recordId: row.record_id ?? null,
      tenantId: row.tenant_id ?? null,
      organizationId: row.organization_id ?? null,
      message: String(row.message ?? ""),
      stack: row.stack ?? null,
      payload: row.payload ?? null,
      occurredAt: occurredAt ? occurredAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
    };
  });
  let logsQuery = db.selectFrom("indexer_status_logs").selectAll();
  if (tenantId != null) {
    logsQuery = logsQuery.where((eb) => eb.or([
      eb("tenant_id", "=", tenantId),
      eb("tenant_id", "is", null)
    ]));
  } else {
    logsQuery = logsQuery.where("tenant_id", "is", null);
  }
  if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {
    logsQuery = logsQuery.where((eb) => eb.or([
      eb("organization_id", "is", null),
      eb("organization_id", "in", organizationScopeIds)
    ]));
  } else {
    logsQuery = logsQuery.where("organization_id", "is", null);
  }
  const logRows = await logsQuery.orderBy("occurred_at", "desc").limit(100).execute();
  const logs = logRows.map((row) => {
    const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null;
    const level = row.level === "warn" ? "warn" : "info";
    return {
      id: String(row.id),
      source: String(row.source ?? ""),
      handler: String(row.handler ?? ""),
      level,
      entityType: row.entity_type ?? null,
      recordId: row.record_id ?? null,
      tenantId: row.tenant_id ?? null,
      organizationId: row.organization_id ?? null,
      message: String(row.message ?? ""),
      details: row.details ?? null,
      occurredAt: occurredAt ? occurredAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
    };
  });
  const response = NextResponse.json({ items, errors, logs });
  const partial = items.find((item) => {
    if (item.baseCount == null && item.indexCount == null) return false;
    if (item.baseCount == null || item.indexCount == null) return true;
    return item.baseCount !== item.indexCount;
  });
  if (partial) {
    response.headers.set(
      "x-om-partial-index",
      JSON.stringify({
        type: "partial_index",
        entity: partial.entityId,
        entityLabel: partial.label ?? partial.entityId,
        baseCount: partial.baseCount,
        indexedCount: partial.indexCount,
        scope: organizationId
      })
    );
  }
  return response;
}
const queryIndexStatusDoc = {
  summary: "Inspect query index coverage",
  description: "Returns entity counts comparing base tables with the query index along with the latest job status.",
  tags: [queryIndexTag],
  responses: [
    { status: 200, description: "Current query index status.", schema: queryIndexStatusResponseSchema }
  ],
  errors: [
    { status: 400, description: "Tenant or organization context required", schema: queryIndexErrorSchema },
    { status: 401, description: "Authentication required", schema: queryIndexErrorSchema }
  ]
};
const openApi = {
  tag: queryIndexTag,
  summary: "Query index status",
  methods: {
    GET: queryIndexStatusDoc
  }
};
export {
  GET,
  metadata,
  openApi
};
//# sourceMappingURL=status.js.map
