#!/usr/bin/env bash
# deckctl fleet — audit/converge/harden/status over the fleet declaration (fail-closed).
set -euo pipefail

fleet_usage() {
  cat >&2 <<'EOF'
usage: deckctl fleet audit    [node...]
       deckctl fleet converge [node...]
       deckctl fleet harden   <one-buildbox-node>
       deckctl fleet status
EOF
  exit 2
}

fleet_die() {
  echo "deckctl fleet: ERROR: $*" >&2
  exit 1
}

fleet_run_node() {
  local verb="$1"
  shift
  local fleet_file="${DECKCTL_FLEET_FILE:-$DECKCTL_ROOT/modules/fleet/fleet.json}"
  (
    cd "$DECKCTL_ROOT"
    DECKCTL_FLEET_FILE="$fleet_file" DECKCTL_ROOT="$DECKCTL_ROOT" node --input-type=module -e '
import { auditBuildTuning, loadFleet, FleetError } from "./lib/fleet/loader.mjs";
import { expandNode } from "./lib/fleet/expand.mjs";
import {
  aggregateExitCode,
  auditNode,
  auditNodes,
  convergeNodes,
  computeItemsRevision,
  formatAuditReport,
  printAuditReport,
} from "./lib/fleet/engine.mjs";
import { hardenNode, HardenError } from "./lib/fleet/harden.mjs";
import { createLocalTransport } from "./lib/fleet/transport-local.mjs";
import { createSshTransport, SshTransportError } from "./lib/fleet/transport-ssh.mjs";
import { BuildboxRegistryError, loadRegistry } from "./modules/workstation/claude/lib/buildbox-registry.mjs";

const fleetPath = process.env.DECKCTL_FLEET_FILE;
const projectRoot = process.env.DECKCTL_PROJECT_ROOT;
const [verb, ...nodeArgs] = process.argv.slice(1);

const NA = "n/a (availability plane not landed)";

function fail(message, code = 1) {
  process.stderr.write(`deckctl fleet: ERROR: ${message}\n`);
  process.exit(code);
}

function countDrift(report) {
  let count = report.drift.length;
  if (report.delegated && !report.delegated.ok) {
    if (Array.isArray(report.delegated.lines)) {
      count += report.delegated.lines.filter((line) => line.level === "FAIL").length;
    } else if (Array.isArray(report.delegated.drift)) {
      count += report.delegated.drift.length;
    }
  }
  return count;
}

function validateNodes(fleet, names) {
  for (const name of names) {
    fleet.node(name);
  }
}

function resolveNodeNames(fleet, requested) {
  const names = requested.length > 0 ? requested : fleet.nodeNames();
  validateNodes(fleet, names);
  return names;
}

function registryHost(registry, hostRef) {
  return registry.hosts.find((entry) => entry.name === hostRef || entry.ssh_alias === hostRef) ?? null;
}

function registryReachability(registry, hostRef) {
  const host = registryHost(registry, hostRef);
  return host ? host.state : "unconfigured";
}

function createTransport(node, registry) {
  if (node.transport === "local") {
    return createLocalTransport();
  }
  return createSshTransport({ hostRef: node.host_ref, registry });
}

function isTransportBlocked(err) {
  if (err instanceof SshTransportError) {
    return true;
  }
  if (err instanceof BuildboxRegistryError) {
    return /must not be contacted|no such buildbox host/.test(err.message);
  }
  return false;
}

function unreachableAuditReport(nodeName, items) {
  return {
    node: nodeName,
    revision: computeItemsRevision(items),
    ok: false,
    drift: [],
    unreachable: true,
    delegated: null,
  };
}

function unreachableConvergeReport(nodeName, items) {
  const finalAudit = unreachableAuditReport(nodeName, items);
  return {
    node: nodeName,
    revision: finalAudit.revision,
    changed: [],
    failed: [{ id: "*", error: "node unreachable" }],
    finalAudit,
  };
}

function prepareNodeRuns(fleet, names, registry) {
  const runs = [];
  for (const nodeName of names) {
    const node = fleet.node(nodeName);
    const items = expandNode(fleet, nodeName);
    try {
      runs.push({
        nodeName,
        items,
        transport: createTransport(node, registry),
        blocked: null,
      });
    } catch (err) {
      if (!isTransportBlocked(err)) {
        throw err;
      }
      runs.push({
        nodeName,
        items,
        transport: null,
        blocked: err,
      });
    }
  }
  return runs;
}

async function runAuditFleet(fleet, names, registry, options) {
  const runs = prepareNodeRuns(fleet, names, registry);
  const stdoutLines = [];
  const reports = [];

  const runnable = runs.filter((entry) => entry.transport);
  const runnableReports =
    runnable.length > 0
      ? await auditNodes(
          runnable.map((entry) => ({
            nodeName: entry.nodeName,
            items: entry.items,
            transport: entry.transport,
          })),
          { log: () => {}, ...options },
        )
      : [];

  let runnableIndex = 0;
  for (const entry of runs) {
    const report = entry.transport
      ? runnableReports[runnableIndex++]
      : unreachableAuditReport(entry.nodeName, entry.items);
    reports.push(report);
    for (const line of formatAuditReport(report)) {
      stdoutLines.push(line);
    }
  }

  for (const line of stdoutLines) {
    process.stdout.write(`${line}\n`);
  }
  const summary = summarizeAudit(reports);
  process.stderr.write(`fleet audit: ${summary.ok} ok, ${summary.failed} failed\n`);
  return aggregateExitCode(reports);
}

async function runConvergeFleet(fleet, names, registry, options) {
  const runs = prepareNodeRuns(fleet, names, registry);
  const stdoutLines = [];
  const reports = [];

  const runnable = runs.filter((entry) => entry.transport);
  const runnableReports =
    runnable.length > 0
      ? await convergeNodes(
          runnable.map((entry) => ({
            nodeName: entry.nodeName,
            items: entry.items,
            transport: entry.transport,
          })),
          { log: () => {}, ...options },
        )
      : [];

  let runnableIndex = 0;
  for (const entry of runs) {
    const report = entry.transport
      ? runnableReports[runnableIndex++]
      : unreachableConvergeReport(entry.nodeName, entry.items);
    reports.push(report);
    for (const line of formatAuditReport(report.finalAudit)) {
      stdoutLines.push(line);
    }
  }

  for (const line of stdoutLines) {
    process.stdout.write(`${line}\n`);
  }
  const summary = summarizeConverge(reports);
  process.stderr.write(`fleet converge: ${summary.ok} ok, ${summary.failed} failed\n`);
  return aggregateExitCode(reports);
}

function summarizeAudit(reports) {
  let ok = 0;
  let failed = 0;
  for (const report of reports) {
    if (report.ok && !report.unreachable) {
      ok += 1;
    } else {
      failed += 1;
    }
  }
  return { ok, failed };
}

function summarizeConverge(reports) {
  let ok = 0;
  let failed = 0;
  for (const report of reports) {
    if (report.failed.length === 0 && report.finalAudit.ok && !report.finalAudit.unreachable) {
      ok += 1;
    } else {
      failed += 1;
    }
  }
  return { ok, failed };
}

async function runStatus(fleet, registry) {
  const lines = [];
  for (const nodeName of fleet.nodeNames()) {
    const node = fleet.node(nodeName);
    const items = expandNode(fleet, nodeName);
    const expansionDigest = computeItemsRevision(items);
    const roles = node.roles.join(",");
    let driftCount = NA;
    let transportReachability;
    let runtimeState;

    if (node.transport === "local") {
      transportReachability = "local";
      runtimeState = "local";
      try {
        const transport = createLocalTransport();
        const audit = await auditNode(nodeName, items, transport);
        if (!audit.unreachable) {
          driftCount = String(countDrift(audit));
        }
      } catch {
        driftCount = NA;
      }
    } else {
      const reachability = registryReachability(registry, node.host_ref);
      transportReachability = reachability;
      runtimeState = reachability;
      if (reachability === "reachable") {
        try {
          const transport = createSshTransport({ hostRef: node.host_ref, registry });
          const audit = await auditNode(nodeName, items, transport);
          if (!audit.unreachable) {
            driftCount = String(countDrift(audit));
          }
        } catch {
          driftCount = NA;
        }
      }
    }

    const fields = [
      `node=${nodeName}`,
      `desired_revision=${fleet.revision}`,
      `expansion_digest=${expansionDigest}`,
      `drift_count=${driftCount}`,
      `last_successful_audit=${NA}`,
      `transport_reachability=${transportReachability}`,
      `runtime_roles=${roles}`,
      `runtime_state=${runtimeState}`,
    ];
    if (nodeName === fleet.fallback.node) {
      fields.push(`fallback_state=${NA}`, `lease_expiry=${NA}`, `local_jobs=${NA}`);
    }
    lines.push(fields.join("\t"));
  }

  for (const line of lines) {
    process.stdout.write(`${line}\n`);
  }
  process.stderr.write(`fleet status: ${lines.length} node(s)\n`);
  return 0;
}

async function main() {
  if (!projectRoot) {
    fail("missing validated caller project root");
  }

  let fleet;
  try {
    fleet = loadFleet(fleetPath);
  } catch (err) {
    fail(err instanceof FleetError ? err.message : String(err.message ?? err));
  }

  const registry = loadRegistry();

  try {
    if (verb === "status") {
      if (nodeArgs.length > 0) {
        fail("status accepts no node arguments");
      }
      process.exit(await runStatus(fleet, registry));
    }

    if (verb === "harden") {
      if (nodeArgs.length !== 1) {
        fail("harden requires exactly one buildbox node");
      }
      validateNodes(fleet, nodeArgs);
      const nodeName = nodeArgs[0];
      const node = fleet.node(nodeName);
      const items = expandNode(fleet, nodeName);
      const transport = createTransport(node, registry);
      const report = await hardenNode(nodeName, items, transport, { seatOpts: { projectRoot } });
      printAuditReport(report.finalAudit);
      const changed = report.changed.length;
      process.stderr.write(
        `fleet harden: ${nodeName} changed=${changed} rebooted=${report.rebooted ? "yes" : "no"}\n`,
      );
      process.exit(report.finalAudit.ok && !report.finalAudit.unreachable ? 0 : 1);
    }

    if (verb === "audit" || verb === "converge") {
      auditBuildTuning(new Set(fleet.nodeNames()));
      const names = resolveNodeNames(fleet, nodeArgs);
      const options = { seatOpts: { projectRoot } };

      if (verb === "audit") {
        process.exit(await runAuditFleet(fleet, names, registry, options));
      }

      process.exit(await runConvergeFleet(fleet, names, registry, options));
    }

    fail(`unknown verb ${JSON.stringify(verb)}`, 2);
  } catch (err) {
    if (err instanceof FleetError || err instanceof HardenError) {
      fail(err.message);
    }
    throw err;
  }
}

main().catch((err) => {
  fail(err instanceof Error ? err.message : String(err));
});
' -- "$verb" "$@"
  )
}

cmd_fleet() {
  [[ $# -ge 1 ]] || fleet_usage
  local verb="$1"
  shift
  case "$verb" in
    audit|converge|harden|status) ;;
    *) fleet_usage ;;
  esac

  if [[ "$verb" == "harden" ]]; then
    if [[ $# -eq 0 || $# -gt 1 ]]; then
      fleet_die "harden requires exactly one buildbox node"
    fi
  fi

  fleet_run_node "$verb" "$@"
}
