// lib/hail/review-gates.js — HAIL review-gate decision ledger.
//
// The process engine owns the actual gate transitions (human_approval stages).
// This module mirrors those decisions into hail_review_gates so the
// counsel/auditor-facing view has a HAIL-native record of every sign-off
// (diagnostic, blueprint, setup-package, pilot, managed-layer) with reviewer,
// decision, and comments. It NEVER writes process_cases.* — it only reads
// approval decisions and writes its own ledger.
//
// See specs/active/hail-operating-model.md + hail-workflow-definition.md.

import { getDb, nowIso, newId } from '../db.js';
import { registerApprovalDecisionHandler } from '../approvals.js';
import { REVIEW_GATE_SET, REVIEWER_ROLE_SET, LIFECYCLE_GATE_STAGES, HAIL_WORKFLOW_GATE_STAGES } from './constants.js';

function rowToGate(row) {
  if (!row) return null;
  return { ...row };
}

export function listGates(engagement_id) {
  if (!engagement_id) return [];
  return getDb()
    .prepare('SELECT * FROM hail_review_gates WHERE engagement_id = ? ORDER BY created_at ASC')
    .all(engagement_id)
    .map(rowToGate);
}

export function getGate(id) {
  if (!id) return null;
  return rowToGate(getDb().prepare('SELECT * FROM hail_review_gates WHERE id = ?').get(id));
}

// Open a pending gate row when the engine enters a human_approval stage. Stores
// the approval_request_id so the decision listener can resolve it later.
export function openGate({ engagement_id, gate_type, reviewer_role, approval_request_id = null }) {
  if (!engagement_id) throw new Error('openGate: engagement_id required');
  if (!REVIEW_GATE_SET.has(gate_type)) throw new Error(`openGate: unknown gate_type "${gate_type}"`);
  if (!REVIEWER_ROLE_SET.has(reviewer_role)) throw new Error(`openGate: unknown reviewer_role "${reviewer_role}"`);
  const db = getDb();
  const id = newId('hailgate');
  const now = nowIso();
  db.prepare(`
    INSERT INTO hail_review_gates
      (id, engagement_id, gate_type, reviewer_role, decision, approval_request_id, created_at)
    VALUES (?, ?, ?, ?, 'pending', ?, ?)
  `).run(id, engagement_id, gate_type, reviewer_role, approval_request_id, now);
  return getGate(id);
}

// Record (or resolve) a gate decision. Idempotent on a given gate id.
export function recordGateDecision({ id, engagement_id, gate_type, reviewer_role, reviewer_user_id = null, decision, comments = null, approval_request_id = null }) {
  if (!['approved', 'rejected', 'revision_requested', 'pending'].includes(decision)) {
    throw new Error(`recordGateDecision: invalid decision "${decision}"`);
  }
  const db = getDb();
  const now = nowIso();

  if (id) {
    const existing = getGate(id);
    if (!existing) throw new Error(`recordGateDecision: gate "${id}" not found`);
    db.prepare(`
      UPDATE hail_review_gates
         SET decision = ?, reviewer_user_id = COALESCE(?, reviewer_user_id),
             comments = COALESCE(?, comments),
             approval_request_id = COALESCE(?, approval_request_id),
             decided_at = ?
       WHERE id = ?
    `).run(decision, reviewer_user_id, comments, approval_request_id, decision === 'pending' ? null : now, id);
    return getGate(id);
  }

  // No id supplied — create a resolved gate row directly.
  if (!engagement_id) throw new Error('recordGateDecision: id or engagement_id required');
  if (!REVIEW_GATE_SET.has(gate_type)) throw new Error(`recordGateDecision: unknown gate_type "${gate_type}"`);
  if (!REVIEWER_ROLE_SET.has(reviewer_role)) throw new Error(`recordGateDecision: unknown reviewer_role "${reviewer_role}"`);
  const newGateId = newId('hailgate');
  db.prepare(`
    INSERT INTO hail_review_gates
      (id, engagement_id, gate_type, reviewer_role, reviewer_user_id, decision, comments, approval_request_id, decided_at, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(newGateId, engagement_id, gate_type, reviewer_role, reviewer_user_id, decision, comments, approval_request_id, decision === 'pending' ? null : now, now);
  return getGate(newGateId);
}

// Open pending HAIL gate rows for any lifecycle human_approval stage the case is
// currently awaiting. Engine-non-invasive: reads the case's stage executions and
// mirrors awaiting-approval gate stages into hail_review_gates so the cockpit can
// show "review required". Idempotent — keyed on approval_request_id. Returns the
// gate rows for the engagement. Never throws.
export function syncLifecycleGates(engagement) {
  if (!engagement?.process_case_id) return listGates(engagement?.id);
  try {
    const db = getDb();
    // Executions of human_approval gate stages that have an approval id.
    const rows = db.prepare(
      "SELECT stage_slug, approval_request_id FROM process_stage_executions WHERE case_id = ? AND approval_request_id IS NOT NULL"
    ).all(engagement.process_case_id);
    for (const ex of rows) {
      const map = LIFECYCLE_GATE_STAGES[ex.stage_slug];
      if (!map) continue;
      const existing = db.prepare('SELECT id FROM hail_review_gates WHERE approval_request_id = ?').get(ex.approval_request_id);
      if (existing) continue;
      openGate({
        engagement_id: engagement.id,
        gate_type: map.gate_type,
        reviewer_role: map.reviewer_role,
        approval_request_id: ex.approval_request_id,
      });
    }
  } catch { /* observation only */ }
  return listGates(engagement.id);
}

// Map an engine approval decision onto a HAIL gate. First resolves an existing
// pending gate by approval_request_id. If none exists but the approval is a HAIL
// lifecycle gate stage (payload.definition_slug + stage_slug), creates a resolved
// gate row so the ledger is correct even when no gate was pre-opened. Safe no-op
// for non-HAIL approvals. Never throws.
function onApprovalDecided({ approval_request_id, decision, row } = {}) {
  if (!approval_request_id) return;
  try {
    const db = getDb();
    const mapped = decision === 'approved' ? 'approved' : decision === 'denied' ? 'rejected' : 'revision_requested';

    // 1. A pre-opened (pending) HAIL gate for this approval — resolve it. This
    //    path is unconditional: any gate HAIL deliberately opened (via openGate)
    //    is resolved by its approval_request_id, whatever the approval payload.
    const gate = db.prepare('SELECT * FROM hail_review_gates WHERE approval_request_id = ? AND decision = \'pending\'').get(approval_request_id);
    if (gate) {
      recordGateDecision({ id: gate.id, decision: mapped, reviewer_user_id: row?.approved_by || null });
      applyGateSideEffects(db, gate.engagement_id, gate.gate_type, mapped, approval_request_id, row?.approved_by);
      return;
    }
    // 2. Already recorded for this approval? Nothing to do.
    if (db.prepare('SELECT 1 FROM hail_review_gates WHERE approval_request_id = ?').get(approval_request_id)) return;

    // 3. Lazy-record path — scoped to HAIL workflow gate approvals only (lifecycle
    //    OR quarterly review), so a non-HAIL approval never creates a spurious row.
    const payload = row?.payload || {};
    const map = (HAIL_WORKFLOW_GATE_STAGES[payload.definition_slug] || {})[payload.stage_slug];
    if (!map || !payload.case_id) return;
    const eng = resolveEngagementForCase(db, payload.case_id);
    if (!eng) return;
    recordGateDecision({
      engagement_id: eng.id,
      gate_type: map.gate_type,
      reviewer_role: map.reviewer_role,
      decision: mapped,
      reviewer_user_id: row?.approved_by || null,
      approval_request_id,
    });
    applyGateSideEffects(db, eng.id, map.gate_type, mapped, approval_request_id, row?.approved_by);
  } catch { /* observation only — never block the approval path */ }
}

// Resolve the engagement a HAIL case belongs to: the lifecycle case matches by
// process_case_id; a quarterly-review case runs in the engagement's project, so
// it matches by the case's project_id (engagement↔project is 1:1).
function resolveEngagementForCase(db, case_id) {
  const direct = db.prepare('SELECT id FROM hail_engagements WHERE process_case_id = ?').get(case_id);
  if (direct) return direct;
  const c = db.prepare('SELECT project_id FROM process_cases WHERE id = ?').get(case_id);
  if (!c?.project_id) return null;
  return db.prepare('SELECT id FROM hail_engagements WHERE project_id = ? ORDER BY created_at ASC LIMIT 1').get(c.project_id) || null;
}

// On an APPROVED HAIL gate, run the engine-non-invasive side effect for that gate
// type (direct SQL only — never touches process_cases, never imports the applier):
//   setup_package_approval → flip the latest dry-run setup package to 'approved'
//   pilot_launch           → mark provisioned blueprint agents 'live'
//   managed_layer_change   → move the engagement to the managed_layer stage
function applyGateSideEffects(db, engagement_id, gate_type, mapped, approval_request_id, approved_by) {
  if (mapped !== 'approved' || !engagement_id) return;
  try {
    if (gate_type === 'setup_package_approval') {
      // Flip exactly the latest dry-run package for the engagement.
      db.prepare(`
        UPDATE hail_setup_packages SET status = 'approved', approved_by_user_id = ?, approval_request_id = ?, updated_at = ?
         WHERE id = (SELECT id FROM hail_setup_packages WHERE engagement_id = ? AND status = 'dry_run' ORDER BY version DESC LIMIT 1)
      `).run(approved_by || null, approval_request_id || null, nowIso(), engagement_id);
    } else if (gate_type === 'pilot_launch') {
      // Only agents that were actually provisioned (applied) go live.
      db.prepare("UPDATE hail_agent_blueprints SET status = 'live', updated_at = ? WHERE engagement_id = ? AND status = 'provisioned'")
        .run(nowIso(), engagement_id);
    } else if (gate_type === 'managed_layer_change') {
      db.prepare("UPDATE hail_engagements SET current_stage = 'managed_layer', updated_at = ? WHERE id = ?")
        .run(nowIso(), engagement_id);
    }
  } catch { /* observation only */ }
}

// Register the listener once per process. Guarded so repeated calls / HMR do
// not stack duplicate handlers. The handler registry (lib/approvals.js) is a
// Set of multiple handlers, so this coexists with the process engine's own.
//
// IMPORTANT: this must NOT be called at module-import time. review-gates.js is
// pulled in through the db.js → seeds.js import chain, and approvals.js is in
// the same circular graph — calling it during import hits a temporal dead zone
// on approvals' `decisionHandlers`. It is invoked explicitly from
// lib/seeds.js::seedDefaults, which runs at DB-open time after all modules are
// fully initialized.
export function registerHailGateListener() {
  if (globalThis.__madarxHailGateListener) return globalThis.__madarxHailGateListener;
  globalThis.__madarxHailGateListener = registerApprovalDecisionHandler(onApprovalDecided);
  return globalThis.__madarxHailGateListener;
}
