/**
 * macOS service install via launchd (ADR-028).
 *
 * Writes ~/Library/LaunchAgents/com.runuai.host.plist (KeepAlive + RunAtLoad),
 * logs to ~/Library/Logs/Uai/, loads with `launchctl bootstrap gui/<uid>`.
 * The service's PATH is the operator's PATH at install time (so node/pnpm/
 * docker/git resolve); .env.local is loaded by the host process itself.
 */

import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, resolve } from "node:path";

import type { InstallContext, Installer } from "./types";
import { escapeXml, printFile, step } from "./util";

export const LABEL = "com.runuai.host";

const uid = process.getuid?.() ?? 0;
const domain = `gui/${uid}`;
const target = `${domain}/${LABEL}`;
const logDir = resolve(homedir(), "Library", "Logs", "Uai");

export function plistPath(): string {
  return resolve(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
}

export interface PlistInputs {
  execPath: string;
  args: string[];
  cwd: string;
  path: string;
  home: string;
  outLog: string;
  errLog: string;
}

export function renderPlist(p: PlistInputs): string {
  const progArgs = [p.execPath, ...p.args]
    .map((a) => `    <string>${escapeXml(a)}</string>`)
    .join("\n");
  return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${LABEL}</string>
  <key>ProgramArguments</key>
  <array>
${progArgs}
  </array>
  <key>WorkingDirectory</key>
  <string>${escapeXml(p.cwd)}</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>${escapeXml(p.path)}</string>
    <key>HOME</key>
    <string>${escapeXml(p.home)}</string>
  </dict>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ThrottleInterval</key>
  <integer>10</integer>
  <key>StandardOutPath</key>
  <string>${escapeXml(p.outLog)}</string>
  <key>StandardErrorPath</key>
  <string>${escapeXml(p.errLog)}</string>
</dict>
</plist>
`;
}

function inputs(ctx: InstallContext): PlistInputs {
  return {
    execPath: ctx.execPath,
    args: ctx.args,
    cwd: ctx.cwd,
    path: process.env.PATH ?? "",
    home: homedir(),
    outLog: resolve(logDir, "host.out.log"),
    errLog: resolve(logDir, "host.log"),
  };
}

async function install(ctx: InstallContext): Promise<void> {
  const plist = renderPlist(inputs(ctx));
  if (ctx.dryRun) {
    printFile(plistPath(), plist);
    step(true, "mkdir", ["-p", logDir]);
    step(true, "launchctl", ["bootout", target]);
    step(true, "launchctl", ["bootstrap", domain, plistPath()]);
    step(true, "launchctl", ["enable", target]);
    return;
  }
  mkdirSync(logDir, { recursive: true });
  mkdirSync(dirname(plistPath()), { recursive: true });
  writeFileSync(plistPath(), plist);
  // Reload cleanly: bootout any prior instance (ignore "not loaded"), bootstrap.
  step(false, "launchctl", ["bootout", target], { ignoreError: true });
  await bootstrapWithRetry();
  step(false, "launchctl", ["enable", target], { ignoreError: true });
}

/**
 * `bootout` unloads the old job asynchronously, so an immediate `bootstrap`
 * can race it ("Bootstrap failed: 5: Input/output error"). Retry a few times
 * so re-running `install` is idempotent instead of leaving nothing loaded.
 */
async function bootstrapWithRetry(): Promise<void> {
  let lastErr: unknown;
  for (let attempt = 0; attempt < 6; attempt += 1) {
    try {
      step(false, "launchctl", ["bootstrap", domain, plistPath()]);
      return;
    } catch (err) {
      lastErr = err;
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
  }
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}

async function uninstall(dryRun = false): Promise<void> {
  step(dryRun, "launchctl", ["bootout", target], { ignoreError: true });
  if (dryRun) {
    console.log(`$ rm -f ${plistPath()}`);
    return;
  }
  rmSync(plistPath(), { force: true });
}

async function start(dryRun = false): Promise<void> {
  // bootstrap loads + RunAtLoad starts; if already loaded that errors, so also
  // kickstart to be sure it's running.
  step(dryRun, "launchctl", ["bootstrap", domain, plistPath()], {
    ignoreError: true,
  });
  step(dryRun, "launchctl", ["kickstart", target], { ignoreError: true });
}

async function stop(dryRun = false): Promise<void> {
  step(dryRun, "launchctl", ["bootout", target], { ignoreError: true });
}

async function restart(dryRun = false): Promise<void> {
  step(dryRun, "launchctl", ["kickstart", "-k", target], { ignoreError: true });
}

async function status(): Promise<string> {
  const out = step(false, "launchctl", ["print", target], { ignoreError: true });
  if (!out) return "not installed / not loaded";
  const state = /state = (\S+)/.exec(out)?.[1] ?? "unknown";
  const pid = /pid = (\d+)/.exec(out)?.[1];
  return `${state}${pid ? ` (pid ${pid})` : ""}`;
}

export const installer: Installer = {
  install,
  uninstall,
  start,
  stop,
  restart,
  status,
};
