// Prerequisite worklist for a conditional shrink scenario. The engine emits
// prerequisite strings (requests:<ns>/<kind>/<name>:cpu=X,mem=Y and
// remove-idle:<ns>/<kind>/<name>); this table joins them with the run's
// workload rows so the operator sees current -> target requests per
// replica, the schedulable capacity each change frees, and the idle
// removals — grouped by the same component groups as the Workloads tab
// (vmcluster expanded by default, the rest collapsed), impact-sorted inside
// each group. Out-of-scope workloads (requestless, <=0.1-core requests)
// never appear here: the engine emits no prerequisite for them.
//
// Unit contract: WorkloadRow.reqCpu/reqMemGib are the workload TOTAL across
// replicas (workloads.ts sums per-pod requests), while the prerequisite
// targets are PER POD — the join divides by npods before comparing.

import { Fragment, useState, type ReactNode } from "react";
import { workloadKeyOf, type WorkloadRow } from "@/engine/classify";
import { fixed } from "@/lib/fmt";
import { groupLabel, groupRank } from "@/lib/workload-groups";
import { DataTable, Td, Th, rowKeyActivate } from "@/components/ui/DataTable";

const REQ_RE = /^requests:([^/]+)\/([^/]+)\/([^:]+):cpu=([\d.]+),mem=([\d.]+)$/;
const IDLE_RE = /^remove-idle:([^/]+)\/([^/]+)\/([^/]+)$/;

interface PrereqRow {
  key: string;
  group: string;
  ns: string;
  kind: string;
  name: string;
  replicas: number | null;
  /** current per-pod request; null when no matching workload row */
  curCpu: number | null;
  curMem: number | null;
  /** target per-pod request (right-size) — null for idle removals */
  tgtCpu: number | null;
  tgtMem: number | null;
  /** schedulable capacity freed across all replicas */
  freedCpu: number;
  freedMem: number;
}

/** Per-pod current request and group from the workload row (total /
 *  replicas; rows without a match fall into the "other" group). */
function perPod(w: WorkloadRow | undefined): {
  reps: number | null;
  cpu: number | null;
  mem: number | null;
  group: string;
} {
  if (!w) return { reps: null, cpu: null, mem: null, group: "other" };
  const reps = w.npods > 0 ? w.npods : null;
  return {
    reps,
    cpu: reps === null ? null : w.reqCpu / reps,
    mem: reps === null ? null : w.reqMemGib / reps,
    group: w.group || "other",
  };
}

function parsePrerequisites(
  prerequisites: string[],
  workloads: WorkloadRow[],
): { rights: PrereqRow[]; idles: PrereqRow[]; unrecognized: string[] } {
  const byKey = new Map(workloads.map((w) => [workloadKeyOf(w), w]));
  const rights: PrereqRow[] = [];
  const idles: PrereqRow[] = [];
  const unrecognized: string[] = [];
  for (const p of prerequisites) {
    const req = REQ_RE.exec(p);
    if (req) {
      const [, ns, kind, name, cpuS, memS] = req;
      const tgtCpu = Number(cpuS);
      const tgtMem = Number(memS);
      const { reps, cpu: curCpu, mem: curMem, group } = perPod(
        byKey.get(`${ns}/${kind}/${name}`),
      );
      rights.push({
        key: p,
        group,
        ns,
        kind,
        name,
        replicas: reps,
        curCpu,
        curMem,
        tgtCpu,
        tgtMem,
        freedCpu:
          curCpu === null || reps === null ? 0 : Math.max(0, curCpu - tgtCpu) * reps,
        freedMem:
          curMem === null || reps === null ? 0 : Math.max(0, curMem - tgtMem) * reps,
      });
      continue;
    }
    const idle = IDLE_RE.exec(p);
    if (idle) {
      const [, ns, kind, name] = idle;
      const { reps, cpu: curCpu, mem: curMem, group } = perPod(
        byKey.get(`${ns}/${kind}/${name}`),
      );
      idles.push({
        key: p,
        group,
        ns,
        kind,
        name,
        replicas: reps,
        curCpu,
        curMem,
        tgtCpu: null,
        tgtMem: null,
        freedCpu: curCpu === null || reps === null ? 0 : curCpu * reps,
        freedMem: curMem === null || reps === null ? 0 : curMem * reps,
      });
      continue;
    }
    // Unknown format (engine/UI drift): render verbatim rather than
    // silently dropping the entry — an empty worklist must mean "nothing
    // to do", never "the parser gave up".
    unrecognized.push(p);
  }
  return { rights, idles, unrecognized };
}

interface GroupedRows {
  id: string;
  rows: PrereqRow[];
  freedCpu: number;
  freedMem: number;
}

/** Bucket rows by component group (engine rank order — same as the
 *  Workloads tab), impact-sorted within each bucket. */
function groupRows(rows: PrereqRow[]): GroupedRows[] {
  const byImpact = (a: PrereqRow, b: PrereqRow): number =>
    b.freedMem - a.freedMem || b.freedCpu - a.freedCpu || a.name.localeCompare(b.name);
  const buckets = new Map<string, PrereqRow[]>();
  for (const r of rows) {
    const b = buckets.get(r.group);
    if (b) b.push(r);
    else buckets.set(r.group, [r]);
  }
  return [...buckets.entries()]
    .map(([id, rs]) => ({
      id,
      rows: [...rs].sort(byImpact),
      freedCpu: rs.reduce((a, r) => a + r.freedCpu, 0),
      freedMem: rs.reduce((a, r) => a + r.freedMem, 0),
    }))
    .sort((a, b) => groupRank(a.id) - groupRank(b.id));
}

/** "4.0c / 70.0g" for a cores/GiB pair (units inline, so headers stay
 *  short), or an em dash when unknown. */
function pair(cpu: number | null, mem: number | null): string {
  if (cpu === null || mem === null) return "—";
  return `${fixed(cpu, 2)}c / ${fixed(mem, 1)}g`;
}

function WorkloadCell({ row }: { row: PrereqRow }): ReactNode {
  return (
    <Td>
      <div className="flex flex-col">
        <span className="truncate font-medium" title={`${row.ns}/${row.name}`}>
          {row.name}
        </span>
        <span className="text-xs text-text-muted">{row.ns} · {row.kind}</span>
      </div>
    </Td>
  );
}

interface GroupToggles {
  expanded: ReadonlySet<string>;
  toggle: (id: string) => void;
}

function GroupHeader({
  colSpan,
  group,
  open,
  onToggle,
}: {
  colSpan: number;
  group: GroupedRows;
  open: boolean;
  onToggle: () => void;
}): ReactNode {
  return (
    <tr
      onClick={onToggle}
      onKeyDown={rowKeyActivate(onToggle)}
      tabIndex={0}
      className="cursor-pointer hover:bg-surface-1"
    >
      <td colSpan={colSpan} className="border-b border-surface-2/50 bg-surface-1/40 px-3 py-2">
        <div className="flex flex-wrap items-center gap-x-4 gap-y-1">
          <span className="text-sm font-medium">
            {open ? "▾" : "▸"} {groupLabel(group.id)}
          </span>
          <span className="text-xs tabular-nums text-text-muted">
            {group.rows.length} {group.rows.length === 1 ? "workload" : "workloads"}
          </span>
          <span className="text-xs tabular-nums text-text-muted">
            frees {fixed(group.freedCpu, 1)}c / {fixed(group.freedMem, 1)}g
          </span>
        </div>
      </td>
    </tr>
  );
}

function RightSizeTable({ groups, toggles }: { groups: GroupedRows[]; toggles: GroupToggles }): ReactNode {
  return (
    <DataTable>
      <thead>
        <tr>
          <Th>Workload</Th>
          <Th numeric>Pods</Th>
          <Th>Now req/pod</Th>
          <Th>Target req/pod</Th>
          <Th numeric>Frees (all replicas)</Th>
        </tr>
      </thead>
      <tbody>
        {groups.map((g) => (
          <Fragment key={g.id}>
            <GroupHeader
              colSpan={5}
              group={g}
              open={toggles.expanded.has(g.id)}
              onToggle={() => toggles.toggle(g.id)}
            />
            {toggles.expanded.has(g.id)
              ? g.rows.map((r) => (
                  <tr key={r.key} className="hover:bg-surface-1">
                    <WorkloadCell row={r} />
                    <Td numeric>{r.replicas === null ? "—" : r.replicas}</Td>
                    <Td><span className="text-text-secondary">{pair(r.curCpu, r.curMem)}</span></Td>
                    <Td><span className="font-medium">{pair(r.tgtCpu, r.tgtMem)}</span></Td>
                    <Td numeric><span className="font-medium text-text-primary">{pair(r.freedCpu, r.freedMem)}</span></Td>
                  </tr>
                ))
              : null}
          </Fragment>
        ))}
      </tbody>
    </DataTable>
  );
}

function IdleTable({ groups, toggles }: { groups: GroupedRows[]; toggles: GroupToggles }): ReactNode {
  return (
    <DataTable>
      <thead>
        <tr>
          <Th>Workload</Th>
          <Th numeric>Pods</Th>
          <Th>Req/pod</Th>
          <Th>Action</Th>
        </tr>
      </thead>
      <tbody>
        {groups.map((g) => (
          <Fragment key={g.id}>
            <GroupHeader
              colSpan={4}
              group={g}
              open={toggles.expanded.has(g.id)}
              onToggle={() => toggles.toggle(g.id)}
            />
            {toggles.expanded.has(g.id)
              ? g.rows.map((r) => (
                  <tr key={r.key} className="hover:bg-surface-1">
                    <WorkloadCell row={r} />
                    <Td numeric>{r.replicas === null ? "—" : r.replicas}</Td>
                    <Td><span className="text-text-secondary">{pair(r.curCpu, r.curMem)}</span></Td>
                    <Td><span className="font-medium">delete or scale to zero</span></Td>
                  </tr>
                ))
              : null}
          </Fragment>
        ))}
      </tbody>
    </DataTable>
  );
}

export function PrerequisiteTable({
  prerequisites,
  workloads,
}: {
  prerequisites: string[];
  workloads: WorkloadRow[];
}): ReactNode | null {
  // Hook order: the empty-prerequisites early return must come after the
  // useState call.
  const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(["vmcluster"]));
  if (prerequisites.length === 0) return null;
  const { rights, idles, unrecognized } = parsePrerequisites(prerequisites, workloads);
  const all = [...rights, ...idles];
  const totCpu = all.reduce((a, r) => a + r.freedCpu, 0);
  const totMem = all.reduce((a, r) => a + r.freedMem, 0);
  const toggles: GroupToggles = {
    expanded: expandedGroups,
    toggle: (id) =>
      setExpandedGroups((cur) => {
        const next = new Set(cur);
        if (next.has(id)) next.delete(id);
        else next.add(id);
        return next;
      }),
  };

  return (
    <div className="space-y-2">
      <div className="text-sm text-text-muted">
        Prerequisites — {rights.length} request adjustment{rights.length === 1 ? "" : "s"} +
        {" "}{idles.length} idle removal{idles.length === 1 ? "" : "s"}: applying all of them
        frees ≈ <span className="font-medium text-text-primary">{fixed(totCpu, 1)}c / {fixed(totMem, 1)}g</span>{" "}
        of schedulable requests (c = cores, g = GiB).
      </div>
      {rights.length > 0 ? <RightSizeTable groups={groupRows(rights)} toggles={toggles} /> : null}
      {idles.length > 0 ? (
        <div className="space-y-1">
          <div className="text-sm font-medium text-text-muted">Idle workloads to remove</div>
          <IdleTable groups={groupRows(idles)} toggles={toggles} />
        </div>
      ) : null}
      {unrecognized.length > 0 ? (
        <div className="text-xs text-text-muted">
          Unrecognized prerequisite entries (rendered verbatim — engine/UI
          format drift?): <span className="font-mono">{unrecognized.join(", ")}</span>
        </div>
      ) : null}
      <div className="text-xs text-text-muted">
        Targets are the §7 recommendation: per-pod 7d-average usage × 1.5,
        snapped to a size ladder — the per-pod burst ceiling (limit) tracks
        2× the observed max instead. Adjust requests only (keep limits ≥ the
        new request); edit the owning manifest or operator CR (VMCluster for
        vmcluster components), then wait for the next evaluation.
        Requestless and ≤0.1-core pods are out of scope and never listed.
      </div>
    </div>
  );
}
