import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import { createWatchdogWebhookAdapter } from "./notify";
import {
  OffLaptopWatchdog,
  type WatchdogNotifier,
  type WatchdogTarget,
} from "./watchdog";

export class WatchdogFatalError extends Error {
  constructor(
    public readonly code: string,
    message: string,
  ) {
    super(message);
    this.name = "WatchdogFatalError";
  }
}

const WatchdogConfigSchema = z.object({
  controllerHeartbeatUrl: z.string(),
  collectorHealthUrl: z.string(),
  tokenFile: z.string(),
  webhookUrl: z.string(),
  intervalMs: z.number().int().optional(),
  controllerRestartArgv: z.array(z.string()).optional(),
  collectorRestartArgv: z.array(z.string()).optional(),
}).strict();

export type WatchdogConfig = z.infer<typeof WatchdogConfigSchema>;

export interface ResolvedWatchdogConfig {
  controllerHeartbeatUrl: string;
  collectorHealthUrl: string;
  tokenFile: string;
  webhookUrl: string;
  intervalMs: number;
  controllerRestartArgv?: string[];
  collectorRestartArgv?: string[];
}

export function defaultWatchdogConfigPath(): string {
  return process.env.OVERDECK_WATCHDOG_CONFIG ?? join(homedir(), ".config", "overdeck", "watchdog.json");
}

function assertLoopbackUrl(url: string, field: string): void {
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`${field} must be a valid URL`);
  }
  if (parsed.hostname !== "127.0.0.1") {
    throw new Error(`${field} must use loopback host 127.0.0.1`);
  }
}

function assertHttpsWebhook(url: string): void {
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error("webhookUrl must be a valid URL");
  }
  if (parsed.protocol !== "https:") {
    throw new Error("webhookUrl must use HTTPS");
  }
}

function assertRestartArgv(argv: string[] | undefined, field: string): void {
  if (argv === undefined) return;
  if (argv.length === 0) return;
  if (argv.some((part) => part === "")) {
    throw new Error(`${field} must contain only non-empty strings`);
  }
}

export function parseWatchdogConfig(raw: unknown): ResolvedWatchdogConfig {
  const parsed = WatchdogConfigSchema.parse(raw);
  assertLoopbackUrl(parsed.controllerHeartbeatUrl, "controllerHeartbeatUrl");
  assertLoopbackUrl(parsed.collectorHealthUrl, "collectorHealthUrl");
  assertHttpsWebhook(parsed.webhookUrl);
  assertRestartArgv(parsed.controllerRestartArgv, "controllerRestartArgv");
  assertRestartArgv(parsed.collectorRestartArgv, "collectorRestartArgv");

  const intervalMs = parsed.intervalMs ?? 30_000;
  if (!Number.isInteger(intervalMs) || intervalMs <= 10_000) {
    throw new Error("intervalMs must be an integer greater than 10000");
  }

  return {
    controllerHeartbeatUrl: parsed.controllerHeartbeatUrl,
    collectorHealthUrl: parsed.collectorHealthUrl,
    tokenFile: parsed.tokenFile,
    webhookUrl: parsed.webhookUrl,
    intervalMs,
    ...(parsed.controllerRestartArgv !== undefined
      ? { controllerRestartArgv: parsed.controllerRestartArgv }
      : {}),
    ...(parsed.collectorRestartArgv !== undefined
      ? { collectorRestartArgv: parsed.collectorRestartArgv }
      : {}),
  };
}

export async function loadWatchdogConfig(path = defaultWatchdogConfigPath()): Promise<ResolvedWatchdogConfig> {
  let raw: string;
  try {
    raw = await Bun.file(path).text();
  } catch {
    throw new Error(`cannot read watchdog config at ${path}`);
  }
  let json: unknown;
  try {
    json = JSON.parse(raw);
  } catch {
    throw new Error(`watchdog config at ${path} is not valid JSON`);
  }
  return parseWatchdogConfig(json);
}

export async function readWatchdogToken(tokenFile: string): Promise<string> {
  if (!existsSync(tokenFile)) {
    throw new WatchdogFatalError("TOKEN_MISSING", `token file not found: ${tokenFile}`);
  }
  const content = (await Bun.file(tokenFile).text()).trim();
  if (content === "") {
    throw new WatchdogFatalError("TOKEN_EMPTY", `token file is empty: ${tokenFile}`);
  }
  return content;
}

export function createWatchdogFromConfig(
  config: ResolvedWatchdogConfig,
  token: string,
  notifier: WatchdogNotifier,
): OffLaptopWatchdog {
  const targets: WatchdogTarget[] = [
    {
      name: "controller",
      url: config.controllerHeartbeatUrl,
      ...(config.controllerRestartArgv !== undefined
        ? { restartArgv: config.controllerRestartArgv }
        : {}),
    },
    {
      name: "collector",
      url: config.collectorHealthUrl,
      ...(config.collectorRestartArgv !== undefined
        ? { restartArgv: config.collectorRestartArgv }
        : {}),
    },
  ];

  return new OffLaptopWatchdog({
    targets,
    token,
    fetcher: fetch,
    clock: { sleep: Bun.sleep },
    restarter: {
      restart: async (_target, restartArgv) => {
        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()}`);
        }
      },
    },
    notifier,
    intervalMs: config.intervalMs,
  });
}

function formatConfigError(error: unknown): { code: string; message: string } {
  if (error instanceof WatchdogFatalError) {
    return { code: error.code, message: error.message };
  }
  if (error instanceof z.ZodError) {
    return { code: "CONFIG_INVALID", message: error.message };
  }
  if (error instanceof Error) {
    return { code: "CONFIG_INVALID", message: error.message };
  }
  return { code: "CONFIG_INVALID", message: String(error) };
}

export async function validateWatchdogConfig(path = defaultWatchdogConfigPath()): Promise<void> {
  const config = await loadWatchdogConfig(path);
  await readWatchdogToken(config.tokenFile);
}

function printHelp(): void {
  console.log(`overdeck-watchdog — off-laptop health observer

Usage:
  overdeck-watchdog [--help]
  overdeck-watchdog config-validate

Environment:
  OVERDECK_WATCHDOG_CONFIG  Path to watchdog JSON config
                            (default: $HOME/.config/overdeck/watchdog.json)
`);
}

async function main(argv: string[]): Promise<number> {
  const [command] = argv;

  if (command === "--help" || command === "-h") {
    printHelp();
    return 0;
  }

  if (command === "config-validate") {
    try {
      await validateWatchdogConfig();
      process.stdout.write('{"ok":true}\n');
      return 0;
    } catch (error) {
      const formatted = formatConfigError(error);
      process.stderr.write(`${JSON.stringify({ ok: false, ...formatted })}\n`);
      return 1;
    }
  }

  if (command !== undefined) {
    process.stderr.write(`${JSON.stringify({ ok: false, code: "USAGE", message: `unknown command: ${command}` })}\n`);
    return 1;
  }

  let config: ResolvedWatchdogConfig;
  try {
    config = await loadWatchdogConfig();
  } catch (error) {
    const formatted = formatConfigError(error);
    process.stderr.write(`${JSON.stringify({ ok: false, ...formatted })}\n`);
    return 1;
  }

  let token: string;
  try {
    token = await readWatchdogToken(config.tokenFile);
  } catch (error) {
    const formatted = formatConfigError(error);
    process.stderr.write(`${JSON.stringify({ ok: false, ...formatted })}\n`);
    return 1;
  }

  const watchdog = createWatchdogFromConfig(
    config,
    token,
    createWatchdogWebhookAdapter({ webhookUrl: config.webhookUrl }),
  );

  const abort = new AbortController();
  const onSignal = () => abort.abort();
  process.on("SIGINT", onSignal);
  process.on("SIGTERM", onSignal);
  try {
    await watchdog.run(abort.signal);
  } finally {
    process.off("SIGINT", onSignal);
    process.off("SIGTERM", onSignal);
  }
  return 0;
}

if (import.meta.main) {
  const exitCode = await main(process.argv.slice(2));
  process.exit(exitCode);
}
