import { spawnSync } from "node:child_process";
import { z } from "zod";
import { loadConfig, type NotifyConfig } from "./config";
import type { IncidentNotification, IncidentNotifier } from "./incidents";
import type { WatchdogNotification, WatchdogNotifier } from "./watchdog";

export type NotifySeverity = "info" | "page" | "high";

export interface CanonicalNotification {
  title: string;
  detail: string;
  severity: NotifySeverity;
  ts: string;
}

export const CanonicalNotificationSchema = z.object({
  title: z.string(),
  detail: z.string(),
  severity: z.enum(["info", "page", "high"]),
  ts: z.string(),
}).strict();

export type NotifyExecResult = {
  status: number | null;
  error?: { code?: string; message?: string };
};

export type ArgvExec = (file: string, argv: readonly string[]) => NotifyExecResult;

export interface LaptopNotifierSink {
  send(record: CanonicalNotification): void;
}

export interface LaptopNotifierSinkOptions {
  mode: NotifyConfig["mode"];
  journalWrite: (line: string) => void;
  desktopExec: ArgvExec;
  notifySendPath?: string;
}

const DEFAULT_NOTIFY_SEND = "/usr/bin/notify-send";
export const WATCHDOG_WEBHOOK_TIMEOUT_MS = 5_000;

export function mapIncidentNotification(
  notification: IncidentNotification,
): CanonicalNotification {
  const { severity, incident } = notification;
  return {
    title: incident.key,
    detail: JSON.stringify({
      affectedJobs: incident.affectedJobs,
      remediation: incident.remediation,
      count: incident.count,
      state: incident.state,
      autoResolveCondition: incident.autoResolveCondition,
      cooldownUntil: incident.cooldownUntil,
    }),
    severity,
    ts: incident.lastSeen,
  };
}

export function mapWatchdogNotification(
  notification: WatchdogNotification,
  now: number,
): CanonicalNotification {
  return {
    title: `watchdog:${notification.target}`,
    detail: JSON.stringify({
      url: notification.url,
      error: notification.error,
      ...(notification.restartError === undefined
        ? {}
        : { restartError: notification.restartError }),
    }),
    severity: "page",
    ts: new Date(now).toISOString(),
  };
}

export function defaultDesktopExec(file: string, argv: readonly string[]): NotifyExecResult {
  try {
    const result = spawnSync(file, [...argv], { stdio: "ignore" });
    return { status: result.status };
  } catch (error) {
    const err = error as NodeJS.ErrnoException;
    return {
      status: null,
      error: { code: err.code, message: err.message },
    };
  }
}

function shouldJournal(mode: NotifyConfig["mode"], severity: NotifySeverity): boolean {
  return mode === "journal" || mode === "both" || severity === "info";
}

function shouldDesktop(mode: NotifyConfig["mode"], severity: NotifySeverity): boolean {
  return (mode === "desktop" || mode === "both")
    && (severity === "page" || severity === "high");
}

export function createLaptopNotifierSink(
  options: LaptopNotifierSinkOptions,
): LaptopNotifierSink {
  const notifySendPath = options.notifySendPath ?? DEFAULT_NOTIFY_SEND;

  return {
    send(record: CanonicalNotification) {
      if (shouldJournal(options.mode, record.severity)) {
        options.journalWrite(JSON.stringify(record));
      }

      if (!shouldDesktop(options.mode, record.severity)) {
        return;
      }

      const argv = [
        "--urgency=critical",
        "--app-name=overdeck",
        record.title,
        record.detail,
      ] as const;
      const result = options.desktopExec(notifySendPath, argv);
      if (result.error || (result.status !== null && result.status !== 0)) {
        options.journalWrite(JSON.stringify({
          type: "desktop-notify-failed",
          title: record.title,
          error: result.error?.message ?? `notify-send exited ${result.status}`,
        }));
      }
    },
  };
}

export function createIncidentNotifierAdapter(
  sink: LaptopNotifierSink,
): IncidentNotifier {
  return {
    notify(notification: IncidentNotification) {
      sink.send(mapIncidentNotification(notification));
    },
  };
}

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

export interface WatchdogWebhookAdapterOptions {
  webhookUrl: string;
  fetcher?: WatchdogWebhookFetcher;
  now?: () => number;
  stderrWrite?: (line: string) => void;
}

export function createWatchdogWebhookAdapter(
  options: WatchdogWebhookAdapterOptions,
): WatchdogNotifier {
  const fetcher = options.fetcher ?? fetch;
  const now = options.now ?? Date.now;
  const stderrWrite = options.stderrWrite ?? ((line: string) => {
    process.stderr.write(`${line}\n`);
  });

  return {
    async notify(notification: WatchdogNotification) {
      const record = mapWatchdogNotification(notification, now());
      try {
        const response = await fetcher(options.webhookUrl, {
          method: "POST",
          body: record.detail,
          headers: {
            Title: record.title,
            Priority: "urgent",
            Tags: "warning",
          },
          signal: AbortSignal.timeout(WATCHDOG_WEBHOOK_TIMEOUT_MS),
        });
        if (!response.ok) {
          throw new Error(`webhook returned ${response.status}`);
        }
      } catch (error) {
        stderrWrite(JSON.stringify({
          type: "watchdog-webhook-failed",
          target: notification.target,
          error: error instanceof Error ? error.message : String(error),
        }));
      }
    },
  };
}

async function runCli(argv: string[]): Promise<number> {
  const raw = argv[0];
  if (!raw) {
    process.stderr.write("usage: notify.ts '<canonical-json>'\n");
    return 1;
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch {
    process.stderr.write("invalid canonical notification JSON\n");
    return 1;
  }

  const validated = CanonicalNotificationSchema.safeParse(parsed);
  if (!validated.success) {
    process.stderr.write(`${validated.error.message}\n`);
    return 1;
  }

  const config = loadConfig();
  const sink = createLaptopNotifierSink({
    mode: config.notify.mode,
    journalWrite: (line) => {
      process.stdout.write(`${line}\n`);
    },
    desktopExec: defaultDesktopExec,
  });
  sink.send(validated.data);
  return 0;
}

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