/**
 * CliDeps factory — PLAN_RELAY_PHASE1 C2, extended for the native tunnel (PLAN_TUNNEL_AUTOMATION B5).
 * Wires the abstract `CliDeps` seams (consumed by `runCli`) to their real implementations: env-driven
 * config, the on-disk keystore, identity generation (Ed25519 + P-256), §4.5 pairing redemption, the
 * native enroll + frpc provisioning + frpc.toml writer, the supervised tunnel, and the two-unit OS
 * service install. All side effects live here so `cli.ts`/`runCli` stay pure and unit-testable.
 */
import { execFile } from 'node:child_process'
import { X509Certificate } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { homedir, userInfo } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { CliDeps, NativeEnrollResult } from '../cli.js'
import type { AgentConfig } from '../config/agentConfig.js'
import { resolveHostIdentity, saveHostRecord } from '../config/hostRecord.js'
import { loadAgentConfig } from '../config/agentConfig.js'
import { openKeystore } from '../keys/keystore.js'
import { generateIdentity, generateP256Identity } from '../keys/identity.js'
import type { AgentIdentity } from '../keys/identity.js'
import type { Keystore } from '../keys/keystore.js'
import { redeemPairingCode } from '../enroll/pair.js'
import { runTunnel } from '../transport/runTunnel.js'
import { buildNativeFrpcToml } from '../transport/frpcToml.js'
import { superviseFrpc } from '../transport/frpSupervise.js'
import { provisionFrpc } from '../provision/frpcBinary.js'
import { startNativeAutoRenew } from '../certs/nativeRenew.js'
import {
  probeLoopbackBaseApp,
  renderHealthStatus,
  runHealthProbe,
  startHealthMonitor,
} from '../health/probe.js'
import { createLogger } from '../log/logger.js'
import { ensureAllowedOrigin } from '../service/originConfig.js'
import {
  buildInstallOptions,
  detectPlatform,
  NATIVE_ORIGIN_ZONE,
  installService as installServiceUnit,
  uninstallService as uninstallServiceUnit,
  type InstallDeps,
  type ServicePlatform,
} from '../service/install.js'

/** Keystore file names in `stateDir` (kept in lockstep with `keys/keystore.ts`). */
const KEYSTORE_CERT = 'agent.cert.pem'
const KEYSTORE_KEY = 'agent.key.pem'
const KEYSTORE_CA = 'agent.ca.pem'
const FRPC_TOML = 'frpc.toml'
const FRPC_LOG = 'frpc.log'
const BASE_APP_ENV_FILE = 'base-app.env'
const DEFAULT_LOCAL_PORT = 3000

/** Resolve this process's own executable path (the bundled `dist/cli.js`) for the service unit. */
function selfBinPath(): string {
  return fileURLToPath(import.meta.url)
}

function runCommand(cmd: string, args: readonly string[]): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    execFile(cmd, [...args], (err) => (err ? reject(err) : resolve()))
  })
}

function realInstallDeps(): InstallDeps {
  return {
    writeFile: (path, content) => {
      mkdirSync(dirname(path), { recursive: true })
      writeFileSync(path, content)
    },
    runCommand,
    getuid: () => (typeof process.getuid === 'function' ? process.getuid() : 0),
    homedir,
    username: () => userInfo().username,
    binPath: selfBinPath,
  }
}

/** Map the current OS to its service manager, or fail fast with a clear message. */
function requirePlatform(): ServicePlatform {
  const platform = detectPlatform(process.platform)
  if (platform === null) {
    throw new Error(`service install/uninstall is unsupported on platform '${process.platform}'`)
  }
  return platform
}

/** Parse a positive-integer PORT from the env (falls back to the base-app default 3000). */
function resolveLocalPort(): number {
  const raw = process.env.PORT
  const port = raw ? Number.parseInt(raw, 10) : NaN
  return Number.isInteger(port) && port > 0 ? port : DEFAULT_LOCAL_PORT
}

/** Native enroll: build the P-256 CSR + POST /enroll (via the frozen redeem flow), store the cert. */
async function enrollNative(
  cfg: AgentConfig,
  code: string,
  id: AgentIdentity,
  ks: Keystore,
): Promise<NativeEnrollResult> {
  const enroll = await redeemPairingCode(cfg.enrollUrl, code, id, ks, { allowMissingContentSecret: true })
  // Write the identifiers down: the long-running `run` process has no other way to learn them, and
  // without them every log line from the tunnel reads `{"subdomain":null,"hostId":null}`.
  saveHostRecord(cfg.stateDir, { hostId: enroll.hostId, subdomain: enroll.subdomain })
  return { hostId: enroll.hostId, subdomain: enroll.subdomain }
}

/**
 * Write the native `frpc.toml` into `stateDir`, pointing frpc at this host's keystore cert/key/CA and
 * the loopback base app. Also materializes the base-app ALLOWED_ORIGINS env file. The frps shared
 * token comes from `FRP_AUTH_TOKEN` (deploy secret; never logged). NOTE: the frpc binary run/e2e is
 * pending the B3 tar.gz extraction; this seam only emits the config.
 */
function writeFrpcConfig(cfg: AgentConfig, subdomain: string): void {
  const domain = process.env.TUNNEL_DOMAIN
  const toml = buildNativeFrpcToml({
    subdomain,
    localPort: resolveLocalPort(),
    authToken: process.env.FRP_AUTH_TOKEN ?? '',
    certFile: join(cfg.stateDir, KEYSTORE_CERT),
    keyFile: join(cfg.stateDir, KEYSTORE_KEY),
    trustedCaFile: join(cfg.stateDir, KEYSTORE_CA),
  })
  mkdirSync(cfg.stateDir, { recursive: true })
  writeFileSync(join(cfg.stateDir, FRPC_TOML), toml, { mode: 0o600 })
  if (domain) {
    ensureAllowedOrigin(join(cfg.stateDir, BASE_APP_ENV_FILE), subdomain, domain, undefined, NATIVE_ORIGIN_ZONE)
  }
}

/** The stored frp-client leaf's `notAfter`, or null if no cert/parse failure (non-secret metadata). */
function certNotAfter(ks: Keystore): Date | null {
  const cert = ks.loadCert()
  if (cert === null) return null
  try {
    return new X509Certificate(cert.certPem).validToDate
  } catch {
    return null
  }
}

/**
 * The single frpc log path in `stateDir`. Used by BOTH the supervisor's file-logging spawn (writer)
 * and `readFrpcLog` (reader) so the health probe can never scan a different file than frpc writes.
 */
export function frpcLogPath(stateDir: string): string {
  return join(stateDir, FRPC_LOG)
}

/** Read the accumulated frpc log (empty string if not yet written) for the proxy-started scan. */
export function readFrpcLog(stateDir: string): string {
  const path = frpcLogPath(stateDir)
  if (!existsSync(path)) return ''
  try {
    return readFileSync(path, 'utf8')
  } catch {
    return ''
  }
}

/**
 * Native run-loop (B4/H4 + A5): supervise the pinned frpc child with restart-on-exit backoff while a
 * periodic health probe (frpc alive, base-app loopback reachable, proxy-started, cert-not-expiring)
 * logs NON-SECRET status only (INV9), AND auto-renew the frp-client leaf at ~2/3 TTL so the tunnel
 * never drops on cert expiry (A5): a successful renewal restarts frpc onto the fresh leaf, a 403
 * revoke tears the tunnel down, and a failed renewal retries with backoff without crashing the
 * supervisor. Resolves when the supervisor stops (SIGTERM/SIGINT).
 */
function superviseNative(cfg: AgentConfig, ks: Keystore): Promise<number> {
  const logger = createLogger('info')
  const binPath = join(cfg.stateDir, 'bin', 'frpc')
  const tomlPath = join(cfg.stateDir, FRPC_TOML)
  // Tee the frpc child's stdout/stderr into `<stateDir>/frpc.log` (the SAME path `readFrpcLog`
  // scans below) so the proxy-started health sub-check has real content — without this wiring the
  // log stays empty and `HealthReport.healthy` can never be true (B4/H4 goal).
  const handle = superviseFrpc(binPath, tomlPath, { logger, logFile: frpcLogPath(cfg.stateDir) })
  const port = resolveLocalPort()
  const monitor = startHealthMonitor(
    () =>
      runHealthProbe({
        isFrpcAlive: () => handle.isChildAlive(),
        probeBaseApp: () => probeLoopbackBaseApp(port, (url) => fetch(url)),
        readFrpcLog: () => readFrpcLog(cfg.stateDir),
        certNotAfter: () => certNotAfter(ks),
        now: () => new Date(),
      }),
    (report) => {
      // INV9: only non-secret identifiers (subdomain/host id/expiry date) + boolean flags are logged.
      const host = resolveHostIdentity(cfg, () => ks.loadCert()?.certPem ?? null)
      const ids = { ...host, certNotAfter: certNotAfter(ks) }
      for (const line of renderHealthStatus(ids, report)) logger.log('info', line)
    },
  )
  // A5: silently renew the leaf before it expires. Restart frpc onto the fresh cert on rotation;
  // stop the whole supervisor on a 403 revoke (INV12). Null ⇒ unenrolled (auto-renew disabled).
  const autoRenew = startNativeAutoRenew(
    cfg,
    ks,
    {
      restartChild: () => handle.restartChild(),
      stop: () => {
        void handle.stop()
      },
    },
    logger,
  )
  const onSignal = (): void => {
    void handle.stop()
  }
  process.once('SIGTERM', onSignal)
  process.once('SIGINT', onSignal)
  return handle.done.finally(() => {
    monitor.stop()
    autoRenew?.stop()
  })
}

/** Build the concrete CliDeps used by the real CLI entrypoint. */
export function createCliDeps(): CliDeps {
  return {
    loadConfig: () => loadAgentConfig(process.env),
    openKeystore: (stateDir) => openKeystore(stateDir),
    generateIdentity: () => generateIdentity(),
    generateP256Identity: () => generateP256Identity(),
    redeem: (cfg: AgentConfig, code, id, ks) => redeemPairingCode(cfg.enrollUrl, code, id, ks),
    enrollNative: (cfg, code, id, ks) => enrollNative(cfg, code, id, ks),
    provisionFrpc: async (cfg) => {
      const result = await provisionFrpc({
        platform: process.platform,
        arch: process.arch,
        binDir: join(cfg.stateDir, 'bin'),
      })
      return result.binPath
    },
    writeFrpcConfig: (cfg, subdomain) => writeFrpcConfig(cfg, subdomain),
    nativeConfigExists: (cfg) => existsSync(join(cfg.stateDir, FRPC_TOML)),
    superviseFrpc: (cfg, ks) => superviseNative(cfg, ks),
    runTunnel: async (cfg, ks) => {
      const handle = await runTunnel(cfg, ks)
      const onSignal = (): void => {
        void handle.stop()
      }
      process.once('SIGTERM', onSignal)
      process.once('SIGINT', onSignal)
      return handle.done
    },
    resolveInstallOptions: () => buildInstallOptions(process.env),
    installService: (cfg, options) =>
      installServiceUnit(cfg, requirePlatform(), realInstallDeps(), options),
    uninstallService: () => uninstallServiceUnit(requirePlatform(), { runCommand, homedir }),
    print: (line) => {
      process.stdout.write(`${line}\n`)
    },
  }
}
