#!/usr/bin/env node
/**
 * scripts/setup-hooks.mjs — install/remove web-terminal's Claude Code hooks (H2).
 *
 * Adds `command` hooks to ~/.claude/settings.json that POST each hook event to
 * the web-terminal server so it can show live Claude status per tab. The command
 * is an inline curl that uses two env vars the server injects into each spawned
 * shell: $WEBTERM_HOOK_URL (the server's /hook URL, with the right port) and
 * $WEBTERM_SESSION (which tab). Outside web-terminal those vars are unset, so
 * curl no-ops (|| true) — the hooks are harmless in normal terminals.
 *
 * v0.7 additions (T-hooks-installer):
 *   H1  PostToolUse added to FF_EVENTS (A4 timeline completeness, AC-A4.5)
 *   L5  --max-time computed from PERM_TIMEOUT_MS env (not hardcoded 350, SEC-M4)
 *   H3  ntfy/Pushover bridge: only installed when WEBTERM_NTFY_URL+TOPIC set;
 *       token always via $WEBTERM_NTFY_TOKEN env — never a literal (SEC-C6)
 *   B2  statusLine handler: scripts/statusline.mjs installed idempotently;
 *       --remove cleans the entry; user's own statusLine is not overwritten
 *
 * Usage:
 *   node scripts/setup-hooks.mjs            # install (idempotent)
 *   node scripts/setup-hooks.mjs --remove   # uninstall
 *
 * Backs up settings.json to settings.json.webterm-bak before writing.
 */

import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs'
import { homedir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

// ── Constants ────────────────────────────────────────────────────────────────

/** Appears in every web-terminal hook command — used by cleanup to identify our entries. */
export const MARKER = 'WEBTERM_HOOK_URL'

/** Appears in our statusLine command — used to detect and remove our entry idempotently. */
export const STATUSLINE_MARKER = 'statusline.mjs'

/** Safety buffer added to PERM_TIMEOUT_MS/1000 for --max-time (L5/SEC-M4). */
const CURL_BUFFER_SEC = 5

/** Default value for PERM_TIMEOUT_MS when env is unset or invalid (5 minutes in ms). */
const DEFAULT_PERM_TIMEOUT_MS = 300_000

// ── Pure helpers (exported for unit tests) ───────────────────────────────────

/**
 * Compute the --max-time value (seconds) for the held PermissionRequest curl.
 * Result is always > permTimeoutMs/1000 so curl never races the server (L5/SEC-M4).
 * Falls back to DEFAULT_PERM_TIMEOUT_MS when the input is missing, zero, or negative.
 *
 * @param {number} [permTimeoutMs]
 * @returns {number}
 */
export function computeMaxTimeSec(permTimeoutMs = DEFAULT_PERM_TIMEOUT_MS) {
  const safeMs =
    Number.isFinite(permTimeoutMs) && permTimeoutMs > 0 ? permTimeoutMs : DEFAULT_PERM_TIMEOUT_MS
  return Math.ceil(safeMs / 1000) + CURL_BUFFER_SEC
}

/**
 * Build the held PermissionRequest curl command string.
 * --max-time is supplied by the caller (from computeMaxTimeSec) — never hardcoded (L5).
 *
 * @param {number} maxTimeSec
 * @returns {string}
 */
export function buildPermCommand(maxTimeSec) {
  return (
    'curl -s --max-time ' +
    maxTimeSec +
    ' -X POST "${WEBTERM_HOOK_URL}/permission"' +
    ' -H "X-Webterm-Session: $WEBTERM_SESSION" -H "Content-Type: application/json" --data-binary @-'
  )
}

/**
 * Build a fire-and-forget ntfy notification curl command (H3).
 *
 * Security guarantees (SEC-C6):
 *   - $WEBTERM_NTFY_TOKEN is a shell variable reference, NEVER a literal value.
 *   - The guard `[ -n "$WEBTERM_HOOK_URL" ]` makes this a no-op outside web-terminal
 *     (WEBTERM_HOOK_URL is injected by the server spawn, not available globally).
 *   - Because the command contains MARKER (WEBTERM_HOOK_URL), the existing cleanup
 *     loop removes it correctly on --remove / re-install.
 *
 * @param {'high' | 'low'} priority - ntfy Priority header value
 * @returns {string}
 */
export function buildNtfyCommand(priority) {
  return (
    '[ -n "$WEBTERM_HOOK_URL" ] && ' +
    'curl -s -X POST "${WEBTERM_NTFY_URL}/${WEBTERM_NTFY_TOPIC}"' +
    ' -H "Authorization: Bearer $WEBTERM_NTFY_TOKEN"' +
    ' -H "Priority: ' +
    priority +
    '"' +
    ' >/dev/null 2>&1 || true'
  )
}

/**
 * Build the statusLine command Claude Code runs on each status update (B2).
 * Uses an absolute path so it works regardless of the shell's CWD.
 *
 * @param {string} scriptDir - directory that contains statusline.mjs
 * @returns {string}
 */
export function buildStatusLineCommand(scriptDir) {
  return 'node ' + path.join(scriptDir, 'statusline.mjs')
}

// ── Hook event lists ─────────────────────────────────────────────────────────

/**
 * Fire-and-forget hook events.
 * H1: PostToolUse added so the A4 timeline captures "task complete" events
 * (AC-A4.5, A4-FR1). These all POST asynchronously; Claude doesn't wait.
 */
export const FF_EVENTS = [
  'UserPromptSubmit',
  'PreToolUse',
  'PostToolUse', // H1 — added for A4 timeline completeness
  'Notification',
  'Stop',
  'SessionEnd',
]

/** Fire-and-forget: POST hook body to web-terminal, discard response. */
export const FF_COMMAND =
  'curl -s -X POST "$WEBTERM_HOOK_URL" -H "X-Webterm-Session: $WEBTERM_SESSION"' +
  ' -H "Content-Type: application/json" --data-binary @- >/dev/null 2>&1 || true'

/** Held PermissionRequest command — computed at module load from PERM_TIMEOUT_MS env. */
export const PERM_COMMAND = buildPermCommand(
  computeMaxTimeSec(
    (() => {
      const raw = process.env['PERM_TIMEOUT_MS']
      if (raw === undefined) return DEFAULT_PERM_TIMEOUT_MS
      const n = Number(raw)
      return Number.isFinite(n) && n > 0 ? n : DEFAULT_PERM_TIMEOUT_MS
    })(),
  ),
)

// ── Core install/remove logic (exported for tests) ───────────────────────────

/**
 * Apply (install or remove) web-terminal hooks to a settings.json file.
 * This is the testable core — the main script entry calls it with real env values.
 *
 * @param {Object} opts
 * @param {string}  opts.settingsPath  - absolute path to settings.json
 * @param {boolean} opts.remove        - true = remove our entries, false = install
 * @param {boolean} opts.hasNtfy       - true = add ntfy bridge hook commands (H3)
 * @param {number}  opts.permTimeoutMs - used to compute --max-time (L5/SEC-M4)
 * @param {string}  opts.scriptDir     - directory of statusline.mjs (for absolute path)
 * @returns {{ ok: boolean; error?: string; settings?: object }}
 */
export function applyHooks({ settingsPath, remove, hasNtfy, permTimeoutMs, scriptDir }) {
  const maxTimeSec = computeMaxTimeSec(permTimeoutMs)
  const permCommand = buildPermCommand(maxTimeSec)
  const statusLineCommand = buildStatusLineCommand(scriptDir)

  /** @type {Record<string, unknown>} */
  let settings = {}

  if (existsSync(settingsPath)) {
    try {
      settings = JSON.parse(readFileSync(settingsPath, 'utf8'))
    } catch {
      return { ok: false, error: `${settingsPath} is not valid JSON — aborting` }
    }
    copyFileSync(settingsPath, `${settingsPath}.webterm-bak`)
  }

  if (typeof settings['hooks'] !== 'object' || settings['hooks'] === null) {
    settings['hooks'] = {}
  }

  /** @type {Record<string, unknown[]>} */
  const hooks = /** @type {Record<string, unknown[]>} */ (settings['hooks'])

  // ── Strip existing web-terminal hook groups (idempotent + --remove) ─────────
  // Any hook group whose command contains MARKER is ours (FF_COMMAND, PERM_COMMAND,
  // and ntfy commands all contain WEBTERM_HOOK_URL, so one cleanup pass covers all).
  for (const ev of Object.keys(hooks)) {
    const groups = hooks[ev]
    if (!Array.isArray(groups)) continue
    hooks[ev] = groups.filter(
      (g) =>
        !(
          g != null &&
          typeof g === 'object' &&
          Array.isArray(/** @type {Record<string,unknown>}*/ (g)['hooks']) &&
          /** @type {Array<Record<string,unknown>>} */ (
            /** @type {Record<string,unknown>} */ (g)['hooks']
          ).some(
            (h) => typeof h['command'] === 'string' && h['command'].includes(MARKER),
          )
        ),
    )
    if (hooks[ev].length === 0) delete hooks[ev]
  }

  // ── Strip statusLine if it's ours (idempotent + --remove) ───────────────────
  if (
    typeof settings['statusLine'] === 'string' &&
    settings['statusLine'].includes(STATUSLINE_MARKER)
  ) {
    delete settings['statusLine']
  }

  // ── Install (skip when --remove) ────────────────────────────────────────────
  if (!remove) {
    // Fire-and-forget events
    for (const ev of FF_EVENTS) {
      if (!Array.isArray(hooks[ev])) hooks[ev] = []
      hooks[ev].push({ hooks: [{ type: 'command', command: FF_COMMAND }] })
    }

    // Held PermissionRequest (waits for server decision JSON)
    if (!Array.isArray(hooks['PermissionRequest'])) hooks['PermissionRequest'] = []
    hooks['PermissionRequest'].push({ hooks: [{ type: 'command', command: permCommand }] })

    // H3: ntfy bridge — only when URL + TOPIC are configured at install time.
    // The token ($WEBTERM_NTFY_TOKEN) is NEVER a literal here (SEC-C6).
    if (hasNtfy) {
      // NEEDS-INPUT signal: PermissionRequest → high priority
      hooks['PermissionRequest'].push({
        hooks: [{ type: 'command', command: buildNtfyCommand('high') }],
      })
      // DONE signal: Stop + SessionEnd → low priority
      for (const ev of ['Stop', 'SessionEnd']) {
        hooks[ev].push({ hooks: [{ type: 'command', command: buildNtfyCommand('low') }] })
      }
    }

    // B2: statusLine handler — only when the key is currently absent, so we never
    // overwrite a statusLine the user configured independently.
    if (!settings['statusLine']) {
      settings['statusLine'] = statusLineCommand
    }
  }

  mkdirSync(path.dirname(settingsPath), { recursive: true })
  writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)

  return { ok: true, settings }
}

// ── Main script entry (runs only when executed directly, not when imported) ──

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const SETTINGS = path.join(homedir(), '.claude', 'settings.json')
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))

  const remove = process.argv.includes('--remove')

  // H3: ntfy bridge is installed only when WEBTERM_NTFY_URL + TOPIC are in env
  const ntfyUrl = process.env['WEBTERM_NTFY_URL']
  const ntfyTopic = process.env['WEBTERM_NTFY_TOPIC']
  const hasNtfy = Boolean(ntfyUrl && ntfyTopic)

  // L5/SEC-M4: --max-time computed from PERM_TIMEOUT_MS (never a bare literal)
  const permTimeoutMs = (() => {
    const raw = process.env['PERM_TIMEOUT_MS']
    if (raw === undefined) return DEFAULT_PERM_TIMEOUT_MS
    const n = Number(raw)
    return Number.isFinite(n) && n > 0 ? n : DEFAULT_PERM_TIMEOUT_MS
  })()

  const result = applyHooks({
    settingsPath: SETTINGS,
    remove,
    hasNtfy,
    permTimeoutMs,
    scriptDir: SCRIPT_DIR,
  })

  if (!result.ok) {
    console.error(result.error)
    process.exit(1)
  }

  const allEvents = [...FF_EVENTS, 'PermissionRequest']
  console.log(
    remove
      ? `Removed web-terminal hooks from ${SETTINGS}`
      : `Installed web-terminal hooks into ${SETTINGS} (events: ${allEvents.join(', ')})`,
  )

  if (!remove) {
    console.log(`Installed statusLine handler: ${buildStatusLineCommand(SCRIPT_DIR)}`)
    if (hasNtfy) {
      console.log(`Installed ntfy bridge (NEEDS-INPUT=high, DONE=low) → ${ntfyUrl}/${ntfyTopic}`)
    }
  }

  if (existsSync(`${SETTINGS}.webterm-bak`)) console.log(`Backup: ${SETTINGS}.webterm-bak`)
  console.log('Restart any running `claude` sessions for the hooks to take effect.')
}
