import { spawn } from 'node:child_process';
import type { ChildProcess } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

// Supervises Radicale, the CalDAV/CardDAV server this sidecar fronts.
//
// WHY A REAL SERVER AND NOT OUR OWN. See docs/nextcloud-replacement.md. The short version: iCalendar and
// vCard are easy, but `sync-collection` REPORT, RRULE expansion, VTIMEZONE and ctag/etag semantics are
// not — and when they are subtly wrong a phone does not error, it silently stops syncing or silently
// duplicates every event. Radicale has been getting that right since 2008.
//
// AUTH. Radicale runs with `type = http_x_remote_user`, meaning it trusts an `X-Remote-User` header
// completely and does no authentication of its own. That is safe here for exactly the reason every
// other sidecar's `X-Officer-User` is safe: it binds LOOPBACK ONLY, so nothing but this sidecar can
// reach it, and this sidecar is only reachable through the platform, which is the thing that
// authenticates. If Radicale is ever bound to a non-loopback interface this becomes an open door —
// hence the explicit hostname below rather than a default.

export type RadicaleHandle = {
  port: number;
  url: string;
  stop: () => void;
  isRunning: () => boolean;
};

type StartParams = {
  /** Where collections live. Created if absent. */
  storagePath: string;
  /** Where the generated config is written. Never hand-edit it; it is rewritten on every boot. */
  configPath: string;
  port: number;
};

// Radicale's own config format. Regenerated at every boot on purpose: the config is derived state, and
// a hand-edit that silently disagreed with what this file thinks is true would be very hard to debug.
const configFor = (storagePath: string, port: number) => `# GENERATED by officer-caldav on every boot.
# Hand edits are lost. See src/servers/sidecar/caldav/radicale.ts.
[server]
hosts = 127.0.0.1:${port}
# The platform terminates TLS; this hop is loopback.
ssl = False

[auth]
# Trusts X-Remote-User outright. Safe ONLY because of the loopback bind above — see the note in
# radicale.ts before changing either.
type = http_x_remote_user

[storage]
type = multifilesystem
filesystem_folder = ${storagePath}

[rights]
# The owner is the only principal that exists; single-user is a platform invariant.
type = owner_only

[logging]
level = warning
`;

export function startRadicale({ storagePath, configPath, port }: StartParams): RadicaleHandle {
  mkdirSync(storagePath, { recursive: true });
  writeFileSync(configPath, configFor(storagePath, port));

  let child: ChildProcess | null = null;
  let stopped = false;
  let restarts = 0;

  const launch = () => {
    if (stopped) return;
    child = spawn('radicale', ['--config', configPath], {
      stdio: ['ignore', 'pipe', 'pipe'],
      // Radicale must not inherit the platform's environment wholesale — Bun auto-loads .env into every
      // process started in the platform directory, and there is no reason for a calendar server to hold
      // an Anthropic key or a database URL.
      env: { PATH: process.env.PATH ?? '', HOME: process.env.HOME ?? '' },
    });

    child.stdout?.on('data', (buf: Buffer) => process.stdout.write(`[radicale] ${buf}`));
    child.stderr?.on('data', (buf: Buffer) => process.stderr.write(`[radicale] ${buf}`));

    child.on('exit', (code, signal) => {
      child = null;
      if (stopped) return;
      // Backoff, capped. A crash loop should be visible in the logs rather than a busy spin, and the
      // sidecar itself stays up so /_health can keep reporting the truth.
      restarts += 1;
      const delay = Math.min(30_000, 500 * 2 ** Math.min(restarts, 6));
      console.error(`[caldav] radicale exited (code=${code} signal=${signal}); restarting in ${delay}ms`);
      setTimeout(launch, delay);
    });
  };

  launch();

  return {
    port,
    url: `http://127.0.0.1:${port}`,
    isRunning: () => child !== null,
    stop: () => {
      stopped = true;
      child?.kill('SIGTERM');
      child = null;
    },
  };
}

/** Resolve the paths this sidecar owns under DATA_PATH. The platform never opens any of them. */
export function davPaths(dataPath: string) {
  const root = join(dataPath, 'dav');
  return { root, storagePath: join(root, 'collections'), configPath: join(root, 'radicale.conf') };
}
