// lib/hail/setup-package.js — dry-run setup package authoring + reads.
//
// The setup package is the single shape gate between an approved HAIL blueprint
// and the runtime apply path (Phase 5). At Phase 4 it is authored DRY-RUN ONLY:
// nothing here mutates runtime configuration. validateSetupPackage enforces
// dry_run:true and rejects any secret value. The per-item ledger
// (hail_setup_package_items) records the proposed items as `pending` so the
// setup-review UI can show per-item cards; Phase 5's applier flips them.
//
// This module NEVER writes process_cases.* and never provisions runtime config.

import { getDb, nowIso, newId } from '../db.js';
import { appendAudit } from '../runtime.js';
import { validateSetupPackage } from './validators.js';
import { renderSetupPlanArtifact } from './artifacts.js';

// Persist a normalized (already validated) setup package + its item ledger.
// Supersedes prior dry-run packages for the engagement (version bump).
export function persistSetupPackage(db, engagement, normalizedPackage) {
  const now = nowIso();
  const eid = engagement.id;
  const prior = db.prepare('SELECT MAX(version) AS v FROM hail_setup_packages WHERE engagement_id = ?').get(eid);
  const version = (prior?.v || 0) + 1;

  // Only one current dry-run draft at a time. Drop the item ledger of any prior
  // dry-run draft being superseded so the items table never accumulates orphaned
  // `pending` rows (which a future apply query could otherwise pick up).
  const priorDraftIds = db.prepare("SELECT id FROM hail_setup_packages WHERE engagement_id = ? AND status = 'dry_run'").all(eid).map((r) => r.id);
  for (const pid of priorDraftIds) {
    db.prepare('DELETE FROM hail_setup_package_items WHERE package_id = ?').run(pid);
  }
  db.prepare("UPDATE hail_setup_packages SET status = 'superseded', updated_at = ? WHERE engagement_id = ? AND status = 'dry_run'").run(now, eid);

  const packageId = newId('hailpkg');
  db.prepare(`
    INSERT INTO hail_setup_packages (id, engagement_id, version, payload_json, status, created_at, updated_at)
    VALUES (?, ?, ?, ?, 'dry_run', ?, ?)
  `).run(packageId, eid, version, JSON.stringify(normalizedPackage), now, now);

  for (const item of normalizedPackage.items) {
    db.prepare(`
      INSERT INTO hail_setup_package_items
        (id, package_id, ref, item_type, risk, spec_json, apply_status, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
    `).run(newId('hailpkgi'), packageId, item.ref, item.type, item.risk, JSON.stringify(item.spec || {}), now, now);
  }
  return { packageId, version };
}

// Build, validate, persist, and render a dry-run setup package from a raw
// package payload (typically the blueprint agent's setup_package output, already
// restricted-scrubbed). Returns the package id + artifact id. Throws
// `setup_package_invalid` on a shape/secret-value violation.
export function buildSetupPackage(db, engagement, rawPackage) {
  // Force the engagement id + dry_run flag — never trust the author for these.
  const candidate = { ...rawPackage, engagement_id: engagement.id, dry_run: true };
  const result = validateSetupPackage(candidate);
  if (!result.ok) {
    const err = new Error(`setup package invalid: ${result.errors.join('; ')}`);
    err.code = 'setup_package_invalid';
    err.errors = result.errors;
    throw err;
  }
  const { packageId, version } = persistSetupPackage(db, engagement, result.normalized);
  const art = renderSetupPlanArtifact({ engagement, setup_package: result.normalized });
  db.prepare('UPDATE hail_setup_packages SET artifact_id = ?, updated_at = ? WHERE id = ?').run(art.artifact_id, nowIso(), packageId);

  appendAudit({
    type: 'hail.setup_package.build',
    summary: `HAIL dry-run setup package built for ${engagement.principal_name} (${result.normalized.items.length} items)`,
    status: 'ok',
    meta: { engagement_id: engagement.id, package_id: packageId, version, items: result.normalized.items.length, artifact_id: art.artifact_id },
  });
  return { package_id: packageId, version, artifact_id: art.artifact_id, items: result.normalized.items.length, warnings: result.warnings };
}

// Read the current setup package (+ item ledger) for an engagement — the latest
// non-superseded one, so it surfaces through its whole lifecycle (dry_run →
// approved → partially_applied → applied), not only while it is a dry-run draft.
export function getSetupPackage(engagement_id) {
  if (!engagement_id) return null;
  const db = getDb();
  const row = db.prepare("SELECT * FROM hail_setup_packages WHERE engagement_id = ? AND status != 'superseded' ORDER BY version DESC LIMIT 1").get(engagement_id);
  if (!row) return { engagement_id, has_package: false, items: [] };
  let payload = null;
  try { payload = JSON.parse(row.payload_json); } catch { payload = null; }
  const items = db.prepare('SELECT * FROM hail_setup_package_items WHERE package_id = ? ORDER BY created_at ASC').all(row.id).map((it) => ({
    ref: it.ref,
    type: it.item_type,
    risk: it.risk,
    apply_status: it.apply_status,
    spec: safeParse(it.spec_json),
  }));
  return {
    engagement_id,
    has_package: true,
    package_id: row.id,
    version: row.version,
    status: row.status,
    artifact_id: row.artifact_id,
    generated_by: payload?.generated_by || null,
    items,
  };
}

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