/**
 * Install-ordering guard (issue #869, Point-1 card C2).
 *
 * Root cause this exists to guard against (#828 version skew, restated): the
 * framework reseed / install path (`framework/install.sh` →
 * `mosaic-link-runtime-assets` → copies `runtime/claude/settings.json` to
 * `~/.claude/settings.json`) wires the ENFORCEMENT half of the lease broker —
 * the `PreToolUse` `mutator-gate.py` hook and the `Stop`
 * `receipt-observer-client.py` hook — unconditionally. If the ACTIVATION half
 * (a CLI build advertising launch-runtime activation + a running broker
 * supervisor — see `lease-activation-probe.ts`, C1) is absent, the fail-closed
 * gate then denies every tool call with GATE_UNAVAILABLE: a bricked host.
 *
 * This module is the WIRING gate, not the enforcement gate: it decides
 * whether the enforcement hook entries are written into the settings.json
 * that ships to `~/.claude/`. It never touches `mutator-gate.py`'s own
 * fail-closed-on-absent-identity runtime behavior (test-locked in
 * `runtime_tools_unittest.py` / `fail-closed-regression.spec.ts`).
 *
 * Default (no opt-out): NOT activatable → strip the enforcement hook entries
 * from the written settings.json and report a non-zero outcome with a loud,
 * actionable message (see FAIL_LOUD_MESSAGE below).
 *
 * Opt-out: `--allow-inactive-enforcement` (an explicit, per-invocation CLI
 * flag — deliberately NOT an environment variable, so it can never sit as a
 * silently-inherited default in a shell profile). When set on a NOT
 * activatable host, the hooks ARE wired but a loud warning is emitted saying
 * so, and the outcome is reported ok (this is a conscious, informed choice).
 */

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import type { Command } from 'commander';
import { leaseEnforcementActivatable } from './lease-activation-probe.js';

// ─── Enforcement hook identification ────────────────────────────────────────

/** Substrings that identify the two enforcement hook commands #828 wired
 * unconditionally. Matches the marker strings documented in
 * `lease-activation-probe.ts`. */
export const ENFORCEMENT_HOOK_MARKERS = {
  preToolUse: 'mutator-gate.py',
  stop: 'receipt-observer-client.py',
} as const;

interface HookEntry {
  command?: string;
  [key: string]: unknown;
}

interface HookTrigger {
  matcher?: string;
  hooks?: HookEntry[];
  [key: string]: unknown;
}

type HooksMap = Record<string, HookTrigger[]>;

function cloneJson<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T;
}

function commandIncludes(hook: HookEntry, marker: string): boolean {
  return String(hook.command ?? '').includes(marker);
}

/**
 * Return a deep clone of `settings` with the enforcement hook entries removed:
 *  - Any `PreToolUse` trigger group containing a `mutator-gate.py` command is
 *    dropped in full (that trigger exists solely to run the gate).
 *  - Within `Stop` trigger groups, only the individual `receipt-observer-client.py`
 *    hook entry is dropped; sibling hooks in the same trigger (e.g.
 *    `reflect-stop-hook.sh`) are preserved.
 * Every other hook (PreCompact/SessionStart revoke-lease, the
 * `prevent-memory-write.sh` PreToolUse trigger, PostToolUse qa/typecheck
 * hooks) is left byte-identical — this function only ever removes the two
 * markers above.
 */
export function stripEnforcementHooks(settings: Record<string, unknown>): {
  settings: Record<string, unknown>;
  removed: string[];
} {
  const cloned = cloneJson(settings);
  const removed: string[] = [];
  const hooks = cloned['hooks'] as HooksMap | undefined;
  if (!hooks || typeof hooks !== 'object') {
    return { settings: cloned, removed };
  }

  const preToolUse = hooks['PreToolUse'];
  if (Array.isArray(preToolUse)) {
    const kept = preToolUse.filter((trigger) => {
      const hasGate = (trigger.hooks ?? []).some((h) =>
        commandIncludes(h, ENFORCEMENT_HOOK_MARKERS.preToolUse),
      );
      if (hasGate) removed.push('PreToolUse:mutator-gate.py');
      return !hasGate;
    });
    if (kept.length > 0) hooks['PreToolUse'] = kept;
    else delete hooks['PreToolUse'];
  }

  const stop = hooks['Stop'];
  if (Array.isArray(stop)) {
    const rebuilt: HookTrigger[] = [];
    for (const trigger of stop) {
      const innerHooks = trigger.hooks ?? [];
      const keptHooks = innerHooks.filter((h) => {
        const isReceiptObserver = commandIncludes(h, ENFORCEMENT_HOOK_MARKERS.stop);
        if (isReceiptObserver) removed.push('Stop:receipt-observer-client.py');
        return !isReceiptObserver;
      });
      if (keptHooks.length > 0) {
        rebuilt.push({ ...trigger, hooks: keptHooks });
      }
    }
    if (rebuilt.length > 0) hooks['Stop'] = rebuilt;
    else delete hooks['Stop'];
  }

  if (Object.keys(hooks).length === 0) {
    delete cloned['hooks'];
  } else {
    cloned['hooks'] = hooks;
  }

  return { settings: cloned, removed };
}

/** True iff `settings` currently wires either enforcement hook. */
export function settingsHasEnforcementHooks(settings: Record<string, unknown>): boolean {
  const hooks = settings['hooks'] as HooksMap | undefined;
  if (!hooks || typeof hooks !== 'object') return false;

  const preToolUse = hooks['PreToolUse'] ?? [];
  const preHit = preToolUse.some((trigger) =>
    (trigger.hooks ?? []).some((h) => commandIncludes(h, ENFORCEMENT_HOOK_MARKERS.preToolUse)),
  );
  if (preHit) return true;

  const stop = hooks['Stop'] ?? [];
  return stop.some((trigger) =>
    (trigger.hooks ?? []).some((h) => commandIncludes(h, ENFORCEMENT_HOOK_MARKERS.stop)),
  );
}

// ─── Guard predicate ────────────────────────────────────────────────────────

export const FAIL_LOUD_MESSAGE =
  '[mosaic] ERROR: enforcement requested but activation half absent — needs a published CLI ' +
  'carrying launch-runtime activation + a broker supervisor; refusing to wire a dead gate (see #869). ' +
  'The PreToolUse mutator-gate.py hook and Stop receipt-observer-client.py hook were NOT written to ' +
  'settings.json. Fix by installing/updating the CLI and broker, then re-run the framework reseed. ' +
  'To wire anyway (NOT recommended — the fail-closed gate will deny every tool call with ' +
  'GATE_UNAVAILABLE until activation is restored), re-run with --allow-inactive-enforcement.';

export function loudOptOutMessage(): string {
  return (
    '[mosaic] WARNING: wiring lease-enforcement hooks (mutator-gate.py / receipt-observer-client.py) ' +
    'WITHOUT confirmed activation — --allow-inactive-enforcement was set explicitly. The fail-closed ' +
    'gate will deny every tool call (GATE_UNAVAILABLE) until the activation half (launch-runtime ' +
    'activation capability + a running broker supervisor) is present on this host (see #869).'
  );
}

export type GuardLogLevel = 'error' | 'warn';

export interface GuardLogLine {
  level: GuardLogLevel;
  message: string;
}

export interface InstallOrderingGuardOptions {
  /** Explicit, per-invocation opt-out. Never source this from an environment
   * variable — see module doc. */
  allowInactiveEnforcement?: boolean;
}

export interface InstallOrderingGuardDeps {
  /** Defaults to {@link leaseEnforcementActivatable}. Injectable for tests. */
  activatable?: () => boolean;
}

export interface InstallOrderingGuardOutcome {
  /** The settings.json content to write (pretty-printed, trailing newline). */
  json: string;
  /** Whether the enforcement hooks are present in `json`. */
  wired: boolean;
  /** 0 = proceed normally; 1 = enforcement was refused (fail-loud default path). */
  exitCode: 0 | 1;
  logs: GuardLogLine[];
}

/**
 * The install-ordering guard: decide whether the enforcement hooks embedded
 * in the Claude settings.json template may be wired into the settings.json
 * actually shipped to `~/.claude/`.
 *
 *   - activatable            → wire as-is. exitCode 0, no logs.
 *   - NOT activatable, no opt-out → strip enforcement hooks. exitCode 1,
 *     one 'error' log with the actionable FAIL_LOUD_MESSAGE.
 *   - NOT activatable, opt-out set → wire as-is anyway. exitCode 0, one
 *     'warn' log making the risk explicit and loud.
 *
 * Pure function: takes the raw settings.json text, returns the text to write
 * plus metadata. No filesystem access — callers (the hidden CLI subcommand
 * below, or a test) own reading/writing so this stays trivially testable with
 * fakes/temp files and never risks touching a real `~/.claude/settings.json`.
 */
export function guardClaudeSettingsWiring(
  rawSettingsJson: string,
  options: InstallOrderingGuardOptions = {},
  deps: InstallOrderingGuardDeps = {},
): InstallOrderingGuardOutcome {
  const parsed = JSON.parse(rawSettingsJson) as Record<string, unknown>;
  const activatable = deps.activatable ?? leaseEnforcementActivatable;
  const isActivatable = activatable();

  const serialize = (settings: Record<string, unknown>): string =>
    JSON.stringify(settings, null, 2) + '\n';

  if (isActivatable) {
    return {
      json: serialize(parsed),
      wired: settingsHasEnforcementHooks(parsed),
      exitCode: 0,
      logs: [],
    };
  }

  if (options.allowInactiveEnforcement === true) {
    return {
      json: serialize(parsed),
      wired: settingsHasEnforcementHooks(parsed),
      exitCode: 0,
      logs: [{ level: 'warn', message: loudOptOutMessage() }],
    };
  }

  const { settings: stripped } = stripEnforcementHooks(parsed);
  return {
    json: serialize(stripped),
    wired: settingsHasEnforcementHooks(stripped),
    exitCode: 1,
    logs: [{ level: 'error', message: FAIL_LOUD_MESSAGE }],
  };
}

// ─── File-level runner (shared by the CLI action + tests) ──────────────────

export interface RunInstallOrderingGuardResult extends InstallOrderingGuardOutcome {
  destWritten: boolean;
  backupPath?: string;
}

/**
 * Read `src`, guard it, and write the result to `dest` — mirroring
 * `copy_file_managed`'s backup-on-change semantics from
 * `mosaic-link-runtime-assets` (skip the write if content is unchanged;
 * back up an existing divergent file once, timestamped). Exported standalone
 * (not only reachable via the CLI action closure) so tests can exercise real
 * file I/O against temp directories without ever touching `~/.claude/`.
 */
export function runInstallOrderingGuard(
  src: string,
  dest: string,
  options: InstallOrderingGuardOptions = {},
  deps: InstallOrderingGuardDeps = {},
): RunInstallOrderingGuardResult {
  const raw = readFileSync(src, 'utf-8');
  const outcome = guardClaudeSettingsWiring(raw, options, deps);

  mkdirSync(dirname(dest), { recursive: true });

  const existing = existsSync(dest) ? readFileSync(dest, 'utf-8') : null;
  let destWritten = false;
  let backupPath: string | undefined;

  if (existing !== outcome.json) {
    if (existing !== null) {
      const stamp = new Date()
        .toISOString()
        .replace(/[-:]/g, '')
        .replace(/\..+$/, '')
        .replace('T', '');
      backupPath = `${dest}.mosaic-bak-${stamp}`;
      writeFileSync(backupPath, existing);
    }
    writeFileSync(dest, outcome.json);
    destWritten = true;
  }

  return { ...outcome, destWritten, backupPath };
}

// ─── Hidden CLI bridge (bash → TS) ──────────────────────────────────────────

/** Hidden CLI subcommand name. `mosaic-link-runtime-assets` (bash) invokes
 * this instead of its generic `copy_file_managed` for the settings.json
 * runtime file specifically, so the guard's decision is made by importing
 * `leaseEnforcementActivatable()` directly rather than re-implementing the
 * capability/supervisor probes in shell. Deliberately undocumented (hidden
 * from `--help`) — internal wiring, not a user-facing command. */
export const INSTALL_ORDERING_GUARD_COMMAND = '__link-claude-settings';

export function registerInstallOrderingGuardCommand(program: Command): void {
  program
    .command(`${INSTALL_ORDERING_GUARD_COMMAND} <src> <dest>`, { hidden: true })
    .description(
      'Internal: copy the Claude settings.json template, gating enforcement-hook ' +
        'wiring on lease-activation capability (#869 Point-1 C2)',
    )
    .option(
      '--allow-inactive-enforcement',
      'Wire enforcement hooks even when activation cannot be confirmed on this host ' +
        '(explicit, loud, non-default opt-out — see #869)',
    )
    .action((src: string, dest: string, opts: { allowInactiveEnforcement?: boolean }) => {
      const result = runInstallOrderingGuard(src, dest, {
        allowInactiveEnforcement: opts.allowInactiveEnforcement === true,
      });
      for (const line of result.logs) {
        (line.level === 'error' ? console.error : console.warn)(line.message);
      }
      process.exit(result.exitCode);
    });
}
