/**
 * Activation-side supervisor for the Mosaic lease broker (issue #869, Point-1
 * C3). #828 shipped fail-closed enforcement hooks (`mutator-gate.py`,
 * `receipt-observer-client.py`) with nothing that guaranteed `daemon.py` was
 * running or that its socket existed before a gated runtime started. This
 * module:
 *
 *  - resolves the broker socket/state paths and the on-disk locations of the
 *    supervisor artifacts, deterministically and consistently with
 *    `defaultLeaseBrokerSocket` in `../commands/launch.ts`;
 *  - idempotently applies (materializes) a systemd `--user` unit plus the
 *    wrapper script and daemon sources it execs, mirroring the tmux fleet
 *    unit convention in `framework/systemd/user/`;
 *  - exposes a health predicate other cards (e.g. the C1 activation probe)
 *    can call to learn whether a broker supervisor is present and healthy.
 *
 * `applyBrokerSupervisor` only writes files under the paths it is given. It
 * never runs `systemctl`, never starts `daemon.py`, and never touches a real
 * host's `~/.config` unless the caller explicitly resolves paths there.
 * Enabling/starting the unit is a separate, later, out-of-scope step.
 */
import { chmod, copyFile, mkdir, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';

const UNIT_NAME = 'mosaic-lease-broker.service';
const WRAPPER_SCRIPT_NAME = 'start-lease-broker.sh';

/** Co-located modules `daemon.py` imports at runtime; kept alongside it. */
const DAEMON_SOURCE_FILE_NAMES = [
  'daemon.py',
  'lease_generation.py',
  'normative_fragments.py',
  'receipt_challenge.py',
  'receipt_observer.py',
] as const;

export interface ResolveBrokerSupervisorPathsOptions {
  /** `~/.config/mosaic` (or an override) — where installed tool copies live. */
  mosaicHome: string;
  /** Root of the checked-out `framework/` directory (canonical file source). */
  frameworkRoot: string;
  /** Defaults to `process.env`; pass a fake for tests. */
  env?: NodeJS.ProcessEnv;
  /** Defaults to `os.homedir()`; pass a temp dir in tests. */
  homeDir?: string;
  /** Defaults to `process.getuid()` (or 0); pass a fake for tests. */
  uid?: number;
}

export interface BrokerSupervisorPaths {
  readonly mosaicHome: string;
  readonly frameworkRoot: string;
  readonly systemdUserDir: string;
  readonly leaseBrokerToolsDir: string;
  readonly unitSourcePath: string;
  readonly unitTargetPath: string;
  readonly wrapperSourcePath: string;
  readonly wrapperTargetPath: string;
  readonly daemonSourcePaths: readonly string[];
  readonly daemonTargetPaths: readonly string[];
  /**
   * Resolved with the same precedence as `defaultLeaseBrokerSocket` in
   * `../commands/launch.ts`: an explicit `MOSAIC_LEASE_BROKER_SOCKET`, else
   * `$XDG_RUNTIME_DIR/mosaic-lease/broker.sock`, else
   * `/run/user/<uid>/mosaic-lease/broker.sock`.
   */
  readonly socketPath: string;
  /** Colocated next to the socket, matching the broker's own generation-file convention. */
  readonly statePath: string;
}

/**
 * Resolve the lease broker socket path alone, with the same precedence as
 * `defaultLeaseBrokerSocket` in `../commands/launch.ts`. Exported so callers
 * (and tests) can assert the two stay in agreement without importing the CLI
 * command module.
 */
export function resolveLeaseBrokerSocketPath(
  env: NodeJS.ProcessEnv = process.env,
  uid: number = typeof process.getuid === 'function' ? process.getuid() : 0,
): string {
  const explicit = env['MOSAIC_LEASE_BROKER_SOCKET'];
  if (explicit) return explicit;
  const runtimeDir = env['XDG_RUNTIME_DIR'];
  if (runtimeDir) return join(runtimeDir, 'mosaic-lease', 'broker.sock');
  return join('/run/user', String(uid), 'mosaic-lease', 'broker.sock');
}

/** Resolve every path the supervisor apply/health functions need, deterministically. */
export function resolveBrokerSupervisorPaths(
  options: ResolveBrokerSupervisorPathsOptions,
): BrokerSupervisorPaths {
  const { mosaicHome, frameworkRoot } = options;
  const env = options.env ?? process.env;
  const homeDir = options.homeDir ?? homedir();
  const systemdUserDir = join(homeDir, '.config', 'systemd', 'user');
  const leaseBrokerToolsDir = join(mosaicHome, 'tools', 'lease-broker');
  const frameworkLeaseBrokerDir = join(frameworkRoot, 'tools', 'lease-broker');
  const socketPath = resolveLeaseBrokerSocketPath(env, options.uid);
  const statePath = join(dirname(socketPath), 'state.json');

  return {
    mosaicHome,
    frameworkRoot,
    systemdUserDir,
    leaseBrokerToolsDir,
    unitSourcePath: join(frameworkRoot, 'systemd', 'user', UNIT_NAME),
    unitTargetPath: join(systemdUserDir, UNIT_NAME),
    wrapperSourcePath: join(frameworkLeaseBrokerDir, WRAPPER_SCRIPT_NAME),
    wrapperTargetPath: join(leaseBrokerToolsDir, WRAPPER_SCRIPT_NAME),
    daemonSourcePaths: DAEMON_SOURCE_FILE_NAMES.map((name) => join(frameworkLeaseBrokerDir, name)),
    daemonTargetPaths: DAEMON_SOURCE_FILE_NAMES.map((name) => join(leaseBrokerToolsDir, name)),
    socketPath,
    statePath,
  };
}

export interface ApplyBrokerSupervisorResult {
  readonly installedFiles: readonly string[];
}

/**
 * Idempotently materialize the supervisor unit, its wrapper script, and the
 * daemon sources it execs. Safe to call on every reseed: every write is a
 * deterministic overwrite of the same target path from the same source, so a
 * second call reproduces identical bytes/modes and never errors.
 *
 * Never runs `systemctl`; the caller decides separately whether/when to
 * `daemon-reload`/`enable`/`start` the installed unit.
 */
export async function applyBrokerSupervisor(
  paths: BrokerSupervisorPaths,
): Promise<ApplyBrokerSupervisorResult> {
  await mkdir(paths.leaseBrokerToolsDir, { recursive: true });
  await mkdir(paths.systemdUserDir, { recursive: true });

  const installedFiles: string[] = [];

  for (let index = 0; index < paths.daemonSourcePaths.length; index += 1) {
    const source = paths.daemonSourcePaths[index];
    const target = paths.daemonTargetPaths[index];
    if (source === undefined || target === undefined) continue;
    await copyFile(source, target);
    await chmod(target, 0o644);
    installedFiles.push(target);
  }

  await copyFile(paths.wrapperSourcePath, paths.wrapperTargetPath);
  await chmod(paths.wrapperTargetPath, 0o755);
  installedFiles.push(paths.wrapperTargetPath);

  await copyFile(paths.unitSourcePath, paths.unitTargetPath);
  await chmod(paths.unitTargetPath, 0o644);
  installedFiles.push(paths.unitTargetPath);

  return { installedFiles };
}

export interface BrokerSupervisorHealth {
  /** Whether the systemd unit file has been materialized at its target path. */
  readonly unitInstalled: boolean;
  /** Whether a Unix domain socket currently exists at the resolved socket path. */
  readonly socketPresent: boolean;
  /**
   * The signal other cards (e.g. C1's activation probe) should treat as
   * "a broker supervisor is present and healthy". Presence of a live socket
   * is the authoritative signal: a gated runtime can only ever succeed by
   * connecting to it, so this is what fail-closed callers must check.
   */
  readonly healthy: boolean;
}

/**
 * Report the supervisor's on-disk/health signals. Never throws for an
 * absent unit or socket — both simply report `false`; unexpected filesystem
 * errors (permission issues, etc.) still propagate.
 */
export async function checkBrokerSupervisorHealth(
  paths: Pick<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>,
): Promise<BrokerSupervisorHealth> {
  const [unitInstalled, socketPresent] = await Promise.all([
    pathExists(paths.unitTargetPath),
    isUnixSocket(paths.socketPath),
  ]);
  return { unitInstalled, socketPresent, healthy: socketPresent };
}

/** Convenience boolean form of {@link checkBrokerSupervisorHealth} for simple call sites. */
export async function isBrokerSupervisorHealthy(
  paths: Pick<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>,
): Promise<boolean> {
  return (await checkBrokerSupervisorHealth(paths)).healthy;
}

async function pathExists(path: string): Promise<boolean> {
  try {
    await stat(path);
    return true;
  } catch (error) {
    if (isEnoent(error)) return false;
    throw error;
  }
}

async function isUnixSocket(path: string): Promise<boolean> {
  try {
    const info = await stat(path);
    return info.isSocket();
  } catch (error) {
    if (isEnoent(error)) return false;
    throw error;
  }
}

function isEnoent(error: unknown): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    (error as NodeJS.ErrnoException).code === 'ENOENT'
  );
}
