export type WatchdogTargetName = "controller" | "collector";

export interface WatchdogTarget {
  name: WatchdogTargetName;
  url: string;
  restartArgv?: string[];
}

export interface WatchdogNotification {
  target: WatchdogTargetName;
  url: string;
  error: string;
  restartError?: string;
}

export interface WatchdogClock {
  sleep(ms: number): Promise<void>;
}

export interface WatchdogRestarter {
  restart(target: WatchdogTargetName, restartArgv: string[]): Promise<void>;
}

export interface WatchdogNotifier {
  notify(notification: WatchdogNotification): Promise<void> | void;
}

export type WatchdogFetcher = (url: string, init?: RequestInit) => Promise<Response>;

export const WATCHDOG_PROBE_TIMEOUT_MS = 10_000;

export interface OffLaptopWatchdogOptions {
  targets: readonly WatchdogTarget[];
  token: string;
  fetcher: WatchdogFetcher;
  clock: WatchdogClock;
  restarter: WatchdogRestarter;
  notifier: WatchdogNotifier;
  intervalMs?: number;
  probeTimeoutMs?: number;
}

export class OffLaptopWatchdog {
  private readonly failed = new Set<WatchdogTargetName>();
  private readonly intervalMs: number;
  private readonly probeTimeoutMs: number;

  constructor(private readonly options: OffLaptopWatchdogOptions) {
    if (options.token === "") throw new Error("watchdog Bearer token is required");
    this.intervalMs = options.intervalMs ?? 30_000;
    if (!Number.isFinite(this.intervalMs) || this.intervalMs <= 0) {
      throw new Error("watchdog interval must be a positive finite number");
    }
    this.probeTimeoutMs = options.probeTimeoutMs ?? WATCHDOG_PROBE_TIMEOUT_MS;
    if (!Number.isFinite(this.probeTimeoutMs) || this.probeTimeoutMs <= 0) {
      throw new Error("watchdog probe timeout must be a positive finite number");
    }
    if (this.probeTimeoutMs >= this.intervalMs) {
      throw new Error("watchdog probe timeout must be strictly below interval");
    }
  }

  async pollOnce(): Promise<void> {
    await Promise.all(this.options.targets.map((target) => this.check(target)));
  }

  async run(signal?: AbortSignal): Promise<void> {
    while (!signal?.aborted) {
      await this.pollOnce();
      if (signal?.aborted) return;
      await this.options.clock.sleep(this.intervalMs);
    }
  }

  private async check(target: WatchdogTarget): Promise<void> {
    try {
      const response = await this.options.fetcher(target.url, {
        headers: { authorization: `Bearer ${this.options.token}` },
        signal: AbortSignal.timeout(this.probeTimeoutMs),
      });
      if (!response.ok) throw new Error(`health request returned ${response.status}`);
      const body = await response.json() as { ok?: unknown };
      if (body.ok !== true) throw new Error("health response did not report ok");
      this.failed.delete(target.name);
      return;
    } catch (error) {
      if (this.failed.has(target.name)) return;
      this.failed.add(target.name);
      let restartError: string | undefined;
      const restartArgv = target.restartArgv ?? [];
      if (restartArgv.length > 0) {
        try {
          await this.options.restarter.restart(target.name, restartArgv);
        } catch (restartFailure) {
          restartError = errorMessage(restartFailure);
        }
      }
      await this.options.notifier.notify({
        target: target.name,
        url: target.url,
        error: errorMessage(error),
        ...(restartError ? { restartError } : {}),
      });
    }
  }
}

class CommandRestarter implements WatchdogRestarter {
  async restart(_target: WatchdogTargetName, restartArgv: string[]): Promise<void> {
    const child = Bun.spawn({ cmd: restartArgv, stdout: "ignore", stderr: "pipe" });
    const [exitCode, stderr] = await Promise.all([
      child.exited,
      new Response(child.stderr).text(),
    ]);
    if (exitCode !== 0) {
      throw new Error(`${restartArgv[0]} exited ${exitCode}: ${stderr.trim()}`);
    }
  }
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export function createEnvironmentWatchdog(): OffLaptopWatchdog {
  const token = process.env.OVERDECK_WATCHDOG_TOKEN ?? "";
  const controllerRestartArgv = parseRestartArgvEnv("OVERDECK_CONTROLLER_RESTART_ARGV");
  const collectorRestartArgv = parseRestartArgvEnv("OVERDECK_COLLECTOR_RESTART_ARGV");
  return new OffLaptopWatchdog({
    targets: [
      {
        name: "controller",
        url: process.env.OVERDECK_CONTROLLER_HEARTBEAT_URL ?? "http://127.0.0.1:8787/heartbeat",
        ...(controllerRestartArgv ? { restartArgv: controllerRestartArgv } : {}),
      },
      {
        name: "collector",
        url: process.env.OVERDECK_COLLECTOR_HEALTH_URL ?? "http://127.0.0.1:3001/health",
        ...(collectorRestartArgv ? { restartArgv: collectorRestartArgv } : {}),
      },
    ],
    token,
    fetcher: fetch,
    clock: { sleep: Bun.sleep },
    restarter: new CommandRestarter(),
    notifier: {
      notify: (notification) => {
        console.error(JSON.stringify({ type: "offload-watchdog-page", ...notification }));
      },
    },
    intervalMs: Number(process.env.OVERDECK_WATCHDOG_INTERVAL_MS ?? "30000"),
  });
}

function parseRestartArgvEnv(variable: string): string[] | undefined {
  const configured = process.env[variable];
  if (!configured) return undefined;
  const parsed = JSON.parse(configured) as unknown;
  if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((part) => typeof part !== "string" || part === "")) {
    throw new Error(`${variable} must be a non-empty JSON argv array`);
  }
  return parsed as string[];
}

if (import.meta.main) {
  await createEnvironmentWatchdog().run();
}
