// lib/hail/setup-applier.js — the SINGLE write path from an approved HAIL
// blueprint into live madarX OS configuration.
//
// Load-bearing guarantees:
//   1. Refuses unless the package status is 'approved' (or 'partially_applied'
//      to resume) — nothing applies before the setup-package approval gate.
//   2. Idempotent by the hail_setup_package_items ledger: an item already
//      'applied'/'skipped' is never re-processed; the underlying surfaces
//      (createAgent / upsertSchedule / upsertDefinition) are also id-idempotent.
//   3. Per-item gating mirrors the 7-gate model: `blocked` never applies;
//      `irreversible_external` ALWAYS creates its own approval (even trust=auto);
//      `high`/`medium` gate only in approval_required mode; `low` applies.
//   4. `secret` items collect a request only — a value is NEVER set or stored.
//   5. Each applied item routes through its EXISTING runtime write surface and
//      writes an audit row. This module NEVER writes process_cases.*.
//
// See specs/active/hail-development-plan.md Phase 5.

import { getDb, nowIso, newId } from '../db.js';
import { appendAudit, createAgent, upsertSchedule, setAgentToolPermission, createArtifact } from '../runtime.js';
import { getMainAgentTrustMode } from '../tools/registry.js';
import { createApprovalRequest, getApprovalRequest, consumeApprovalRequest } from '../approvals.js';
import { addEntry as addAllowlistEntry } from '../tool-allowlists.js';
import { setItemInstalled, getItem } from '../marketplace.js';
import { upsertDefinition } from '../process/definitions.js';
import { getEngagement } from './engagements.js';
import { GOVERNANCE_RISK_SET } from './constants.js';

const APPLY_ACTION_TYPE = 'hail.setup.apply_item';
const REPROCESSABLE = new Set(['pending', 'awaiting_approval', 'failed']);
const TERMINAL_OK = new Set(['applied', 'skipped']);

function safeParse(s) { try { return JSON.parse(s); } catch { return {}; } }

function getPackageRow(db, package_id) {
  return db.prepare('SELECT * FROM hail_setup_packages WHERE id = ?').get(package_id);
}

// Decide whether an item must gate before applying, per the runtime risk model.
function needsGate(risk, trustMode) {
  if (risk === 'irreversible_external') return true; // always — even trust=auto
  if (risk === 'high' || risk === 'medium') return trustMode === 'approval_required';
  return false; // low
}

// Apply a single item through its EXISTING runtime write surface. Returns
// { apply_status, applied_target_id?, note? }. Throws on a real failure (the
// caller marks the item 'failed'). NEVER sets a secret value.
function applyItem(item, { engagement, user_id }) {
  const spec = safeParse(item.spec_json);
  switch (item.item_type) {
    case 'agent': {
      // Require an explicit slug — never let a malformed item fall back to a
      // misnamed live agent (createAgent would slugify name→'agent').
      if (!spec.id || typeof spec.id !== 'string' || !spec.id.trim()) {
        throw new Error('agent item missing spec.id');
      }
      const { agent } = createAgent({
        id: spec.id,
        name: spec.name,
        type: 'custom',
        mission: spec.purpose || spec.mission || null,
        role: spec.role_scope || 'individual_contributor',
        engine: spec.engine || 'claude-code',
        tools: Array.isArray(spec.tools) ? spec.tools : [],
        schedule: spec.schedule || 'on demand',
        approval: spec.approval || 'human-in-loop',
        expected_output: spec.expected_output || null,
        project_id: engagement.project_id || null,
        metadata_json: { hail: { engagement_id: engagement.id, risk_tier: spec.risk_tier, memory_scope: spec.memory_scope, review_gates: spec.review_gates } },
        triggered_by: 'hail-setup-applier',
        suppress_live_event: true,
      });
      // Link the provisioned agent back onto the blueprint record.
      try {
        getDb().prepare("UPDATE hail_agent_blueprints SET status = 'provisioned', provisioned_agent_id = ?, updated_at = ? WHERE engagement_id = ? AND agent_slug = ?")
          .run(agent.id, nowIso(), engagement.id, spec.id);
      } catch { /* observation only */ }
      return { apply_status: 'applied', applied_target_id: agent.id };
    }
    case 'schedule': {
      const res = upsertSchedule({ agent_id: spec.agent_id, schedule: spec.schedule || 'daily 07:30', created_by_user_id: user_id || null });
      return { apply_status: 'applied', applied_target_id: res?.schedule?.id || `schedule-${spec.agent_id}` };
    }
    case 'tool': {
      const id = spec.item_id || (spec.slug ? `tool:${spec.slug}` : null);
      if (!id || !getItem(id)) throw new Error(`unknown marketplace tool "${spec.item_id || spec.slug || ''}"`);
      setItemInstalled(id, true);
      return { apply_status: 'applied', applied_target_id: id };
    }
    case 'allowlist_entry': {
      addAllowlistEntry({ tool_name: spec.tool_name, scope_key: spec.scope_key, scope_value: spec.scope_value, label: spec.label || `HAIL ${engagement.id}` });
      return { apply_status: 'applied', applied_target_id: `${spec.tool_name}:${spec.scope_value}` };
    }
    case 'workflow': {
      const def = upsertDefinition(spec.spec || spec, { created_by_user_id: user_id || null });
      return { apply_status: 'applied', applied_target_id: def?.id || def?.slug || null };
    }
    case 'approval_policy': {
      // Only a per-agent tool permission is a concrete mutation we apply here;
      // a global trust-posture intent is recorded, never auto-flipped.
      if (spec.agent_id && spec.tool_name && spec.mode) {
        setAgentToolPermission(spec.agent_id, spec.tool_name, spec.mode);
        return { apply_status: 'applied', applied_target_id: `${spec.agent_id}:${spec.tool_name}` };
      }
      return { apply_status: 'applied', note: 'approval policy recorded (no global trust change auto-applied)' };
    }
    case 'memory_scope':
      return { apply_status: 'applied', note: `memory scope "${spec.scope || 'project'}" declared` };
    case 'artifact_acl':
      return { apply_status: 'applied', note: `default artifact ACL "${spec.scope || 'private'}" recorded` };
    case 'secret':
      // Collection request ONLY — a value is never carried, set, or stored.
      return { apply_status: 'awaiting_secret', note: `secret "${spec.name || item.ref}" must be supplied via the secrets vault` };
    default:
      throw new Error(`unknown setup item type "${item.item_type}"`);
  }
}

function setItemStatus(db, item, patch) {
  db.prepare(`
    UPDATE hail_setup_package_items
       SET apply_status = ?, applied_target_id = COALESCE(?, applied_target_id),
           approval_request_id = COALESCE(?, approval_request_id), error = ?, updated_at = ?
     WHERE id = ?
  `).run(patch.apply_status, patch.applied_target_id ?? null, patch.approval_request_id ?? null, patch.error ?? null, nowIso(), item.id);
}

// Promote an APPROVED setup package into live configuration. Returns the package
// status + per-item ledger. Re-runnable: only re-processes non-terminal items.
export function applySetupPackage(package_id, { user_id = null } = {}) {
  const db = getDb();
  const pkg = getPackageRow(db, package_id);
  if (!pkg) { const e = new Error('setup package not found'); e.code = 'not_found'; e.status = 404; throw e; }
  if (pkg.status !== 'approved' && pkg.status !== 'partially_applied') {
    const e = new Error(`setup package is "${pkg.status}", not approved`);
    e.code = 'package_not_approved';
    e.status = 403;
    throw e;
  }
  const engagement = getEngagement(pkg.engagement_id);
  if (!engagement) { const e = new Error('engagement not found'); e.code = 'not_found'; e.status = 404; throw e; }

  const trustMode = getMainAgentTrustMode();
  const items = db.prepare('SELECT * FROM hail_setup_package_items WHERE package_id = ? ORDER BY created_at ASC').all(package_id);

  for (const item of items) {
    if (TERMINAL_OK.has(item.apply_status) || item.apply_status === 'awaiting_secret') continue;
    if (!REPROCESSABLE.has(item.apply_status)) continue;

    // Defense-in-depth: an unknown/blocked risk never applies.
    if (!GOVERNANCE_RISK_SET.has(item.risk) || item.risk === 'blocked') {
      setItemStatus(db, item, { apply_status: 'skipped', error: item.risk === 'blocked' ? 'blocked risk — never applied' : `unknown risk "${item.risk}"` });
      appendAudit({ type: 'hail.setup.item.skipped', summary: `HAIL setup item ${item.ref} skipped (${item.risk})`, status: 'ok', meta: { package_id, ref: item.ref, risk: item.risk, user_id } });
      continue;
    }

    // Per-item gate.
    if (needsGate(item.risk, trustMode)) {
      if (item.approval_request_id) {
        const appr = getApprovalRequest(item.approval_request_id);
        if (!appr || appr.status === 'pending') continue; // still awaiting
        if (appr.status !== 'approved') {
          setItemStatus(db, item, { apply_status: 'failed', error: `approval ${appr.status}` });
          continue;
        }
        // Approved — consume it once (scoped to THIS package+ref so an unrelated
        // approval can't be replayed). If the consume fails (already used / scope
        // mismatch / race), do NOT apply — fail the item.
        const consumed = consumeApprovalRequest(item.approval_request_id, { action_type: APPLY_ACTION_TYPE, matches: (p) => p?.package_id === package_id && p?.ref === item.ref });
        if (!consumed) {
          setItemStatus(db, item, { apply_status: 'failed', error: 'approval could not be consumed (already used or scope mismatch)' });
          continue;
        }
      } else {
        const appr = createApprovalRequest({
          action_type: APPLY_ACTION_TYPE,
          risk: item.risk,
          summary: `Apply HAIL setup item ${item.ref} (${item.item_type}, ${item.risk}) for ${engagement.principal_name}`,
          payload: { package_id, ref: item.ref, item_type: item.item_type, engagement_id: engagement.id },
          requested_by: user_id || 'hail-setup',
          project_id: engagement.project_id || null,
        });
        setItemStatus(db, item, { apply_status: 'awaiting_approval', approval_request_id: appr.id });
        appendAudit({ type: 'hail.setup.item.gated', summary: `HAIL setup item ${item.ref} requires approval (${item.risk})`, status: 'pending', meta: { package_id, ref: item.ref, risk: item.risk, approval_request_id: appr.id, user_id } });
        continue;
      }
    }

    // Apply.
    try {
      const out = applyItem(item, { engagement, user_id });
      setItemStatus(db, item, { apply_status: out.apply_status, applied_target_id: out.applied_target_id, error: null });
      appendAudit({
        type: out.apply_status === 'awaiting_secret' ? 'hail.setup.item.awaiting_secret' : 'hail.setup.item.applied',
        summary: `HAIL setup item ${item.ref} ${out.apply_status}${out.note ? ` — ${out.note}` : ''}`,
        status: 'ok',
        meta: { package_id, ref: item.ref, item_type: item.item_type, risk: item.risk, applied_target_id: out.applied_target_id || null, user_id },
      });
    } catch (err) {
      setItemStatus(db, item, { apply_status: 'failed', error: String(err?.message || err) });
      appendAudit({ type: 'hail.setup.item.failed', summary: `HAIL setup item ${item.ref} failed: ${err?.message || err}`, status: 'error', meta: { package_id, ref: item.ref, user_id } });
    }
  }

  // Recompute package status.
  const after = db.prepare('SELECT apply_status FROM hail_setup_package_items WHERE package_id = ?').all(package_id);
  const allDone = after.every((r) => r.apply_status === 'applied' || r.apply_status === 'skipped');
  const status = allDone ? 'applied' : 'partially_applied';
  db.prepare('UPDATE hail_setup_packages SET status = ?, applied_at = COALESCE(applied_at, ?), updated_at = ? WHERE id = ?')
    .run(status, allDone ? nowIso() : null, nowIso(), package_id);

  appendAudit({
    type: 'hail.setup.apply',
    summary: `HAIL setup package ${package_id} → ${status} for ${engagement.principal_name}`,
    status: 'ok',
    meta: { package_id, engagement_id: engagement.id, status, items: after.length, applied: after.filter((r) => r.apply_status === 'applied').length, user_id },
  });

  return getApplyResult(db, package_id);
}

export function getApplyResult(db, package_id) {
  const dbh = db || getDb();
  const pkg = getPackageRow(dbh, package_id);
  if (!pkg) return null;
  const items = dbh.prepare('SELECT ref, item_type, risk, apply_status, applied_target_id, approval_request_id, error FROM hail_setup_package_items WHERE package_id = ? ORDER BY created_at ASC').all(package_id);
  return { package_id, engagement_id: pkg.engagement_id, status: pkg.status, applied_at: pkg.applied_at, items };
}

// Read a package by id (engagement-scoped helpers live in setup-package.js).
export function getPackageById(package_id) {
  return getPackageRow(getDb(), package_id);
}
