import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import type { Adapter, FetchLike } from "../adapter";
import type { AdapterResult } from "../schema";
import { defaultLedgerDir, readLedger, type LedgerReadResult, type SessionRecord } from "../sessions/ledger";
import { defaultKubernetesActivityPath } from "../activity/sources/kubernetes";
import { lifecycleTransitions, type KubernetesLifecycleObject } from "../activity/kubernetes-lifecycle";

export const KUBERNETES_ADAPTER_ID = "kubernetes";
export const CLUSTER_PANEL_ID = "cluster:k3s";
export const BUILD_KEY_ANNOTATION = "overdeck.dev/build-key";
export const SESSION_ID_ANNOTATION = "overdeck.dev/session-id";

export type SourceStatus = "present" | "partial" | "absent";
export interface SourceCoverage {
  status: SourceStatus;
  observedAt?: string;
  coverageFrom?: string;
  error?: string;
  scopes?: { complete: string[]; incomplete: string[] };
}
export interface Evidence { source: "kubernetes" | "builds" | "ledger"; field: string; value: string }
export interface CoverageGap { source: string; reason: string; scope?: string }
export interface ResourceValues { cpuCores: number | null; memoryBytes: number | null }
export interface ContainerPlacement {
  name: string;
  state: string | null;
  reason: string | null;
  restartCount: number | null;
  requests: ResourceValues;
  limits: ResourceValues;
}
export interface WorkloadPlacement {
  cluster: string;
  namespace: string;
  kind: "Job" | "Pod";
  name: string;
  uid: string;
  podUid: string | null;
  podName: string | null;
  nodeName: string | null;
  phase: string | null;
  reason: string | null;
  createdAt: string | null;
  startedAt: string | null;
  finishedAt: string | null;
  containers: ContainerPlacement[];
  requests: ResourceValues;
  limits: ResourceValues;
  utilization: ResourceValues;
  ownerReference: { kind: string; name: string; uid: string } | null;
  annotations: { buildKey: string | null; podBuildKey: string | null; jobBuildKey: string | null; sessionId: string | null; conflict: boolean };
  buildKey: string | null;
  sessionId: string | null;
  evidence: Evidence[];
  historical: boolean;
}
export interface SessionPlacement {
  sessionId: string;
  buildKey?: string;
  nodeName?: string;
  workloadUid?: string;
  confidence: "exact" | "host-only" | "unplaced";
  evidence: Evidence[];
  gaps: CoverageGap[];
  session: SessionRecord;
}
export interface BuildPlacement {
  buildKey: string;
  nodeName: string | null;
  workloadUid: string | null;
  sessionId: string | null;
  confidence: "exact" | "unplaced";
  evidence: Evidence[];
  gaps: CoverageGap[];
  historical: boolean;
}
export interface ClusterNode {
  name: string;
  uid: string;
  ready: boolean | null;
  conditions: Array<{ type: string; status: string; reason: string | null; observedAt: string | null }>;
  taints: Array<{ key: string; value: string | null; effect: string }>;
  capacity: ResourceValues;
  allocatable: ResourceValues;
  utilization: ResourceValues;
  requests: ResourceValues;
  limits: ResourceValues;
  workloadCounts: { running: number; pending: number; failed: number };
  restartCount: number | null;
  sessions: SessionPlacement[];
  builds: BuildPlacement[];
  workloads: WorkloadPlacement[];
  sourceTimestamps: Record<string, string | null>;
  coverageGaps: CoverageGap[];
}
export interface ClusterSnapshot {
  observedAt: string;
  cluster: { id: string; version?: string; state: "reachable" | "degraded" };
  sources: { kubernetes: SourceCoverage; prometheus: SourceCoverage; ledger: SourceCoverage; builds: SourceCoverage };
  summary: {
    readyNodes: number | null; totalNodes: number | null;
    runningWorkloads: number | null; pendingWorkloads: number | null; failedWorkloads: number | null;
    requestedCpuCores: number | null; requestedMemoryBytes: number | null;
    usedCpuCores: number | null; usedMemoryBytes: number | null;
  };
  nodes: ClusterNode[];
  unplacedSessions: SessionPlacement[];
  unmatchedWorkloads: WorkloadPlacement[];
  unplacedBuilds: BuildPlacement[];
}

type KubeObject = Record<string, any>;
interface KubeList { items?: KubeObject[]; metadata?: { continue?: string } }
interface BuildRecord { buildKey: string; sessionId?: string | null; workloadUid?: string | null }
interface ScopeResult { namespace: string; pods?: KubeObject[]; jobs?: KubeObject[]; error?: string }

export interface KubernetesAdapterOptions {
  id?: string;
  interval?: number;
  clusterId?: string;
  kubeconfigPath?: string;
  namespaces?: string[];
  prometheusUrl?: string;
  fetchImpl?: FetchLike;
  readFileImpl?: (path: string) => string;
  readLedgerImpl?: (dir: string) => LedgerReadResult | Promise<LedgerReadResult>;
  ledgerDir?: string;
  readBuildsImpl?: () => BuildRecord[] | Promise<BuildRecord[]>;
  now?: () => number;
  activityPath?: string;
  appendActivityImpl?: (path: string, events: string) => void;
}

function iso(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const ms = Date.parse(value);
  return Number.isNaN(ms) ? null : new Date(ms).toISOString();
}
function cpu(value: unknown): number | null {
  if (typeof value !== "string") return null;
  if (/^\d+m$/.test(value)) return Number(value.slice(0, -1)) / 1000;
  if (/^\d+(?:\.\d+)?$/.test(value)) return Number(value);
  if (/^\d+n$/.test(value)) return Number(value.slice(0, -1)) / 1e9;
  return null;
}
function bytes(value: unknown): number | null {
  if (typeof value !== "string") return null;
  const match = /^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|K|M|G|T)?$/.exec(value);
  if (!match) return null;
  const powers: Record<string, number> = { Ki: 1024, Mi: 1024 ** 2, Gi: 1024 ** 3, Ti: 1024 ** 4, K: 1000, M: 1e6, G: 1e9, T: 1e12 };
  return Number(match[1]) * (powers[match[2] ?? ""] ?? 1);
}
function resources(raw: any): ResourceValues {
  return { cpuCores: cpu(raw?.cpu), memoryBytes: bytes(raw?.memory) };
}
function sumResources(values: ResourceValues[]): ResourceValues {
  const sum = (key: keyof ResourceValues) => values.some((v) => v[key] !== null)
    ? values.reduce((total, v) => total + (v[key] ?? 0), 0) : null;
  return { cpuCores: sum("cpuCores"), memoryBytes: sum("memoryBytes") };
}
function containerState(status: any): { state: string | null; reason: string | null; startedAt: string | null; finishedAt: string | null } {
  for (const state of ["running", "waiting", "terminated"]) {
    if (status?.state?.[state]) return {
      state, reason: status.state[state].reason ?? null,
      startedAt: iso(status.state[state].startedAt), finishedAt: iso(status.state[state].finishedAt),
    };
  }
  return { state: null, reason: null, startedAt: null, finishedAt: null };
}

function normalizePod(pod: KubeObject, cluster: string, jobsByUid: Map<string, KubeObject>): WorkloadPlacement | null {
  const uid = pod.metadata?.uid;
  const name = pod.metadata?.name;
  const namespace = pod.metadata?.namespace;
  if (!uid || !name || !namespace) return null;
  const owner = pod.metadata?.ownerReferences?.find((entry: any) => entry.controller === true)
    ?? pod.metadata?.ownerReferences?.[0];
  const job = owner?.kind === "Job" ? jobsByUid.get(owner.uid) : undefined;
  const podKey = pod.metadata?.annotations?.[BUILD_KEY_ANNOTATION] ?? null;
  const jobKey = job?.metadata?.annotations?.[BUILD_KEY_ANNOTATION] ?? null;
  const annotationConflict = Boolean(podKey && jobKey && podKey !== jobKey);
  const buildKey = annotationConflict ? null : podKey ?? jobKey;
  const sessionId = pod.metadata?.annotations?.[SESSION_ID_ANNOTATION]
    ?? job?.metadata?.annotations?.[SESSION_ID_ANNOTATION] ?? null;
  const specs = pod.spec?.containers ?? [];
  const statuses = new Map((pod.status?.containerStatuses ?? []).map((s: any) => [s.name, s]));
  const containers: ContainerPlacement[] = specs.map((spec: any): ContainerPlacement => {
    const status: any = statuses.get(spec.name);
    const state = containerState(status);
    return { name: spec.name, state: state.state, reason: state.reason, restartCount: Number.isInteger(status?.restartCount) ? status.restartCount : null, requests: resources(spec.resources?.requests), limits: resources(spec.resources?.limits) };
  });
  const states = [...statuses.values()].map(containerState);
  const evidence: Evidence[] = [];
  if (pod.spec?.nodeName) evidence.push({ source: "kubernetes", field: "pod.spec.nodeName", value: pod.spec.nodeName });
  if (owner?.uid) evidence.push({ source: "kubernetes", field: "pod.metadata.ownerReferences.controller.uid", value: owner.uid });
  if (podKey) evidence.push({ source: "kubernetes", field: `pod.metadata.annotations[${BUILD_KEY_ANNOTATION}]`, value: podKey });
  if (jobKey) evidence.push({ source: "kubernetes", field: `job.metadata.annotations[${BUILD_KEY_ANNOTATION}]`, value: jobKey });
  if (sessionId) evidence.push({ source: "kubernetes", field: `metadata.annotations[${SESSION_ID_ANNOTATION}]`, value: sessionId });
  return {
    cluster, namespace, kind: "Pod", name, uid, podUid: uid, podName: name,
    nodeName: pod.spec?.nodeName ?? null, phase: pod.status?.phase ?? null, reason: pod.status?.reason ?? null,
    createdAt: iso(pod.metadata?.creationTimestamp),
    startedAt: iso(pod.status?.startTime) ?? states.map((s) => s.startedAt).find(Boolean) ?? null,
    finishedAt: states.map((s) => s.finishedAt).filter(Boolean).sort().at(-1) ?? null,
    containers, requests: sumResources(containers.map((c) => c.requests)), limits: sumResources(containers.map((c) => c.limits)),
    utilization: { cpuCores: null, memoryBytes: null },
    ownerReference: owner?.uid ? { kind: owner.kind ?? "unknown", name: owner.name ?? "unknown", uid: owner.uid } : null,
    annotations: { buildKey, podBuildKey: podKey, jobBuildKey: jobKey, sessionId, conflict: annotationConflict }, buildKey, sessionId, evidence, historical: false,
  };
}

function normalizeJob(job: KubeObject, cluster: string): WorkloadPlacement | null {
  const { uid, name, namespace, creationTimestamp, annotations = {} } = job.metadata ?? {};
  if (!uid || !name || !namespace) return null;
  const buildKey = annotations[BUILD_KEY_ANNOTATION] ?? null;
  const sessionId = annotations[SESSION_ID_ANNOTATION] ?? null;
  const evidence: Evidence[] = [];
  if (buildKey) evidence.push({ source: "kubernetes", field: `job.metadata.annotations[${BUILD_KEY_ANNOTATION}]`, value: buildKey });
  return { cluster, namespace, kind: "Job", name, uid, podUid: null, podName: null, nodeName: null,
    phase: job.status?.failed ? "Failed" : job.status?.succeeded ? "Succeeded" : job.status?.active ? "Running" : null,
    reason: null, createdAt: iso(creationTimestamp), startedAt: iso(job.status?.startTime), finishedAt: iso(job.status?.completionTime),
    containers: [], requests: { cpuCores: null, memoryBytes: null }, limits: { cpuCores: null, memoryBytes: null },
    utilization: { cpuCores: null, memoryBytes: null }, ownerReference: null,
    annotations: { buildKey, podBuildKey: null, jobBuildKey: buildKey, sessionId, conflict: false }, buildKey, sessionId, evidence, historical: false };
}

function conflict(values: Array<string | null | undefined>): boolean {
  return new Set(values.filter((v): v is string => Boolean(v))).size > 1;
}

export function projectCluster(input: {
  clusterId: string; observedAt: string; nodes: KubeObject[]; scopes: ScopeResult[];
  ledger: LedgerReadResult; builds: BuildRecord[]; prometheus: SourceCoverage;
}): ClusterSnapshot {
  const complete = input.scopes.filter((s) => !s.error).map((s) => s.namespace);
  const incomplete = input.scopes.filter((s) => s.error).map((s) => s.namespace);
  const jobs = input.scopes.flatMap((s) => s.jobs ?? []);
  const jobsByUid = new Map(jobs.map((job) => [job.metadata?.uid, job]));
  const pods = input.scopes.flatMap((s) => s.pods ?? []).map((p) => normalizePod(p, input.clusterId, jobsByUid)).filter((p): p is WorkloadPlacement => p !== null);
  const jobWorkloads = jobs.map((j) => normalizeJob(j, input.clusterId)).filter((j): j is WorkloadPlacement => j !== null);
  const relevantPods = pods.filter((p) => p.buildKey || p.sessionId || p.annotations.conflict);
  const buildRecords = new Map(input.builds.map((b) => [b.buildKey, b]));
  const sessionsById = new Map(input.ledger.sessions.map((s) => [s.id, s]));
  const podByBuild = new Map<string, WorkloadPlacement[]>();
  for (const pod of relevantPods) if (pod.buildKey) podByBuild.set(pod.buildKey, [...(podByBuild.get(pod.buildKey) ?? []), pod]);
  const allKeys = new Set([...podByBuild.keys(), ...buildRecords.keys(), ...input.ledger.sessions.map((s) => s.buildKey).filter((v): v is string => Boolean(v))]);
  const builds: BuildPlacement[] = [...allKeys].sort().map((buildKey) => {
    const candidates = podByBuild.get(buildKey) ?? [];
    const record = buildRecords.get(buildKey);
    const sessionCandidates = input.ledger.sessions.filter((s) => s.buildKey === buildKey || (record?.sessionId && s.id === record.sessionId));
    const nodes = new Set(candidates.map((p) => p.nodeName).filter(Boolean));
    const conflictJoin = nodes.size > 1 || sessionCandidates.length > 1 || conflict([record?.workloadUid, ...candidates.map((p) => p.uid)]);
    const pod = !conflictJoin && candidates.length === 1 ? candidates[0]! : null;
    const session = !conflictJoin && sessionCandidates.length === 1 ? sessionCandidates[0]! : null;
    const evidence: Evidence[] = [...(pod?.evidence ?? [])];
    if (record) evidence.push({ source: "builds", field: "build.buildKey", value: buildKey });
    if (session?.buildKey) evidence.push({ source: "ledger", field: "session.buildKey", value: session.buildKey });
    return { buildKey, nodeName: pod?.nodeName ?? null, workloadUid: pod?.uid ?? null, sessionId: session?.id ?? null,
      confidence: pod?.nodeName && (record || session) ? "exact" : "unplaced", evidence,
      gaps: conflictJoin ? [{ source: "correlation", reason: "conflicting exact evidence" }] : pod?.nodeName ? [] : [{ source: "kubernetes", reason: "no uniquely placed annotated Pod" }], historical: false };
  });
  const placements: SessionPlacement[] = input.ledger.sessions.map((session) => {
    const direct = relevantPods.filter((p) => p.sessionId === session.id || (session.workloadUid && p.uid === session.workloadUid));
    const viaBuild = session.buildKey ? relevantPods.filter((p) => p.buildKey === session.buildKey) : [];
    const exact = [...new Map([...direct, ...viaBuild].map((p) => [p.uid, p])).values()];
    const nodeNames = new Set(exact.map((p) => p.nodeName).filter((n): n is string => Boolean(n)));
    const contradicted = nodeNames.size > 1 || conflict([session.workloadUid, ...exact.map((p) => p.uid)]);
    if (!contradicted && exact.length === 1 && exact[0]!.nodeName) {
      const pod = exact[0]!;
      return { sessionId: session.id, ...(session.buildKey ? { buildKey: session.buildKey } : {}), nodeName: pod.nodeName!, workloadUid: pod.uid,
        confidence: "exact", evidence: [...pod.evidence, { source: "ledger", field: session.buildKey ? "session.buildKey" : "session.workloadUid", value: session.buildKey ?? session.workloadUid! }], gaps: [], session };
    }
    const nodeExists = input.nodes.some((n) => n.metadata?.name === session.host);
    if (!contradicted && exact.length === 0 && session.host && nodeExists) return { sessionId: session.id, nodeName: session.host,
      confidence: "host-only", evidence: [{ source: "ledger", field: "session.host", value: session.host }], gaps: [{ source: "correlation", reason: "no explicit workload/build key chain" }], session };
    return { sessionId: session.id, ...(session.buildKey ? { buildKey: session.buildKey } : {}), confidence: "unplaced", evidence: exact.flatMap((p) => p.evidence),
      gaps: [{ source: "correlation", reason: contradicted ? "conflicting exact evidence" : "no explicit node evidence" }], session };
  });
  const nodes: ClusterNode[] = input.nodes.flatMap((node): ClusterNode[] => {
    const name = node.metadata?.name; const uid = node.metadata?.uid;
    if (!name || !uid) return [];
    const nodePods = pods.filter((p) => p.nodeName === name);
    const conditions: Array<{ type: string; status: string; reason: string | null; observedAt: string | null }> = (node.status?.conditions ?? []).map((c: any) => ({ type: c.type, status: c.status, reason: c.reason ?? null, observedAt: iso(c.lastTransitionTime) }));
    const counts = { running: nodePods.filter((p) => p.phase === "Running").length, pending: nodePods.filter((p) => p.phase === "Pending").length, failed: nodePods.filter((p) => p.phase === "Failed").length };
    return [{ name, uid, ready: conditions.find((c) => c.type === "Ready")?.status === "True" ? true : conditions.some((c) => c.type === "Ready") ? false : null,
      conditions, taints: (node.spec?.taints ?? []).map((t: any) => ({ key: t.key, value: t.value ?? null, effect: t.effect })), capacity: resources(node.status?.capacity), allocatable: resources(node.status?.allocatable),
      utilization: { cpuCores: null, memoryBytes: null }, requests: sumResources(nodePods.map((p) => p.requests)), limits: sumResources(nodePods.map((p) => p.limits)), workloadCounts: counts,
      restartCount: nodePods.some((p) => p.containers.some((c) => c.restartCount !== null)) ? nodePods.flatMap((p) => p.containers).reduce((n, c) => n + (c.restartCount ?? 0), 0) : null,
      sessions: placements.filter((p) => p.nodeName === name), builds: builds.filter((b) => b.nodeName === name), workloads: nodePods,
      sourceTimestamps: { kubernetes: input.observedAt, prometheus: input.prometheus.observedAt ?? null },
      coverageGaps: [...(input.prometheus.status === "present" ? [] : [{ source: "prometheus", reason: input.prometheus.error ?? "source absent" }]), ...incomplete.map((scope) => ({ source: "kubernetes", reason: "namespace scope incomplete", scope }))] }];
  }).sort((a, b) => a.name.localeCompare(b.name));
  const counts = nodes.reduce((out, n) => ({ running: out.running + n.workloadCounts.running, pending: out.pending + n.workloadCounts.pending, failed: out.failed + n.workloadCounts.failed }), { running: 0, pending: 0, failed: 0 });
  const kubernetes: SourceCoverage = { status: incomplete.length ? "partial" : "present", observedAt: input.observedAt, scopes: { complete, incomplete }, ...(incomplete.length ? { error: input.scopes.filter((s) => s.error).map((s) => `${s.namespace}: ${s.error}`).join("; ") } : {}) };
  const ledger: SourceCoverage = input.ledger.missing ? { status: "absent", error: "session ledger unavailable" } : { status: input.ledger.unreadable.length ? "partial" : "present", observedAt: input.observedAt, ...(input.ledger.unreadable.length ? { error: `unreadable entries: ${input.ledger.unreadable.join(", ")}` } : {}) };
  return { observedAt: input.observedAt, cluster: { id: input.clusterId, state: incomplete.length ? "degraded" : "reachable" }, sources: { kubernetes, prometheus: input.prometheus, ledger, builds: { status: input.builds.length ? "present" : "absent", ...(input.builds.length ? { observedAt: input.observedAt } : { error: "build source has no records" }) } },
    summary: { readyNodes: nodes.filter((n) => n.ready).length, totalNodes: nodes.length, runningWorkloads: counts.running, pendingWorkloads: counts.pending, failedWorkloads: counts.failed,
      requestedCpuCores: sumResources(nodes.map((n) => n.requests)).cpuCores, requestedMemoryBytes: sumResources(nodes.map((n) => n.requests)).memoryBytes,
      usedCpuCores: null, usedMemoryBytes: null }, nodes,
    unplacedSessions: placements.filter((p) => p.confidence === "unplaced"), unmatchedWorkloads: relevantPods.filter((p) => !placements.some((s) => s.workloadUid === p.uid) && !builds.some((b) => b.workloadUid === p.uid)), unplacedBuilds: builds.filter((b) => b.confidence === "unplaced") };
}

interface KubeClient { server: string; headers: Record<string, string>; tls: Record<string, string | undefined> }
export function loadKubeClient(path: string, readFile: (path: string) => string = (p) => readFileSync(p, "utf8")): KubeClient {
  const raw = Bun.YAML.parse(readFile(path)) as any;
  const context = raw.contexts?.find((c: any) => c.name === raw["current-context"])?.context;
  const cluster = raw.clusters?.find((c: any) => c.name === context?.cluster)?.cluster;
  const user = raw.users?.find((u: any) => u.name === context?.user)?.user;
  if (!cluster?.server || !user) throw new Error("kubeconfig is missing its current cluster or user");
  const decode = (value?: string) => value ? Buffer.from(value, "base64").toString("utf8") : undefined;
  const headers: Record<string, string> = {};
  if (user.token) headers.authorization = `Bearer ${user.token}`;
  return { server: cluster.server.replace(/\/$/, ""), headers, tls: { ca: decode(cluster["certificate-authority-data"]), cert: decode(user["client-certificate-data"]), key: decode(user["client-key-data"]) } };
}

export function createKubernetesAdapter(options: KubernetesAdapterOptions = {}): Adapter {
  const id = options.id ?? KUBERNETES_ADAPTER_ID;
  const interval = options.interval ?? 15_000;
  const kubeconfigPath = options.kubeconfigPath ?? join(homedir(), ".kube", "config-overdeck-observer");
  const namespaces = options.namespaces?.length ? [...new Set(options.namespaces)] : ["default"];
  const fetchImpl = options.fetchImpl ?? fetch;
  const now = options.now ?? Date.now;
  const activityPath = options.activityPath ?? defaultKubernetesActivityPath();
  const appendActivity = options.appendActivityImpl ?? ((path: string, body: string) => { mkdirSync(dirname(path), { recursive: true }); appendFileSync(path, body); });
  let priorLifecycle = new Map<string, KubernetesLifecycleObject>();
  async function get(client: KubeClient, path: string): Promise<KubeList> {
    const response = await fetchImpl(`${client.server}${path}`, { headers: client.headers, tls: client.tls, signal: AbortSignal.timeout(8_000) } as RequestInit);
    if (!response.ok) throw new Error(`Kubernetes ${path} returned ${response.status}`);
    return response.json() as Promise<KubeList>;
  }
  async function prometheusCoverage(at: string): Promise<SourceCoverage> {
    const base = options.prometheusUrl ?? "http://127.0.0.1:9090";
    try {
      const response = await fetchImpl(`${base}/api/v1/query?query=${encodeURIComponent("up{job=~\"kube-state-metrics|cadvisor\"}")}`);
      if (!response.ok) throw new Error(`Prometheus returned ${response.status}`);
      const body = await response.json() as any;
      const metrics = body?.data?.result ?? [];
      if (!metrics.length) return { status: "absent", error: "kube-state-metrics and cAdvisor targets have no samples" };
      return { status: metrics.some((m: any) => m.value?.[1] !== "1") ? "partial" : "present", observedAt: at };
    } catch (error) { return { status: "absent", error: String(error) }; }
  }
  return { id, interval, async poll(): Promise<AdapterResult> {
    const observedAt = new Date(now()).toISOString();
    const client = loadKubeClient(kubeconfigPath, options.readFileImpl);
    const nodes = (await get(client, "/api/v1/nodes")).items ?? [];
    const scopes = await Promise.all(namespaces.map(async (namespace): Promise<ScopeResult> => {
      const encoded = encodeURIComponent(namespace);
      try {
        const [pods, jobs] = await Promise.all([get(client, `/api/v1/namespaces/${encoded}/pods`), get(client, `/apis/batch/v1/namespaces/${encoded}/jobs`)]);
        return { namespace, pods: pods.items ?? [], jobs: jobs.items ?? [] };
      } catch (error) { return { namespace, error: String(error) }; }
    }));
    const ledger = await (options.readLedgerImpl ?? readLedger)(options.ledgerDir ?? defaultLedgerDir());
    const builds = await (options.readBuildsImpl?.() ?? []);
    const prometheus = await prometheusCoverage(observedAt);
    const snapshot = projectCluster({ clusterId: options.clusterId ?? "k3s", observedAt, nodes, scopes, ledger, builds, prometheus });
    const rawObjects = scopes.flatMap((scope) => [...(scope.pods ?? []).map((raw) => ({ raw, kind: "Pod" as const })), ...(scope.jobs ?? []).map((raw) => ({ raw, kind: "Job" as const }))]);
    const lifecycle = rawObjects.flatMap(({ raw, kind }): KubernetesLifecycleObject[] => {
      const metadata = raw.metadata ?? {}; if (!metadata.uid || !metadata.name || !metadata.namespace || !metadata.resourceVersion) return [];
      const annotations = metadata.annotations ?? {}; const statuses = raw.status?.containerStatuses ?? [];
      const finishedAt = kind === "Job" ? iso(raw.status?.completionTime) : statuses.map((status: any) => iso(status.state?.terminated?.finishedAt)).filter(Boolean).sort().at(-1);
      return [{ uid: metadata.uid, resourceVersion: metadata.resourceVersion, kind, namespace: metadata.namespace, name: metadata.name,
        ...(raw.spec?.nodeName ? { nodeName: raw.spec.nodeName } : {}), ...(annotations[BUILD_KEY_ANNOTATION] ? { buildKey: annotations[BUILD_KEY_ANNOTATION] } : {}),
        ...(annotations[SESSION_ID_ANNOTATION] ? { sessionId: annotations[SESSION_ID_ANNOTATION] } : {}), ...(iso(metadata.creationTimestamp) ? { createdAt: iso(metadata.creationTimestamp)! } : {}),
        ...(iso(raw.status?.startTime) ? { startedAt: iso(raw.status.startTime)! } : {}), ...(raw.status?.phase ? { phase: raw.status.phase } : kind === "Job" && raw.status?.succeeded ? { phase: "Succeeded" } : kind === "Job" && raw.status?.failed ? { phase: "Failed" } : {}),
        ...(finishedAt ? { finishedAt } : {}), ...(raw.status?.reason ? { reason: raw.status.reason } : {}) }];
    });
    const incompleteNamespaces = new Set(scopes.filter((scope) => scope.error).map((scope) => scope.namespace));
    const reconciledLifecycle = [...lifecycle, ...[...priorLifecycle.values()].filter((object) => incompleteNamespaces.has(object.namespace) && !lifecycle.some((entry) => entry.uid === object.uid))];
    const transitions = lifecycleTransitions(priorLifecycle, reconciledLifecycle, observedAt);
    if (transitions.length) appendActivity(activityPath, `${transitions.map((entry) => JSON.stringify(entry)).join("\n")}\n`);
    priorLifecycle = new Map(reconciledLifecycle.map((object) => [object.uid, object]));
    return { items: [], panels: [{ id: CLUSTER_PANEL_ID, ts: observedAt, data: snapshot }], completeScopes: scopes.filter((s) => !s.error).map((s) => `namespace:${s.namespace}`) };
  } };
}
