import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import { buildCapabilityEvent } from "./events";
import {
  type CapabilityBreakerRecord,
  type CapabilityManifestRecord,
  type CapabilityBreakerState,
  type ControllerStore,
} from "./store";

export const ToolchainManifestSchema = z.object({
  repo: z.string().min(1),
  command: z.string().min(1),
  version: z.string().min(1),
  writablePaths: z.array(z.string().min(1)),
  minimumDiskBytes: z.number().int().nonnegative(),
  requiresSystemd: z.boolean(),
}).strict();

export type ToolchainManifest = z.infer<typeof ToolchainManifestSchema>;

export const CapabilityProbeResultSchema = z.object({
  commandPresent: z.boolean(),
  version: z.string().nullable(),
  writablePaths: z.array(z.string()),
  diskFreeBytes: z.number().int().nonnegative(),
  systemd: z.boolean(),
  /** Why the probe could not confirm the capability. Null when it confirmed one. */
  failureReason: z.string().min(1).nullable().default(null),
}).strict();

export type CapabilityProbeResult = z.infer<typeof CapabilityProbeResultSchema>;

export interface CapabilityProber {
  probe(
    host: string,
    manifest: ToolchainManifest,
  ): Promise<z.input<typeof CapabilityProbeResultSchema>>;
}

export interface AdmissionResult {
  admitted: boolean;
  missing: string[];
  /** Operator-facing cause when not admitted. Null when admitted. */
  reason: string | null;
}

export const HOST_ENROLLMENT_MANIFEST = {
  repo: "host-enrollment",
  command: "true",
  version: "ssh-exit-0",
  writablePaths: [] as string[],
  minimumDiskBytes: 0,
  requiresSystemd: false,
} as const satisfies ToolchainManifest;

const FAILURE_THRESHOLD = 2;

function failedProbe(failureReason: string): CapabilityProbeResult {
  return {
    commandPresent: false,
    version: null,
    writablePaths: [],
    diskFreeBytes: 0,
    systemd: false,
    failureReason,
  };
}

const successProbe: CapabilityProbeResult = {
  commandPresent: true,
  version: "ssh-exit-0",
  writablePaths: [],
  diskFreeBytes: 0,
  systemd: false,
  failureReason: null,
};

const HOSTNAME_PATTERN =
  /^(?!-)[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.(?!-)[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

export interface SpawnChild {
  exited: Promise<number>;
  kill(signal?: string): void;
}

export type SpawnExec = (
  cmd: string[],
  options: { shell: false },
) => SpawnChild;

export function isValidHostname(host: string): boolean {
  return host.length > 0 && host.length <= 253 && HOSTNAME_PATTERN.test(host);
}

/** ssh reserves 255 for its own failures; 127/126 come from the remote shell. */
function sshExitHint(code: number): string {
  if (code === 255) return "ssh transport, auth, or host-key failure";
  if (code === 127) return "remote command not found";
  if (code === 126) return "remote command not executable";
  return "remote command failed";
}

function isHostEnrollmentManifest(manifest: ToolchainManifest): boolean {
  return manifest.repo === HOST_ENROLLMENT_MANIFEST.repo
    && manifest.command === HOST_ENROLLMENT_MANIFEST.command
    && manifest.version === HOST_ENROLLMENT_MANIFEST.version
    && manifest.writablePaths.length === 0
    && manifest.minimumDiskBytes === 0
    && manifest.requiresSystemd === false;
}

export function createSshCapabilityProber(
  exec: SpawnExec = Bun.spawn as SpawnExec,
  home: string = homedir(),
  timeoutMs: number = 5_000,
): CapabilityProber {
  return {
    probe(host, manifest) {
      if (!isValidHostname(host)) {
        return Promise.resolve(failedProbe(`not a valid hostname: ${host}`));
      }
      if (!isHostEnrollmentManifest(manifest)) {
        return Promise.resolve(
          failedProbe(`no prober for manifest ${manifest.repo}/${manifest.command}`),
        );
      }

      const keyPath = join(home, ".ssh/id_ed25519_buildbox");
      return new Promise((resolve) => {
        let settled = false;
        const settle = (result: CapabilityProbeResult) => {
          if (settled) return;
          settled = true;
          clearTimeout(timer);
          resolve(result);
        };

        let child: SpawnChild;
        try {
          child = exec(
            ["ssh", "-p", "2222", "-i", keyPath, host, "true"],
            { shell: false },
          );
        } catch (err) {
          settle(failedProbe(`cannot spawn ssh: ${(err as Error).message}`));
          return;
        }

        const timer = setTimeout(() => {
          try {
            child.kill();
          } catch {
            // ignore kill failures on timed-out children
          }
          settle(failedProbe(`ssh ${host}:2222 did not answer within ${timeoutMs}ms`));
        }, timeoutMs);

        void child.exited
          .then((code) => {
            settle(
              code === 0
                ? { ...successProbe }
                : failedProbe(
                  `ssh ${host}:2222 running \`${manifest.command}\` exited ${code} (${sshExitHint(code)})`,
                ),
            );
          })
          .catch((err: unknown) => {
            settle(failedProbe(`ssh ${host}:2222 failed: ${(err as Error).message}`));
          });
      });
    },
  };
}

// A probe that could not run observed nothing: absence stays unproven, so the
// breaker must not open and the host must not be marked capability-failed.
function unconfirmed(failureReason: string): AdmissionResult {
  return { admitted: false, missing: ["probe-unavailable"], reason: failureReason };
}

const unavailableProber: CapabilityProber = {
  probe() {
    throw new Error("capability prober is not configured");
  },
};

export class CapabilityService {
  constructor(
    private readonly store: ControllerStore,
    private readonly prober: CapabilityProber = unavailableProber,
    private readonly now: () => number = () => Date.now(),
  ) {}

  async probeAdmission(
    host: string,
    manifestInput: ToolchainManifest = HOST_ENROLLMENT_MANIFEST,
  ): Promise<AdmissionResult> {
    const manifest = ToolchainManifestSchema.parse(manifestInput);
    const probe = CapabilityProbeResultSchema.parse(await this.prober.probe(host, manifest));
    if (probe.failureReason) return unconfirmed(probe.failureReason);
    const missing = missingCapabilities(manifest, probe);
    return {
      admitted: missing.length === 0,
      missing,
      reason: admissionReason(missing, probe),
    };
  }

  async admit(host: string, manifestInput: ToolchainManifest): Promise<AdmissionResult> {
    const manifest = ToolchainManifestSchema.parse(manifestInput);
    this.requireHost(host);
    this.store.upsertCapabilityManifest(manifest as CapabilityManifestRecord);

    const breaker = this.breaker(host, manifest.command);
    if (breaker.state === "open") {
      return {
        admitted: false,
        missing: ["circuit-open"],
        reason: `circuit open for ${manifest.command} after repeated failures`,
      };
    }

    const probe = CapabilityProbeResultSchema.parse(await this.prober.probe(host, manifest));
    if (probe.failureReason) return unconfirmed(probe.failureReason);
    const missing = missingCapabilities(manifest, probe);
    if (missing.length > 0) {
      this.openMissing(breaker);
      this.setHostCapabilityOk(host, false, admissionReason(missing, probe));
      return { admitted: false, missing, reason: admissionReason(missing, probe) };
    }

    if (breaker.state === "half-open") {
      return { admitted: true, missing: [], reason: null };
    }

    const closed: CapabilityBreakerRecord = {
      ...breaker,
      state: "closed",
      failureCount: 0,
      missingEventEmitted: false,
    };
    this.store.setCapabilityBreaker(closed);
    this.setHostCapabilityOk(host, true, null);
    return { admitted: true, missing: [], reason: null };
  }

  recordExit(
    host: string,
    command: string,
    code: number,
  ): CapabilityBreakerRecord {
    this.requireHost(host);
    const current = this.breaker(host, command);

    if (current.state === "open") {
      return current;
    }

    if (current.state === "half-open") {
      if (code === 0) {
        const closed: CapabilityBreakerRecord = {
          ...current,
          state: "closed",
          failureCount: 0,
          missingEventEmitted: false,
        };
        this.store.setCapabilityBreakerAndAppendEvent(
          closed,
          buildCapabilityEvent(host, command, "capability-restored", this.now),
        );
        return closed;
      }

      const reopened: CapabilityBreakerRecord = {
        ...current,
        state: "open",
        failureCount: FAILURE_THRESHOLD,
      };
      if (!current.missingEventEmitted) {
        this.emitMissing(reopened);
        return { ...reopened, missingEventEmitted: true };
      }
      this.store.setCapabilityBreaker(reopened);
      return reopened;
    }

    if (code !== 126 && code !== 127) {
      return current;
    }

    const failureCount = current.failureCount + 1;
    const state: CapabilityBreakerState = failureCount >= FAILURE_THRESHOLD
      ? "open"
      : current.state;
    const next = { ...current, state, failureCount };
    if (state === "open") {
      this.emitMissing(next);
      return { ...next, missingEventEmitted: true };
    }
    this.store.setCapabilityBreaker(next);
    return next;
  }

  quarantine(host: string, command: string): CapabilityBreakerRecord {
    this.requireHost(host);
    const next: CapabilityBreakerRecord = {
      ...this.breaker(host, command),
      state: "open",
      failureCount: FAILURE_THRESHOLD,
    };
    this.store.setCapabilityBreaker(next);
    return next;
  }

  halfOpen(host: string, command: string): CapabilityBreakerRecord {
    this.requireHost(host);
    const current = this.breaker(host, command);
    const next: CapabilityBreakerRecord = { ...current, state: "half-open" };
    this.store.setCapabilityBreaker(next);
    return next;
  }

  private breaker(hostname: string, command: string): CapabilityBreakerRecord {
    return this.store.getCapabilityBreaker(hostname, command) ?? {
      hostname,
      command,
      state: "closed",
      failureCount: 0,
      missingEventEmitted: false,
    };
  }

  private openMissing(current: CapabilityBreakerRecord): void {
    const next: CapabilityBreakerRecord = {
      ...current,
      state: "open",
      failureCount: Math.max(current.failureCount, FAILURE_THRESHOLD),
    };
    if (!current.missingEventEmitted) {
      this.emitMissing(next);
      return;
    }
    this.store.setCapabilityBreaker(next);
  }

  private emitMissing(record: CapabilityBreakerRecord): void {
    const emitted = { ...record, missingEventEmitted: true };
    this.store.setCapabilityBreakerAndAppendEvent(
      emitted,
      buildCapabilityEvent(record.hostname, record.command, "capability-missing", this.now),
    );
  }

  private setHostCapabilityOk(
    hostname: string,
    capabilityOk: boolean,
    capabilityReason: string | null,
  ): void {
    const host = this.requireHost(hostname);
    if (host.capabilityOk !== capabilityOk || host.capabilityReason !== capabilityReason) {
      this.store.upsertHost({ ...host, capabilityOk, capabilityReason });
    }
  }

  private requireHost(hostname: string) {
    const host = this.store.getHost(hostname);
    if (!host) throw new Error(`unknown host: ${hostname}`);
    return host;
  }
}

/**
 * The probe's own words when it has them; otherwise the manifest expectations it could not
 * meet. Never an empty string — an admission failure the operator cannot read is the defect
 * this exists to prevent.
 */
export function admissionReason(
  missing: readonly string[],
  probe: CapabilityProbeResult,
): string | null {
  if (missing.length === 0) return null;
  if (probe.failureReason) return probe.failureReason;
  return `unmet: ${missing.join(", ")}`;
}

export function missingCapabilities(
  manifest: ToolchainManifest,
  probe: CapabilityProbeResult,
): string[] {
  const missing: string[] = [];
  if (!probe.commandPresent) missing.push("command");
  if (probe.version !== manifest.version) missing.push("version");
  const writable = new Set(probe.writablePaths);
  for (const path of manifest.writablePaths) {
    if (!writable.has(path)) missing.push(`writable:${path}`);
  }
  if (probe.diskFreeBytes < manifest.minimumDiskBytes) missing.push("disk");
  if (manifest.requiresSystemd && !probe.systemd) missing.push("systemd");
  return missing;
}
