// lib/mines/diligence.js
//
// End-to-end diligence pipeline for a Mines project. Single orchestration shared
// by the UI route (POST /api/mines/projects/[id]/diligence), the main-agent chat
// trigger (mines_run_report), and the CLI demo, so they can never drift.
//
// ENGINE-AGNOSTIC (re-architecture): mines is a WORKFLOW, not an autonomous
// agent. The pipeline dispatches the tools itself as **direct gated calls**
// (executeRegistryTool with an agent identity → every tier/allowlist/rate-limit/
// approval/audit gate still applies) and uses the engine ONLY for the two genuine
// LLM steps — fact extraction and number-free prose — through the engine-abstracted
// authorJson helper. `engine` is a pure cost/scale tier (personal = Claude Code
// CLI subscription; production = Anthropic API key), never a capability gate.
//
// Provenance invariants are preserved on BOTH engines: numbers are rendered from
// cited SQL rows by the report tool; prose is number-free; any number that leaks
// into prose is redacted by proseSlot(). The LLM authors readable prose; it never
// authors a figure.
//
// Progress is broadcast as `mines_diligence` live events for real-time UI render.

import { getDb, newId, nowIso } from '../db.js';
import { executeRegistryTool } from '../tools/registry.js';
import { authorJson } from '../engine-author.js';
import { normalizeAgentEngine } from '../agent-engines.js';
import { retrieveChunks } from './rag.js';
import { setAgentToolPermission } from '../runtime.js';
import { publishLiveEvent } from '../live-events.js';
import { createSpace, getSpace, listSpaces, moveArtifactToSpace } from '../dashboard-spaces.js';
import { minesValuationRun } from '../tools/adapters/mines/valuation-run.js';
import { minesAssumptionValidate } from '../tools/adapters/mines/assumption-validate.js';

// Tool grants per Mines agent. Idempotent. These grants are what let the
// workflow dispatch the registry tools under an agent identity at trust-mode
// 'auto' (the agent-permission gate passes; every OTHER gate still runs). NB:
// granting tools is NOT the same as forcing an engine — we no longer flip any
// agent to 'anthropic'. The agents keep their configured engine (claude-code by
// default) and the LLM steps satisfy it through authorJson.
const AGENT_TOOLS = {
  'mines-data-interpreter': ['mines_project_read', 'mines_document_extract', 'mines_extract_facts', 'mines_assumption_validate'],
  'mines-report-packager': ['mines_project_read', 'mines_report_generate'],
  'mines-valuation-analyst': ['mines_project_read', 'mines_assumption_validate', 'mines_valuation_run', 'mines_sensitivity_run'],
  'mines-feasibility-lead': ['mines_project_read', 'mines_report_generate'],
  'mines-analyst': ['mines_project_read', 'mines_extract_facts', 'mines_report_generate'],
};

/**
 * Idempotently grant the Mines agents the tools the workflow dispatches on their
 * behalf, at mode 'auto'. Does NOT change any agent's engine (engine is a
 * runtime cost/scale choice resolved per run, not a stored capability).
 */
export function ensureMinesToolGrants() {
  const db = getDb();
  for (const [agentId, tools] of Object.entries(AGENT_TOOLS)) {
    if (!db.prepare('SELECT 1 FROM agents WHERE id = ?').get(agentId)) continue;
    for (const t of tools) {
      try { setAgentToolPermission(agentId, t, 'auto'); } catch { /* non-fatal */ }
    }
  }
}

/**
 * Resolve the engine for a mines run. Explicit opt wins; otherwise the
 * data-interpreter agent's configured engine; otherwise claude-code (personal
 * tier default). Never throws — returns a normalized engine id.
 */
export function resolveMinesEngine(opts = {}) {
  if (opts.engine) return normalizeAgentEngine(opts.engine);
  try {
    const row = getDb().prepare("SELECT engine FROM agents WHERE id = 'mines-data-interpreter'").get();
    if (row?.engine) return normalizeAgentEngine(row.engine);
  } catch { /* fall through */ }
  return 'claude-code';
}

/** Which agent identity packages a given report kind. */
function reportAgentFor(kind) {
  return kind === 'pfs_readiness_report' ? 'mines-feasibility-lead' : 'mines-report-packager';
}

function factCount(db, projectId) {
  return db.prepare('SELECT COUNT(*) n FROM mines_extracted_facts WHERE project_id = ?').get(projectId).n;
}

// ── LLM narrative authoring (prose only — never numbers) ────────────────────
// Every report's readable prose is authored by the LLM on the chosen engine;
// figures are rendered from cited SQL rows by the report tool. Slot keys map 1:1
// to proseSlot(...) reads in report-generate.js.
const NARRATIVE_SCHEMAS = {
  investor_presentation: {
    hero_headline: { type: 'string', description: 'One punchy line, < 12 words.' },
    hero_subhead: { type: 'string', description: '1–2 sentences framing the opportunity.' },
    opportunity: { type: 'array', items: { type: 'string' }, description: '2–3 short paragraphs on why this ground.' },
    findings_lead: { type: 'string' },
    upside_lead: { type: 'string' },
    risk_lead: { type: 'string', description: 'Lead with the risk honestly.' },
    the_ask: { type: 'string' },
  },
  data_quality_report: {
    lead: { type: 'string', description: 'One short paragraph introducing the state and coverage of the project data archive.' },
    gaps: { type: 'string', description: 'One short paragraph assessing data gaps and what would raise confidence.' },
  },
  interpretation_memo: {
    lead: { type: 'string', description: 'One short paragraph framing the geological interpretation.' },
    interpretation: { type: 'string', description: '1–2 paragraphs interpreting the geology and mineralization qualitatively.' },
  },
  due_diligence_memo: {
    exec_summary: { type: 'string', description: '1–2 paragraph executive summary of the opportunity.' },
    thesis: { type: 'string', description: 'One paragraph investment thesis.' },
    overview: { type: 'string', description: 'One short paragraph project overview.' },
    geology: { type: 'string', description: 'One short paragraph on geology and mineralization.' },
    exploration: { type: 'string', description: 'One short paragraph framing exploration results.' },
    comparables: { type: 'string', description: 'One short paragraph on comparable assets / analogues in the belt.' },
    risk_intro: { type: 'string', description: 'One short paragraph introducing the risk register.' },
    recommendation: { type: 'string', description: 'A short recommended next-phase work program, as prose or a numbered list.' },
  },
  // PFS-readiness: an honest GAP ASSESSMENT, never a Pre-Feasibility Study.
  // The tool computes every gate/section status + count from the database; the
  // LLM authors only these prose slots.
  pfs_readiness_report: {
    verdict: { type: 'string', description: 'Lead with the honest verdict: is a PFS supportable, and if not, the single hard blocker (almost always: no maiden mineral resource). Qualitative only.' },
    gap_intro: { type: 'string', description: 'One short paragraph introducing the data/gate gaps between today and PFS confidence.' },
    work_program: { type: 'string', description: 'The critical-path work program to reach PFS, ordered to the decision gates. Prose or a numbered list. No figures.' },
    recommendation: { type: 'string', description: 'A short closing recommendation on the next phase of work.' },
  },
};

const NARRATIVE_GUIDANCE = {
  investor_presentation: 'Write for a non-technical investor. Lead with the risk.',
  data_quality_report: 'Write for a diligence analyst assessing data completeness.',
  interpretation_memo: 'Write for a technical reviewer interpreting the geology.',
  due_diligence_memo: 'Write a credible institutional investment memorandum. Lead with the risk.',
  pfs_readiness_report: 'Write as a Feasibility Lead assessing PFS readiness. A PFS requires a Mineral Reserve converted from Measured & Indicated Resources. If there is no declared resource, say plainly that a PFS cannot be produced and frame the path to a maiden resource. Never present an exploration target as a resource.',
};

/**
 * Author the number-free prose for one report kind on the given engine. Returns
 * the validated slot object, or {} if authoring fails — figures come from cited
 * SQL rows regardless, so a sparse narrative degrades gracefully (the report
 * still renders; it is never fabricated). Hard failures are surfaced upstream.
 *
 * @param {Function} [authorFn]  Test seam; defaults to the real authorJson.
 */
async function authorNarrativeFor(db, project, kind, engine, authorFn) {
  if (!NARRATIVE_SCHEMAS[kind]) return {};
  const author = authorFn || authorJson;

  const facts = db.prepare('SELECT fact_type, value_text FROM mines_extracted_facts WHERE project_id = ? ORDER BY fact_type').all(project.id);
  let ragText = '';
  try {
    const { chunks } = await retrieveChunks(project.id, `${project.name} geology mineralization targets copper porphyry exploration`, 5);
    ragText = chunks.map((c) => `- (p${c.page ?? '?'}) ${String(c.text || '').replace(/\s+/g, ' ').slice(0, 300)}`).join('\n');
  } catch { /* RAG optional */ }

  const system = [
    `You are a mining-advisory analyst authoring the prose for a ${kind.replace(/_/g, ' ')}. ${NARRATIVE_GUIDANCE[kind] || ''}`,
    'Rules:',
    '1. NEVER state a numeric figure (grade, tonnage, value, percentage, depth, or digit count). Numbers are rendered separately from cited database rows. Write qualitative prose only.',
    '2. Never present a conceptual exploration target as a mineral resource.',
    '3. Ground every claim in the supplied facts. No outside knowledge, no invented geology.',
    '4. Fill every slot.',
  ].join('\n');
  const label = kind.replace(/_/g, ' ');
  const user = `PROJECT: ${project.name} (${project.commodity || 'Cu'}, ${project.asset_stage || 'exploration'}).\n\nEXTRACTED FACTS:\n${facts.map((f) => `- ${f.fact_type}: ${f.value_text}`).join('\n') || '(none)'}\n\nSOURCE EXCERPTS:\n${ragText || '(none)'}\n\nWrite the ${label} narrative now.`;

  try {
    return await author({ engine, schema: NARRATIVE_SCHEMAS[kind], system, user, label, temperature: 0.3, max_tokens: 2048 });
  } catch {
    // Prose-only authoring failed → empty slots. Figures still render from SQL.
    return {};
  }
}

/**
 * Author prose + generate one report through the DIRECT gated tool call. The
 * report tool runs under the packaging agent's identity, so the registry gate
 * (permission/allowlist/rate-limit/audit) still applies. Returns the tool result.
 */
async function packageReport({ db, project, space, kind, engine, user_id, org_id, authorFn }) {
  const narrative = await authorNarrativeFor(db, project, kind, engine, authorFn);
  const res = await executeRegistryTool(
    'mines_report_generate',
    { project_id: project.id, report_kind: kind, space_id: space.id, narrative },
    { agent_id: reportAgentFor(kind), acting_on_behalf_of_user_id: user_id, org_id, engine },
  );
  const ok = Boolean(res && (res.status === 'ok' || res.artifact_id));
  return { ok, result: res, narrative };
}

/** Resolve (or create) the dashboard space for a project's diligence artifacts. */
function resolveSpace(db, project, space_id) {
  let space = space_id ? getSpace(space_id) : null;
  if (!space && project.default_space_id) space = getSpace(project.default_space_id);
  if (!space) {
    space = listSpaces().spaces.find((s) => s.name === project.name) || createSpace({ name: project.name });
    db.prepare('UPDATE mines_projects SET default_space_id = ?, updated_at = ? WHERE id = ?').run(space.id, nowIso(), project.id);
  }
  return space;
}

/**
 * Run the full diligence pipeline for one project.
 *
 * @param {object} opts
 * @param {string} opts.projectId
 * @param {string} [opts.user_id='local-user']
 * @param {string} [opts.org_id='personal-org']
 * @param {string} [opts.space_id]
 * @param {string} [opts.engine]            'claude-code' (default tier) or 'anthropic'
 * @param {boolean} [opts.wipeFacts=false]
 * @param {string} [opts.run_id]
 * @param {Function} [opts._authorJson]     Test seam — overrides authorJson for narratives.
 * @returns {Promise<{ status, run_id, space_id, facts, artifacts, failed_kinds }>}
 */
export async function runDiligencePipeline({ projectId, user_id = 'local-user', org_id = 'personal-org', space_id = null, engine = null, wipeFacts = false, run_id = null, _authorJson = null } = {}) {
  const db = getDb();
  const project = db.prepare('SELECT * FROM mines_projects WHERE id = ?').get(projectId);
  if (!project) return { status: 'error', error: 'project_not_found' };

  const resolvedEngine = resolveMinesEngine({ engine });
  const rid = run_id || newId('mdil');
  const emit = (phase, status, message, extra = {}) => {
    try { publishLiveEvent({ type: 'mines_diligence', project_id: projectId, run_id: rid, phase, status, message, engine: resolvedEngine, ...extra }); } catch { /* non-fatal */ }
  };

  // Grant tools (no engine force). The agents keep their configured engine.
  ensureMinesToolGrants();

  const space = resolveSpace(db, project, space_id);
  emit('start', 'running', `Generating diligence for ${project.name} (${resolvedEngine})`, { space_id: space.id });

  const reportDoc = db.prepare(
    "SELECT id, file_name FROM mines_source_documents WHERE project_id = ? AND (file_name LIKE '%43-101%' OR file_name LIKE '%43_101%') ORDER BY id LIMIT 1"
  ).get(projectId);

  if (wipeFacts) db.prepare('DELETE FROM mines_extracted_facts WHERE project_id = ?').run(projectId);

  // ── 1. Extraction — DIRECT gated call (no agent tick) ──
  emit('extract', 'running', 'Extracting cited facts from the technical report…');
  try {
    const res = await executeRegistryTool(
      'mines_extract_facts',
      { project_id: projectId, source_document_id: reportDoc?.id || undefined, char_budget: 90000 },
      { agent_id: 'mines-data-interpreter', acting_on_behalf_of_user_id: user_id, org_id, engine: resolvedEngine },
    );
    if (res?.status && res.status !== 'ok') emit('extract', 'warn', `extract: ${res.error || res.status}`);
  } catch (e) {
    emit('extract', 'warn', `extract error: ${e.message}`);
  }
  const facts = factCount(db, projectId);
  emit('extract', facts > 0 ? 'done' : 'warn', `${facts} cited facts available`, { facts });

  // ── 2. Deterministic valuation (no LLM math; figures feed the reports) ──
  emit('valuation', 'running', 'Running tiered valuation…');
  let val = null;
  try {
    await minesAssumptionValidate({ project_id: projectId }, { agent_id: 'mines-valuation-analyst', user_id, org_id });
    val = await minesValuationRun({ project_id: projectId }, { agent_id: 'mines-valuation-analyst', user_id, org_id });
  } catch (e) {
    emit('valuation', 'warn', `valuation error: ${e.message}`);
  }
  emit('valuation', 'done', `tier ${val?.tier || 'n/a'}`);

  // ── 3. Author prose + generate every report via DIRECT gated calls ──
  // Figures render from cited SQL rows inside the tool; proseSlot redacts any
  // number that leaks into prose, so provenance holds on both engines.
  const REPORTS = [
    { kind: 'investor_presentation', title: `Investor Presentation — ${project.name}` },
    { kind: 'data_quality_report',  title: `Data Quality Report — ${project.name}` },
    { kind: 'interpretation_memo',  title: `Interpretation Memo — ${project.name}` },
    { kind: 'due_diligence_memo',   title: `Investment Diligence Memo — ${project.name}` },
    { kind: 'pfs_readiness_report', title: `PFS-Readiness Assessment — ${project.name}` },
  ];

  const failedKinds = [];
  for (const r of REPORTS) {
    const label = r.kind.replace(/_/g, ' ');
    emit('reports', 'running', `Authoring + generating ${label}…`, { kind: r.kind });
    let made = false;
    try {
      const { ok } = await packageReport({ db, project, space, kind: r.kind, engine: resolvedEngine, user_id, org_id, authorFn: _authorJson });
      made = ok;
    } catch (e) {
      emit('reports', 'warn', `${r.kind} error: ${e.message}`, { kind: r.kind });
    }
    if (made) emit('reports', 'done', `${label} ready`, { kind: r.kind });
    else { failedKinds.push(r.kind); emit('reports', 'warn', `${r.kind} not produced`, { kind: r.kind }); }
  }
  if (failedKinds.length) {
    emit('reports', 'error', `Incomplete package — ${failedKinds.length} report(s) failed: ${failedKinds.join(', ')}`, { failed_kinds: failedKinds });
  }
  db.prepare('UPDATE mines_projects SET has_technical_report = 1, updated_at = ? WHERE id = ?').run(nowIso(), projectId);

  // Collapse any duplicate artifacts in the space, keeping the newest per title.
  {
    const rows = db.prepare("SELECT id, title FROM artifacts WHERE space_id = ? ORDER BY created_at DESC, id DESC").all(space.id);
    const keep = new Set();
    const del = db.prepare('DELETE FROM artifacts WHERE id = ?');
    for (const row of rows) {
      if (keep.has(row.title)) del.run(row.id);
      else keep.add(row.title);
    }
  }

  const artifacts = db.prepare("SELECT id, kind, title FROM artifacts WHERE space_id = ? ORDER BY created_at DESC").all(space.id);
  for (const a of artifacts) { try { moveArtifactToSpace(a.id, space.id); } catch { /* ignore */ } }

  const incomplete = failedKinds.length > 0;
  const doneMsg = incomplete
    ? `Done with gaps — ${artifacts.length} artifacts in "${space.name}" (${failedKinds.length} report(s) failed)`
    : `Done — ${artifacts.length} artifacts in "${space.name}"`;
  emit('complete', incomplete ? 'warn' : 'done', doneMsg, { space_id: space.id, space_name: space.name, artifacts, failed_kinds: failedKinds });
  return { status: incomplete ? 'partial' : 'ok', run_id: rid, space_id: space.id, space_name: space.name, engine: resolvedEngine, facts, artifacts, failed_kinds: failedKinds };
}

/**
 * Generate ONE report for a project as a single action (used by the mines page
 * "single report" path and the main-agent `mines_run_report` trigger). Authors
 * the prose on the resolved engine and renders via the gated report tool.
 *
 * @param {object} opts
 * @param {string} opts.projectId
 * @param {string} opts.report_kind
 * @param {string} [opts.engine]
 * @param {string} [opts.user_id='local-user']
 * @param {string} [opts.org_id='personal-org']
 * @param {string} [opts.space_id]
 * @param {Function} [opts._authorJson]   Test seam.
 * @returns {Promise<{ status, report_kind, engine, artifact_id?, space_id?, error? }>}
 */
export async function runSingleReport({ projectId, report_kind, engine = null, user_id = 'local-user', org_id = 'personal-org', space_id = null, _authorJson = null } = {}) {
  if (!projectId) return { status: 'error', error: 'project_id_required' };
  if (!report_kind) return { status: 'error', error: 'report_kind_required' };
  if (!NARRATIVE_SCHEMAS[report_kind]) {
    return { status: 'error', error: 'unsupported_report_kind', report_kind, supported: Object.keys(NARRATIVE_SCHEMAS) };
  }

  const db = getDb();
  const project = db.prepare('SELECT * FROM mines_projects WHERE id = ?').get(projectId);
  if (!project) return { status: 'error', error: 'project_not_found' };

  const resolvedEngine = resolveMinesEngine({ engine });
  ensureMinesToolGrants();
  const space = resolveSpace(db, project, space_id);

  const rid = newId('mrep');
  try {
    publishLiveEvent({ type: 'mines_diligence', project_id: projectId, run_id: rid, phase: 'reports', status: 'running', message: `Generating ${report_kind.replace(/_/g, ' ')} (${resolvedEngine})`, engine: resolvedEngine, kind: report_kind });
  } catch { /* non-fatal */ }

  let pkg;
  try {
    pkg = await packageReport({ db, project, space, kind: report_kind, engine: resolvedEngine, user_id, org_id, authorFn: _authorJson });
  } catch (e) {
    return { status: 'error', error: 'report_failed', report_kind, engine: resolvedEngine, reason: e.message };
  }

  if (!pkg.ok) {
    return { status: 'error', error: 'report_not_produced', report_kind, engine: resolvedEngine, tool_result: pkg.result };
  }

  try {
    publishLiveEvent({ type: 'mines_diligence', project_id: projectId, run_id: rid, phase: 'complete', status: 'done', message: `${report_kind.replace(/_/g, ' ')} ready`, engine: resolvedEngine, kind: report_kind, space_id: space.id });
  } catch { /* non-fatal */ }

  return {
    status: 'ok',
    report_kind,
    engine: resolvedEngine,
    artifact_id: pkg.result?.artifact_id || null,
    space_id: space.id,
    space_name: space.name,
  };
}
