import { execFileSync, spawn } from "node:child_process";
import type { NotificationBackend, NotifyConfig } from "./types.ts";

const ESC = "\x1b";
const BEL = "\x07";
const ST = `${ESC}\\`;

type NotifierDependencies = {
  spawn: typeof spawn;
  readTmuxClientInfo: () => string | undefined;
  platform: NodeJS.Platform;
};

export function createNotifier(dependencies: Partial<NotifierDependencies> = {}) {
  const spawnCommand = dependencies.spawn ?? spawn;
  const tmuxClientInfo = dependencies.readTmuxClientInfo ?? readTmuxClientInfo;
  const platform = dependencies.platform ?? process.platform;

  return {
    getBackend(): NotificationBackend {
      return detectNotificationBackend(tmuxClientInfo, platform);
    },
    sendNotification(config: NotifyConfig): void {
      try {
        const backend = detectNotificationBackend(tmuxClientInfo, platform);
        if (backend === "kitty") notifyKitty(config);
        else if (backend === "apple-script") notifyAppleScript(config.title, config.body, spawnCommand);
        else notifyOsc777(config.title, config.body);

        runSoundHook(config.soundCommand, spawnCommand);
      } catch {}
    },
  };
}

const defaultNotifier = createNotifier();

export function sendNotification(config: NotifyConfig): void {
  defaultNotifier.sendNotification(config);
}

export function getNotificationBackend(): NotificationBackend {
  return defaultNotifier.getBackend();
}

export function detectNotificationBackend(
  readClientInfo: () => string | undefined = readTmuxClientInfo,
  platform: NodeJS.Platform = process.platform,
): NotificationBackend {
  if (isKitty(readClientInfo)) return "kitty";
  return platform === "darwin" ? "apple-script" : "osc-777";
}

function isKitty(readClientInfo: () => string | undefined): boolean {
  if (!process.env.TMUX) return Boolean(process.env.KITTY_WINDOW_ID);

  const info = readClientInfo()?.toLowerCase();
  if (info) return info.includes("kitty");
  return Boolean(process.env.KITTY_WINDOW_ID);
}

function readTmuxClientInfo(): string | undefined {
  try {
    return execFileSync("tmux", ["display-message", "-p", "#{client_termname} #{client_termtype}"], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    });
  } catch {
    return undefined;
  }
}

function notifyKitty(config: NotifyConfig): void {
  const safeTitle = Buffer.from(config.title, "utf8").toString("base64");
  const safeBody = Buffer.from(config.body, "utf8").toString("base64");
  const id = sanitizeMetadataValue(config.notificationId);
  writeSequence(
    `${ESC}]99;i=${id}:d=0:e=1:o=${config.visibility}:a=${config.action};${safeTitle}${ST}`,
  );
  writeSequence(`${ESC}]99;i=${id}:p=body:e=1;${safeBody}${ST}`);
}

function notifyAppleScript(title: string, body: string, spawnCommand: typeof spawn): void {
  const script = `display notification ${appleScriptString(body)} with title ${appleScriptString(title)}`;
  spawnDetached(spawnCommand, "osascript", ["-e", script]);
}

function notifyOsc777(title: string, body: string): void {
  process.stdout.write(`${ESC}]777;notify;${sanitizeOsc777(title)};${sanitizeOsc777(body)}${BEL}`);
}

function writeSequence(sequence: string): void {
  process.stdout.write(wrapForTmux(sequence));
}

function wrapForTmux(sequence: string): string {
  if (!process.env.TMUX) return sequence;
  return `${ESC}Ptmux;${sequence.replaceAll(ESC, ESC + ESC)}${ST}`;
}

function sanitizeMetadataValue(value: string): string {
  const sanitized = value.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 128);
  return sanitized || `pi-${process.pid}`;
}

function sanitizeOsc777(value: string): string {
  return value.replace(/[\x00-\x1f\x7f;]/g, " ").replace(/\s+/g, " ").trim();
}

function appleScriptString(value: string): string {
  return `"${value.replace(/[\x00-\x1f\x7f]/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"').trim()}"`;
}

function runSoundHook(command: string | undefined, spawnCommand: typeof spawn): void {
  if (!command) return;
  spawnDetached(spawnCommand, command, [], true);
}

function spawnDetached(spawnCommand: typeof spawn, command: string, args: string[], shell = false): void {
  const child = spawnCommand(command, args, {
    shell,
    detached: true,
    stdio: "ignore",
  });
  child.on?.("error", () => {});
  child.unref();
}
