/**
 * Service install dispatcher — PLAN_RELAY_AGENT T17. Detects the platform, writes the unit, and
 * loads it. REFUSES to install as root (EXPLORE §4d least privilege). All IO is injected so the
 * logic is unit-testable without touching the real system.
 */
import type { AgentConfig } from '../config/agentConfig.js'
import {
  buildLaunchdPlist,
  launchdLoadCommand,
  launchdPlistPath,
  launchdUnloadCommand,
  type ServiceEnv,
} from './launchd.js'
import {
  buildSystemdUnit,
  systemdDisableCommand,
  systemdEnableCommand,
  systemdUnitPath,
  type SystemdUnitOptions,
} from './systemd.js'
import { DEFAULT_ORIGIN_ZONE, mergeOrigins, subdomainOrigin } from './originConfig.js'

export type ServicePlatform = 'launchd' | 'systemd'

/**
 * Per-host packaging inputs threaded to the unit writers (PLAN_NATIVE_TUNNEL S2). All optional so
 * existing callers (and relay callers) are unaffected — an empty object writes the historical unit.
 */
export interface InstallOptions {
  /** S0 env injected into the supervised process (BIND_HOST, ALLOWED_ORIGINS, PORT, …). */
  readonly env?: ServiceEnv
  /** systemd `EnvironmentFile=` path (preferred over inline for values best kept off the unit). */
  readonly envFile?: string
  /** DNS zone label for the tunnel origin (`terminal` for native-tunnel hosts, default `term`). */
  readonly zone?: string
  /** Parent domain (e.g. `yaojia.wang`); with `cfg.subdomain` derives the tunnel ALLOWED_ORIGINS. */
  readonly domain?: string
}

/** S0 base-app env vars the install CLI bakes into the per-host unit, passed through verbatim. */
const BASE_APP_ENV_KEYS = ['ALLOWED_ORIGINS', 'PORT', 'SHELL_PATH', 'IDLE_TTL', 'USE_TMUX'] as const

/**
 * Tunnel hosts MUST bind loopback. At the relay the device-cert mTLS is the ONLY auth gate, so a
 * default `0.0.0.0` bind (`src/config.ts`) would serve an unauth'd shell on the LAN, bypassing mTLS
 * entirely (PLAN_NATIVE_TUNNEL S0/R2). `buildInstallOptions` therefore defaults `BIND_HOST` here.
 */
export const TUNNEL_DEFAULT_BIND_HOST = '127.0.0.1'

/** Native-tunnel origin zone → `https://<subdomain>.terminal.<domain>`; overridable via TUNNEL_ZONE. */
export const TUNNEL_ORIGIN_ZONE = 'terminal'

/**
 * Resolve the per-host `InstallOptions` from the process environment (PLAN_NATIVE_TUNNEL S0/S2).
 * Sources the S0 base-app env — defaulting `BIND_HOST` to loopback so a tunnel install can never
 * emit a LAN-exposed unit — plus the tunnel-origin `domain`/`zone` used to derive ALLOWED_ORIGINS.
 * Pure/immutable: `env` is a parameter, so this is unit-testable without touching real process state.
 */
export function buildInstallOptions(env: NodeJS.ProcessEnv): InstallOptions {
  const passthrough = Object.fromEntries(
    BASE_APP_ENV_KEYS.map((key) => [key, env[key]] as [string, string | undefined]).filter(
      (entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0,
    ),
  )
  const serviceEnv: ServiceEnv = { BIND_HOST: env.BIND_HOST || TUNNEL_DEFAULT_BIND_HOST, ...passthrough }
  const domain = env.TUNNEL_DOMAIN
  const envFile = env.AGENT_ENV_FILE
  return {
    env: serviceEnv,
    ...(domain ? { domain, zone: env.TUNNEL_ZONE || TUNNEL_ORIGIN_ZONE } : {}),
    ...(envFile ? { envFile } : {}),
  }
}

export class RootRefusedError extends Error {
  constructor() {
    super('refusing to install the agent service as root — run as the logged-in user (least privilege)')
    this.name = 'RootRefusedError'
  }
}

export interface InstallDeps {
  writeFile(path: string, content: string): void
  runCommand(cmd: string, args: readonly string[]): Promise<void>
  getuid(): number
  homedir(): string
  username(): string
  binPath(): string
}

/** Map a Node platform to its service manager, or null if unsupported. */
export function detectPlatform(os: NodeJS.Platform): ServicePlatform | null {
  if (os === 'darwin') return 'launchd'
  if (os === 'linux') return 'systemd'
  return null
}

/**
 * Resolve the env map injected into the unit. When a `domain` (+ `cfg.subdomain`) is supplied, the
 * tunnel origin `https://<subdomain>.<zone>.<domain>` is merged into ALLOWED_ORIGINS so the base app
 * trusts its own tunnel host — never weakening any origin the caller already provided. Immutable.
 */
function resolveEnv(cfg: AgentConfig, options: InstallOptions): ServiceEnv {
  const base = options.env ?? {}
  if (!options.domain || !cfg.subdomain) return base
  const origin = subdomainOrigin(cfg.subdomain, options.domain, options.zone ?? DEFAULT_ORIGIN_ZONE)
  return { ...base, ALLOWED_ORIGINS: mergeOrigins(base.ALLOWED_ORIGINS, origin) }
}

/** Write + load the service unit for `platform`. Throws RootRefusedError if running as root. */
export async function installService(
  cfg: AgentConfig,
  platform: ServicePlatform,
  deps: InstallDeps,
  options: InstallOptions = {},
): Promise<void> {
  if (deps.getuid() === 0) throw new RootRefusedError()
  const bin = deps.binPath()
  const env = resolveEnv(cfg, options)
  if (platform === 'launchd') {
    const path = launchdPlistPath(deps.homedir())
    deps.writeFile(path, buildLaunchdPlist(bin, env))
    const { cmd, args } = launchdLoadCommand(path)
    await deps.runCommand(cmd, args)
    return
  }
  const path = systemdUnitPath(deps.homedir())
  const systemdOptions: SystemdUnitOptions = options.envFile
    ? { env, envFile: options.envFile }
    : { env }
  deps.writeFile(path, buildSystemdUnit(bin, deps.username(), systemdOptions))
  const { cmd, args } = systemdEnableCommand()
  await deps.runCommand(cmd, args)
}

/** Unload the service unit for `platform`. */
export async function uninstallService(
  platform: ServicePlatform,
  deps: Pick<InstallDeps, 'runCommand' | 'homedir'>,
): Promise<void> {
  if (platform === 'launchd') {
    const { cmd, args } = launchdUnloadCommand(launchdPlistPath(deps.homedir()))
    await deps.runCommand(cmd, args)
    return
  }
  const { cmd, args } = systemdDisableCommand()
  await deps.runCommand(cmd, args)
}
