/**
 * Linux systemd unit writer — PLAN_RELAY_AGENT T17. Runs as the LOGGED-IN USER (never root,
 * EXPLORE §4d), restart-on-failure; no secrets in the unit (key/cert stay in the keystore).
 *
 * PLAN_NATIVE_TUNNEL S2: the unit can inject the per-host tunnel env via an `EnvironmentFile=`
 * line (preferred — keeps values out of the world-readable unit) and/or inline `Environment=`
 * lines from a caller-supplied env map. Inline `Environment=` is emitted after `EnvironmentFile=`
 * so an explicit value overrides the file on conflict.
 *
 * PLAN_TUNNEL_AUTOMATION B5 (FIX M-host-2service): the writer is PARAMETERIZED on the full
 * `ExecStart` command + `Description`, so a native-tunnel install emits TWO distinct units — the
 * base-app (`node dist/server.js`, carrying the base-app env) and the agent (`<bin> run`, which
 * supervises frpc). Base-app env is routed to the base-app unit ONLY by the caller (`install.ts`).
 */
import type { ServiceEnv } from './launchd.js'

/** The native-tunnel agent unit (supervises frpc). */
const AGENT_UNIT = 'web-terminal-agent.service'
/** The base-app unit (`node dist/server.js`, loopback-bound). */
const BASE_APP_UNIT = 'web-terminal-base-app.service'

/** DEL (0x7F) and everything below the printable ASCII range are rejected in env values. */
const FIRST_PRINTABLE_ASCII = 0x20
const DEL_CODE = 0x7f

export interface SystemdUnitOptions {
  /** Inline env map → one `Environment="KEY=value"` line each (keys sorted, values escaped). */
  readonly env?: ServiceEnv
  /** Path referenced by a single `EnvironmentFile=` line (preferred over inline for secrets). */
  readonly envFile?: string
}

export function agentUnitName(): string {
  return AGENT_UNIT
}
export function baseAppUnitName(): string {
  return BASE_APP_UNIT
}
/** Back-compat alias for the agent unit name. */
export function systemdUnitName(): string {
  return AGENT_UNIT
}

export function systemdUnitPath(homedir: string, unitName: string = AGENT_UNIT): string {
  return `${homedir}/.config/systemd/user/${unitName}`
}

/**
 * True if `value` contains any control character (C0 range below 0x20, or DEL 0x7F). A raw
 * newline/CR would terminate the current line and let the remainder inject arbitrary directives into
 * the `[Service]` section — so such values are rejected rather than escaped.
 */
function hasControlChar(value: string): boolean {
  for (const char of value) {
    const code = char.codePointAt(0)
    if (code === undefined) continue
    if (code < FIRST_PRINTABLE_ASCII || code === DEL_CODE) return true
  }
  return false
}

/**
 * Reject ANY field interpolated into the unit that carries a control character. EVERY value written
 * into the unit (ExecStart, User, Description, EnvironmentFile path, and each Environment key+value)
 * flows through this one guard, so no field can smuggle a newline that starts a new `[Service]`
 * directive. Fail loud rather than escape — these fields are never legitimately multi-line.
 */
function assertNoControlChar(fieldName: string, value: string): void {
  if (hasControlChar(value)) {
    throw new Error(
      `refusing to write systemd unit: ${fieldName} contains a control character ` +
        '(newline/CR/etc.) that could corrupt the [Service] section',
    )
  }
}

/** Quote a systemd `Environment=` value: reject control chars (key AND value), then escape `\` and `"`. */
function quoteEnvAssignment(key: string, value: string): string {
  assertNoControlChar(`Environment key '${key}'`, key)
  assertNoControlChar(`Environment value for '${key}'`, value)
  const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
  return `"${key}=${escaped}"`
}

/** Build the `EnvironmentFile=` / `Environment=` lines (empty array when neither is supplied). */
function environmentLines(options: SystemdUnitOptions): readonly string[] {
  const lines: string[] = []
  if (options.envFile) {
    assertNoControlChar('EnvironmentFile path', options.envFile)
    lines.push(`EnvironmentFile=${options.envFile}`)
  }
  const entries = Object.entries(options.env ?? {}).sort(([a], [b]) => a.localeCompare(b))
  for (const [key, value] of entries) {
    lines.push(`Environment=${quoteEnvAssignment(key, value)}`)
  }
  return lines
}

/**
 * Build a unit whose `ExecStart` is `execStart` (a full command line, e.g. `<bin> run` or
 * `node dist/server.js`); Restart=on-failure; User=<user> (never root). When `options.env`/
 * `options.envFile` are supplied, the per-host tunnel env is injected. Pure/immutable.
 */
export function buildSystemdUnit(
  execStart: string,
  user: string,
  options: SystemdUnitOptions = {},
  description: string = 'web-terminal host agent',
): string {
  // Guard every non-env field the same way env values are guarded — a newline in ExecStart/User/
  // Description would otherwise inject a `[Service]` directive on the next line.
  assertNoControlChar('ExecStart', execStart)
  assertNoControlChar('User', user)
  assertNoControlChar('Description', description)
  return [
    '[Unit]',
    `Description=${description}`,
    'After=network-online.target',
    '',
    '[Service]',
    'Type=simple',
    `ExecStart=${execStart}`,
    'Restart=on-failure',
    'RestartSec=1',
    `User=${user}`,
    ...environmentLines(options),
    '',
    '[Install]',
    'WantedBy=default.target',
    '',
  ].join('\n')
}

export function systemdEnableCommand(
  unitName: string = AGENT_UNIT,
): { cmd: string; args: readonly string[] } {
  return { cmd: 'systemctl', args: ['--user', 'enable', '--now', unitName] }
}
export function systemdDisableCommand(
  unitName: string = AGENT_UNIT,
): { cmd: string; args: readonly string[] } {
  return { cmd: 'systemctl', args: ['--user', 'disable', '--now', unitName] }
}
