import { z } from "zod";
import type { ConfigLoadResult } from "./config";
import type { ControllerStore } from "./store";

export const HostStateSchema = z.enum([
  "available",
  "draining",
  "maintenance",
  "restoring",
  "degraded",
]);
export type HostState = z.infer<typeof HostStateSchema>;

const CapabilityProbeSchema = z.object({
  name: z.string(),
  ok: z.boolean(),
  version: z.string().optional(),
  // Why this probe is not ok. A red probe without one is the silent-red defect.
  detail: z.string().optional(),
  checkedAt: z.string().optional(),
}).strict();

const HostCapabilitySchema = z.object({
  probes: z.array(CapabilityProbeSchema),
  circuitOpen: z.boolean().optional(),
  missingCommand: z.string().optional(),
}).strict();

const HostStatusSchema = z.object({
  state: HostStateSchema,
  role: z.enum(["builder", "workstation"]),
  primary: z.boolean().optional(),
  enrolling: z.boolean().optional(),
  capability: HostCapabilitySchema.optional(),
}).strict();

const QueueOwnerSchema = z.object({
  pid: z.number().int(),
  starttime: z.number().int(),
  label: z.string().optional(),
}).strict();

const QueueTicketSchema = z.object({
  position: z.number().int(),
  key: z.string(),
  repo: z.string(),
  owner: QueueOwnerSchema,
  enqueueAgeSeconds: z.number(),
  state: z.string(),
  dispatchTarget: z.string().optional(),
}).strict();

const QueueStatusSchema = z.object({
  depth: z.number().int(),
  oldestAgeSeconds: z.number(),
  p95AgeSeconds: z.number(),
  sloSeconds: z.number(),
  dispatchTarget: z.string().optional(),
  spill: z
    .object({
      active: z.boolean(),
      stale: z.boolean().optional(),
    }).strict()
    .optional(),
  tickets: z.array(QueueTicketSchema),
  wedge: z
    .object({
      host: z.string(),
      detail: z.string(),
    }).strict()
    .optional(),
}).strict();

const PublicationStateSchema = z.object({
  state: z.enum(["none", "staged", "promoted", "blocked", "discarded"]),
  reason: z.string().optional(),
}).strict();

const RemoteJobSchema = z.object({
  id: z.string(),
  repo: z.string(),
  snapshot: z.string(),
  stage: z.string(),
  host: z.string(),
  rc: z.number().int().nullable().optional(),
  pullBytes: z.number().int().nullable().optional(),
  pullDurationSeconds: z.number().nullable().optional(),
  publication: PublicationStateSchema.optional(),
}).strict();

const OffloadKpiSamplesSchema = z.object({
  remote24h: z.number().int().nonnegative(),
  remoteSuccessPct: z.number().min(0).max(100).optional(),
  exit127: z.object({
    count: z.number().int().nonnegative(),
    hosts: z.array(z.string()),
  }).strict(),
}).strict();

export const ControllerStatusSchema = z.object({
  desired: HostStateSchema,
  observed: HostStateSchema,
  revision: z.number().int(),
  lease: z.object({
    active: z.boolean(),
    expired: z.boolean().optional(),
    host: z.string().nullable().optional(),
    expiresAt: z.string().nullable().optional(),
    reason: z.string().nullable().optional(),
  }).strict(),
  capacity: z.object({
    builders: z.number().int(),
    idleSlots: z.number().int(),
    overloaded: z.boolean().optional(),
  }).strict(),
  reconciler: z.object({
    healthy: z.boolean(),
    lastAt: z.string().optional(),
  }).strict(),
  dispatch: z.object({
    state: z.enum(["healthy", "wedged", "paused"]),
    detail: z.string().optional(),
    host: z.string().optional(),
  }).strict(),
  hosts: z.record(HostStatusSchema),
  landConduct: z.record(z.object({
    lastPassAt: z.string().nullable(),
    lastOk: z.boolean(),
    lastDetail: z.string(),
    consecutiveFailures: z.number().int().nonnegative(),
  }).strict()),
  deployWatcher: z.object({
    targetSha: z.string(),
    attempts: z.number().int().nonnegative(),
    lastStatus: z.string(),
    lastDetail: z.string(),
    lastAt: z.string(),
    lastOk: z.boolean(),
    failureClass: z.enum(["none", "transient", "permanent"]),
    nextRetryAt: z.string().nullable(),
  }).strict().nullable(),
  queue: QueueStatusSchema,
  jobs: z.array(RemoteJobSchema),
  kpis: OffloadKpiSamplesSchema,
}).strict();
export type ControllerStatus = z.infer<typeof ControllerStatusSchema>;

const DEFAULT_QUEUE_SLO_SECONDS = 300;

export function emptyControllerStatus(): ControllerStatus {
  return {
    desired: "available",
    observed: "available",
    revision: 0,
    lease: { active: false },
    capacity: { builders: 0, idleSlots: 0 },
    reconciler: { healthy: true },
    dispatch: { state: "healthy" },
    hosts: {},
    landConduct: {},
    deployWatcher: null,
    queue: {
      depth: 0,
      oldestAgeSeconds: 0,
      p95AgeSeconds: 0,
      sloSeconds: DEFAULT_QUEUE_SLO_SECONDS,
      tickets: [],
    },
    jobs: [],
    kpis: { remote24h: 0, exit127: { count: 0, hosts: [] } },
  };
}

const MAX_STATUS_JOBS = 100;

export function buildControllerStatus(
  store: ControllerStore,
  configHealth?: ConfigLoadResult,
  now: () => number = () => Date.now(),
): ControllerStatus {
  const meta = store.getMetaSnapshot();
  const lease = store.getLease();
  const expired =
    lease.expiresAt !== null && Date.parse(lease.expiresAt) <= now();
  const capacity = store.countBuilderCapacity();
  const hosts = store.listHosts();
  const activeTickets = store
    .listQueueTickets()
    .filter((ticket) => !["completed", "failed", "blocked", "discarded", "cancelled"].includes(ticket.state));
  const queuedTickets = activeTickets.filter((ticket) => ticket.state === "queued");
  const dispatchTarget = activeTickets.find((ticket) => ticket.dispatchTarget)
    ?.dispatchTarget;
  const ages = queuedTickets.map((t) => t.enqueueAgeSeconds);
  const oldestAgeSeconds = ages.length > 0 ? Math.max(...ages) : 0;
  const p95AgeSeconds =
    ages.length > 0
      ? ages.slice().sort((a, b) => a - b)[Math.floor(ages.length * 0.95)] ?? 0
      : 0;

  const desired = configHealth?.degraded ? "degraded" : meta.desired;
  const observed = configHealth?.degraded ? "degraded" : meta.observed;

  const hostMap: ControllerStatus["hosts"] = {};
  for (const host of hosts) {
    hostMap[host.hostname] = {
      state: host.state,
      role: host.role,
      ...(host.primary ? { primary: true } : {}),
      ...(host.enrolling ? { enrolling: true } : {}),
      capability: {
        probes: [
          {
            name: "storage",
            ok: host.healthStorage,
          },
          {
            name: "runner",
            ok: host.healthRunner,
          },
          {
            name: "offload",
            ok: host.healthOffload,
          },
          {
            name: "capability",
            ok: host.capabilityOk,
            ...(host.capabilityReason ? { detail: host.capabilityReason } : {}),
            ...(host.capabilityCheckedAt ? { checkedAt: host.capabilityCheckedAt } : {}),
          },
        ],
        ...(host.quarantinedCommands.length > 0
          ? {
              circuitOpen: true,
              missingCommand: host.quarantinedCommands[0],
            }
          : {}),
      },
    };
  }

  const landConduct: ControllerStatus["landConduct"] = {};
  for (const health of store.listLandConductHealth()) {
    landConduct[health.root] = {
      lastPassAt: health.lastPassAt,
      lastOk: health.lastOk,
      lastDetail: health.lastDetail,
      consecutiveFailures: health.consecutiveFailures,
    };
  }

  const deployWatcherState = store.getDeployWatcherState();
  const deployWatcher: ControllerStatus["deployWatcher"] = deployWatcherState && {
    targetSha: deployWatcherState.targetSha,
    attempts: deployWatcherState.attempts,
    lastStatus: deployWatcherState.lastStatus,
    lastDetail: deployWatcherState.lastDetail,
    lastAt: deployWatcherState.lastAt,
    lastOk: deployWatcherState.lastOk,
    failureClass: deployWatcherState.failureClass,
    nextRetryAt: deployWatcherState.nextRetryAt,
  };

  const recordedJobs = store.listJobs();
  const activeJobs = recordedJobs.filter((job) => !job.finishedAt);
  const recentFinishedJobs = recordedJobs
    .filter((job) => job.finishedAt)
    .sort((a, b) => Date.parse(b.finishedAt!) - Date.parse(a.finishedAt!))
    .slice(0, MAX_STATUS_JOBS);
  const jobs = [...activeJobs, ...recentFinishedJobs]
    .map((job) => {
    let stage = job.stage;
    if (
      stage === "running" &&
      job.timeoutSec !== undefined &&
      job.startedAt
    ) {
      const lastAt = job.lastReportAt ?? job.startedAt;
      if (now() - Date.parse(lastAt) >= job.timeoutSec * 1000) {
        stage = "stale";
      }
    }
    return {
      id: job.id,
      repo: job.repo,
      snapshot: job.snapshot,
      stage,
      host: job.host,
      rc: job.rc,
      publication: job.publication ?? { state: "none" as const },
    };
  });

  const cutoff24h = now() - 24 * 60 * 60 * 1000;
  const jobs24h = recordedJobs.filter((job) => {
    const startedAt = job.startedAt ? Date.parse(job.startedAt) : Number.NaN;
    return Number.isFinite(startedAt) && startedAt >= cutoff24h;
  });
  const finished24h = jobs24h.filter((job) => job.finishedAt && job.rc !== null);
  const exit127Jobs = finished24h.filter((job) => job.rc === 127);
  const remoteSuccessPct = finished24h.length > 0
    ? finished24h.filter((job) => job.rc === 0).length / finished24h.length * 100
    : undefined;

  return {
    desired,
    observed,
    revision: meta.revision,
    lease: {
      active: lease.active,
      ...(expired ? { expired: true } : {}),
      host: lease.host,
      expiresAt: lease.expiresAt,
      reason: lease.reason,
    },
    capacity: {
      builders: capacity.builders,
      idleSlots: capacity.idleSlots,
      overloaded: capacity.idleSlots === 0 && capacity.builders > 0,
    },
    reconciler: {
      healthy: configHealth?.degraded ? false : meta.reconcilerHealthy,
      ...(meta.reconcilerLastAt ? { lastAt: meta.reconcilerLastAt } : {}),
    },
    dispatch: {
      state: configHealth?.degraded ? "paused" : meta.dispatchState,
      ...(meta.dispatchDetail ? { detail: meta.dispatchDetail } : {}),
      ...(meta.dispatchHost ? { host: meta.dispatchHost } : {}),
      ...(configHealth?.incident?.detail && configHealth.degraded
        ? { detail: configHealth.incident.detail }
        : {}),
      ...(!meta.dispatchHost && dispatchTarget
        ? { host: dispatchTarget }
        : {}),
    },
    hosts: hostMap,
    landConduct,
    deployWatcher,
    queue: {
      depth: queuedTickets.length,
      oldestAgeSeconds,
      p95AgeSeconds,
      sloSeconds: DEFAULT_QUEUE_SLO_SECONDS,
      ...(dispatchTarget
        ? { dispatchTarget }
        : {}),
      tickets: activeTickets.map((ticket) => ({
        position: ticket.position,
        key: ticket.key,
        repo: ticket.repo,
        owner: ticket.owner,
        enqueueAgeSeconds: ticket.enqueueAgeSeconds,
        state: ticket.state,
        ...(ticket.dispatchTarget ? { dispatchTarget: ticket.dispatchTarget } : {}),
      })),
      spill: {
        active: lease.active,
        stale: expired,
      },
    },
    jobs,
    kpis: {
      remote24h: jobs24h.length,
      ...(remoteSuccessPct !== undefined ? { remoteSuccessPct } : {}),
      exit127: {
        count: exit127Jobs.length,
        hosts: [...new Set(exit127Jobs.map((job) => job.host))].sort(),
      },
    },
  };
}
