// lib/tools/adapters/mines/valuation-run.js
// P3 valuation adapters: synthetic-fixture-ready T1/T2 engine with hard DCF gates.

import { getDb, newId, nowIso } from '../../../db.js';
import { selectTier } from '../../../mines/valuation/tier-selector.js';
import { computeInSitu } from '../../../mines/valuation/t1-insitu.js';
import { computeDcf } from '../../../mines/valuation/t2-dcf.js';
import { t1Sensitivity } from '../../../mines/valuation/sensitivity.js';
import { getBrandProfile } from '../../../brand.js';

function safeJson(text, fallback = null) {
  if (!text) return fallback;
  try { return JSON.parse(text); } catch { return fallback; }
}

function latestFact(projectId, factType) {
  return getDb().prepare(`
    SELECT * FROM mines_extracted_facts
    WHERE project_id = ? AND fact_type = ?
    ORDER BY created_at DESC, id DESC
    LIMIT 1
  `).get(projectId, factType);
}

function latestPrice(commodity) {
  const row = getDb().prepare(`
    SELECT price_usd FROM mines_market_snapshots
    WHERE lower(commodity) = lower(?)
    ORDER BY fetched_at DESC LIMIT 1
  `).get(commodity);
  return row?.price_usd ?? null;
}

function avgAssays(projectId) {
  return getDb().prepare(`
    SELECT AVG(au_ppm) AS grade_au_gpt,
           AVG(ag_ppm) AS grade_ag_gpt,
           AVG(cu_pct) AS grade_cu_pct,
           COUNT(*) AS assay_count
    FROM mines_assay_samples WHERE project_id = ?
  `).get(projectId) || {};
}

function assumptionSourceFromFact(fact, field, value) {
  if (value == null) return null;
  return {
    field,
    value,
    source: fact ? 'extracted_fact' : 'default_assumption',
    fact_id: fact?.id ?? null,
    source_document_id: fact?.source_document_id ?? null,
    source_chunk_id: fact?.source_chunk_id ?? null,
    page_or_location: fact?.page_or_location ?? null,
  };
}

function buildT1Assumptions(project, input = {}) {
  const prices = typeof input.commodity_prices_json === 'string'
    ? safeJson(input.commodity_prices_json, {})
    : (input.commodity_prices_json || {});
  const resourceFact = latestFact(project.id, 'resource_estimate');
  const resourceJson = safeJson(resourceFact?.value_json, {});
  const assays = avgAssays(project.id);
  const resourceTonnes = resourceJson?.tonnes ?? resourceJson?.resource_tonnes ?? null;
  const gradeAu = resourceJson?.grade_au_gpt ?? resourceJson?.au_gpt ?? assays.grade_au_gpt ?? null;
  const gradeAg = resourceJson?.grade_ag_gpt ?? resourceJson?.ag_gpt ?? assays.grade_ag_gpt ?? null;
  const gradeCu = resourceJson?.grade_cu_pct ?? resourceJson?.cu_pct ?? assays.grade_cu_pct ?? null;
  const priceAu = prices.Au ?? prices.au ?? latestPrice('Au') ?? 2300;
  const priceAg = prices.Ag ?? prices.ag ?? latestPrice('Ag') ?? 28;
  const priceCu = prices.Cu ?? prices.cu ?? latestPrice('Cu') ?? 4.2;
  const inSituFactor = input.in_situ_factor_override ?? input.in_situ_factor ?? undefined;

  const assumptions = {
    project_id: project.id,
    asset_stage: project.asset_stage,
    resource_tonnes: resourceTonnes,
    grade_au_gpt: gradeAu,
    grade_ag_gpt: gradeAg,
    grade_cu_pct: gradeCu,
    price_au_usd_oz: priceAu,
    price_ag_usd_oz: priceAg,
    price_cu_usd_lb: priceCu,
    recovery_au: resourceJson?.recovery_au ?? input.recovery_au ?? 0.90,
    recovery_ag: resourceJson?.recovery_ag ?? input.recovery_ag ?? 0.75,
    recovery_cu: resourceJson?.recovery_cu ?? input.recovery_cu ?? 0.85,
    in_situ_factor: inSituFactor,
    assay_count: assays.assay_count ?? 0,
    provenance: [
      assumptionSourceFromFact(resourceFact, 'resource_tonnes', resourceTonnes),
      assumptionSourceFromFact(resourceFact, 'grade_au_gpt', gradeAu),
      assumptionSourceFromFact(resourceFact, 'grade_ag_gpt', gradeAg),
      assumptionSourceFromFact(resourceFact, 'grade_cu_pct', gradeCu),
      { field: 'price_au_usd_oz', value: priceAu, source: prices.Au || prices.au ? 'operator_override' : (latestPrice('Au') == null ? 'default_assumption' : 'market_snapshot') },
      { field: 'price_ag_usd_oz', value: priceAg, source: prices.Ag || prices.ag ? 'operator_override' : (latestPrice('Ag') == null ? 'default_assumption' : 'market_snapshot') },
      { field: 'price_cu_usd_lb', value: priceCu, source: prices.Cu || prices.cu ? 'operator_override' : (latestPrice('Cu') == null ? 'default_assumption' : 'market_snapshot') },
    ].filter(Boolean),
  };
  return assumptions;
}

function scenarioInput(base, scenario) {
  if (scenario === 'downside') {
    return {
      ...base,
      price_au_usd_oz: base.price_au_usd_oz * 0.8,
      price_ag_usd_oz: base.price_ag_usd_oz * 0.8,
      price_cu_usd_lb: base.price_cu_usd_lb * 0.8,
      in_situ_factor: (base.in_situ_factor ?? 0.03) * 0.8,
    };
  }
  if (scenario === 'upside') {
    return {
      ...base,
      price_au_usd_oz: base.price_au_usd_oz * 1.2,
      price_ag_usd_oz: base.price_ag_usd_oz * 1.2,
      price_cu_usd_lb: base.price_cu_usd_lb * 1.2,
      in_situ_factor: (base.in_situ_factor ?? 0.03) * 1.2,
    };
  }
  return base;
}

function insertArtifact({ projectId, kind = 'mines_valuation_report', title, content, sourceMd }) {
  const db = getDb();
  const id = newId('artifact');
  const now = nowIso();
  const path = `workspace/mines/${projectId}/${id}.html`;
  db.prepare(`
    INSERT INTO artifacts (id, run_id, agent_id, task_id, path, kind, title, content, source_md, created_at, project_id)
    VALUES (?, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)
  `).run(id, path, kind, title, content, sourceMd, now, projectId);
  return id;
}

function escapeHtml(value) {
  return String(value ?? '')
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}

function formatMoney(value) {
  if (value == null || !Number.isFinite(Number(value))) return 'Not available';
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    maximumFractionDigits: 0,
  }).format(Number(value));
}

function formatNumber(value, suffix = '') {
  if (value == null || !Number.isFinite(Number(value))) return 'Not available';
  return `${new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(Number(value))}${suffix}`;
}

function formatPercent(value) {
  if (value == null || !Number.isFinite(Number(value))) return 'Not available';
  return `${Number(value) > 0 ? '+' : ''}${Number(value).toFixed(1)}%`;
}

function scenarioValue(result = {}) {
  return result.in_situ_value_usd ?? result.npv_usd ?? null;
}

function hasValuationValue(scenarios) {
  return scenarios.some(s => scenarioValue(s.result) != null);
}

function scenarioRows(scenarios) {
  return scenarios.map((s) => {
    const r = s.result || {};
    return `<tr>
      <td><strong>${escapeHtml(s.scenario[0].toUpperCase() + s.scenario.slice(1))}</strong></td>
      <td>${escapeHtml(s.valuation_tier)}</td>
      <td>${formatNumber(r.contained_au_oz, ' oz Au')}</td>
      <td>${formatNumber(r.contained_cu_lb, ' lb Cu')}</td>
      <td>${formatMoney(r.gross_metal_value_usd)}</td>
      <td>${formatMoney(scenarioValue(r))}</td>
    </tr>`;
  }).join('');
}

function sensitivityRows(sensitivity) {
  const drivers = sensitivity?.drivers || {};
  return Object.entries(drivers).flatMap(([driver, points]) => {
    if (!Array.isArray(points)) return [];
    return points.map((p) => `<tr>
      <td>${escapeHtml(driver.replaceAll('_', ' '))}</td>
      <td>${escapeHtml(p.label)}</td>
      <td>${formatMoney(p.value_usd)}</td>
      <td>${formatPercent(p.delta_pct)}</td>
    </tr>`);
  }).join('') || '<tr><td colspan="4">Sensitivity could not be calculated from the available inputs.</td></tr>';
}

function provenanceRows(scenarios) {
  const base = scenarios.find(s => s.scenario === 'base') || scenarios[0];
  const assumptions = base?.assumptions || {};
  const rows = Object.entries(assumptions)
    .filter(([key]) => !['provenance', 'scenario', 'project_id'].includes(key))
    .map(([key, value]) => {
      const source = assumptions.provenance?.find(p => p.field === key);
      return `<tr>
        <td>${escapeHtml(key)}</td>
        <td>${escapeHtml(value == null ? 'Not available' : value)}</td>
        <td>${escapeHtml(source?.source || 'derived/default')}</td>
        <td>${escapeHtml(source?.fact_id || source?.source_document_id || source?.page_or_location || '—')}</td>
      </tr>`;
    });
  return rows.join('') || '<tr><td colspan="4">No assumptions were recorded.</td></tr>';
}

function dataGapList(tier, scenarios) {
  const base = scenarios.find(s => s.scenario === 'base') || scenarios[0];
  const gaps = [];
  if (base?.assumptions?.resource_tonnes == null) gaps.push('No resource tonnage is available; T1 can show readiness only and must not invent resource figures.');
  if (base?.assumptions?.grade_au_gpt == null && base?.assumptions?.grade_cu_pct == null && base?.assumptions?.grade_ag_gpt == null) gaps.push('No project grade fact is available; import assays or extract resource facts before valuation.');
  for (const missing of tier.blocking_inputs_missing || []) gaps.push(`T2 DCF input missing or unsuitable: ${missing}.`);
  if (!gaps.length) gaps.push('No blocking data gaps for the selected tier were detected.');
  return gaps;
}

function jsonForScript(value) {
  return JSON.stringify(value).replace(/</g, '\\u003c');
}

function reportHtml(project, tier, scenarios, sensitivity) {
  const ready = hasValuationValue(scenarios);
  const title = ready ? `Mines valuation report — ${project.name}` : `Valuation readiness report — ${project.name}`;
  const base = scenarios.find(s => s.scenario === 'base') || scenarios[0];
  const value = scenarioValue(base?.result || {});
  const gaps = dataGapList(tier, scenarios);
  const conclusion = ready
    ? `The agent selected ${tier.tier} and produced an indicative valuation range. Base-case value is ${formatMoney(value)}.`
    : 'The agent completed the tier check, but valuation output is readiness-only because critical resource inputs are missing.';
  const t2Gate = tier.t2_blocked ? tier.t2_blocking_reason : 'T2 DCF inputs are complete and the asset stage is DCF-ready.';
  const brand = getBrandProfile();
  const brandName = brand?.company_name || 'madarX';
  const logo = brand?.active && brand.logo_data_url
    ? `<img src="${escapeHtml(brand.logo_data_url)}" alt="" class="brand-logo">`
    : '<span class="brand-dot" aria-hidden="true"></span>';

  return `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${escapeHtml(title)}</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Cairo:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
  <style>
    :root{color-scheme:dark;--onyx:#0F0F11;--onyx-2:#171719;--onyx-3:#202024;--platinum:#E8E8E5;--bone:#DCDCD8;--graphite:#4A4A4D;--graphite-2:#888883;--mars:#C2452A;--mars-soft:rgba(194,69,42,.14);--rule:rgba(232,232,229,.12);--rule-strong:rgba(232,232,229,.24);--good:#5FA97A;--warn:#D4A341;--font-sans:"Inter","Cairo",system-ui,-apple-system,"Segoe UI",sans-serif;--font-mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
    *{box-sizing:border-box} html,body{margin:0;padding:0} body{min-height:100vh;background:radial-gradient(circle at 10% 0%,rgba(194,69,42,.18),transparent 26rem),linear-gradient(135deg,#0F0F11 0%,#171719 54%,#0A0A0B 100%);color:var(--platinum);font-family:var(--font-sans);font-size:15px;line-height:1.62;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;padding:clamp(18px,4vw,48px)}
    main{max-width:1120px;margin:0 auto;background:linear-gradient(180deg,rgba(232,232,229,.055),rgba(232,232,229,.025));border:1px solid var(--rule);border-radius:28px;padding:clamp(22px,4vw,48px);box-shadow:0 32px 100px rgba(0,0,0,.42)}
    .brand{display:flex;align-items:center;gap:12px;margin-bottom:34px;color:var(--graphite-2);font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.14em;text-transform:uppercase}.brand-logo{width:34px;max-height:34px;object-fit:contain}.brand-dot{width:9px;height:9px;border-radius:50%;background:var(--mars);box-shadow:0 0 0 6px var(--mars-soft)}
    .eyebrow{display:inline-flex;align-items:center;gap:10px;color:var(--mars);font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.16em;text-transform:uppercase}.eyebrow::before{content:"";width:28px;height:1px;background:var(--mars)}
    h1{max-width:850px;margin:12px 0 12px;color:#fff;font-size:clamp(2.1rem,5.2vw,4.6rem);font-weight:800;letter-spacing:-.055em;line-height:.96}h2{margin:34px 0 12px;padding-top:24px;border-top:1px solid var(--rule);color:#fff;font-size:clamp(1.05rem,2vw,1.45rem);letter-spacing:-.02em}p{color:var(--bone)}strong{color:#fff}.small{color:var(--graphite-2);font-size:13px}.mono{font-family:var(--font-mono)}
    .note{margin:28px 0;background:rgba(15,15,17,.58);border:1px solid var(--rule);border-left:4px solid var(--warn);border-radius:18px;padding:18px 20px}.note.ok{border-left-color:var(--good)}.note h2{border:0;margin:0 0 6px;padding:0}.note p{margin:.4em 0 0}
    .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;margin:24px 0}.card{position:relative;overflow:hidden;border:1px solid var(--rule);border-radius:18px;padding:16px;background:rgba(232,232,229,.055)}.card::after{content:"";position:absolute;inset:auto 0 0;height:2px;background:linear-gradient(90deg,var(--mars),transparent)}.label{color:var(--graphite-2);font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.12em;font-weight:600}.value{color:#fff;font-size:20px;font-weight:700;letter-spacing:-.02em;margin-top:6px}.pill{display:inline-block;border:1px solid var(--rule-strong);border-radius:999px;padding:4px 9px;background:rgba(232,232,229,.06);color:#fff;font-size:12px;font-weight:600}
    table{width:100%;border-collapse:collapse;margin:14px 0 22px;background:rgba(15,15,17,.42);border:1px solid var(--rule);border-radius:16px;overflow:hidden}th,td{border-bottom:1px solid var(--rule);text-align:left;padding:12px 12px;font-size:13px;vertical-align:top}th{background:rgba(232,232,229,.07);color:var(--graphite-2);font-family:var(--font-mono);text-transform:uppercase;font-size:10.5px;letter-spacing:.12em;font-weight:600}tr:last-child td{border-bottom:0}td{color:var(--bone)}ul,ol{color:var(--bone);padding-left:1.25rem}li{margin:.42rem 0}script{display:none}@media(max-width:720px){body{padding:12px}main{border-radius:20px}table{display:block;overflow-x:auto;white-space:nowrap}h1{font-size:2.15rem}}
  </style>
</head>
<body>
<main>
  <div class="brand">${logo}<span>${escapeHtml(brandName)} · Mines artifact</span></div>
  <div class="eyebrow">MadarX OS · Mines · Agent-produced artifact</div>
  <h1>${escapeHtml(title)}</h1>
  <p class="small">Project: ${escapeHtml(project.name)} · Asset stage: ${escapeHtml(project.asset_stage || 'unknown')} · Commodity: ${escapeHtml(project.commodity || 'unknown')}</p>

  <section class="note ${ready ? 'ok' : ''}">
    <h2>Executive conclusion</h2>
    <p>${escapeHtml(conclusion)}</p>
  </section>

  <section class="grid" aria-label="valuation summary">
    <div class="card"><div class="label">Selected tier</div><div class="value">${escapeHtml(tier.tier)}</div></div>
    <div class="card"><div class="label">Confidence</div><div class="value">${escapeHtml(tier.confidence || 'indicative')}</div></div>
    <div class="card"><div class="label">Base value</div><div class="value">${formatMoney(value)}</div></div>
    <div class="card"><div class="label">DCF gate</div><div class="value">${tier.t2_blocked ? 'Blocked' : 'Allowed'}</div></div>
  </section>

  <h2>Selected tier rationale</h2>
  <p>${escapeHtml(tier.reason)}</p>
  <p><strong>T2 DCF blocked status:</strong> ${escapeHtml(t2Gate)}</p>

  <h2>Scenario table</h2>
  <table>
    <thead><tr><th>Scenario</th><th>Tier</th><th>Contained Au</th><th>Contained Cu</th><th>Gross metal value</th><th>Valuation output</th></tr></thead>
    <tbody>${scenarioRows(scenarios)}</tbody>
  </table>

  <h2>Sensitivity</h2>
  <table>
    <thead><tr><th>Driver</th><th>Case</th><th>Output</th><th>Delta from base</th></tr></thead>
    <tbody>${sensitivityRows(sensitivity)}</tbody>
  </table>

  <h2>Assumptions and provenance</h2>
  <table>
    <thead><tr><th>Input</th><th>Value</th><th>Source</th><th>Provenance</th></tr></thead>
    <tbody>${provenanceRows(scenarios)}</tbody>
  </table>

  <h2>Data gaps and T2 blocking reasons</h2>
  <ul>${gaps.map(g => `<li>${escapeHtml(g)}</li>`).join('')}</ul>

  <h2>Limitations</h2>
  <p>This artifact is generated by the Mines agent from currently available structured facts, assay aggregates, market snapshots, operator overrides, and default assumptions. It does not include confidential Batamote files and must not be treated as a definitive valuation until the NI 43-101 reconciliation gate is completed.</p>

  <h2>Recommended next actions</h2>
  <ol>
    <li>Import or extract the missing resource, reserve, operating-cost, and production-schedule facts listed above.</li>
    <li>When confidential Batamote files are available, populate the NI 43-101 fixture and run the ±15% reconciliation gate.</li>
    <li>Use the generated scenario and sensitivity outputs as the artifact record; do not treat any UI surface as the valuation source of truth.</li>
  </ol>

  <p class="small mono">Artifact kind: ${ready ? 'mines_valuation_report' : 'mines_valuation_readiness_report'}</p>
  <script type="application/json" id="valuation-payload">${jsonForScript({ tier, scenarios, sensitivity })}</script>
</main>
</body>
</html>`;
}

function insertScenario({ projectId, tier, scenario, assumptions, result, sensitivity, artifactId, confidence }) {
  const id = newId('mines-val');
  getDb().prepare(`
    INSERT INTO mines_valuation_scenarios
      (id, project_id, valuation_tier, scenario, assumptions_json, result_json, tier_rationale, sensitivity_json, confidence, created_by_agent_id, artifact_id, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
  `).run(
    id,
    projectId,
    tier.tier,
    scenario,
    JSON.stringify(assumptions),
    JSON.stringify(result),
    tier.reason,
    sensitivity ? JSON.stringify(sensitivity) : null,
    confidence,
    artifactId,
    nowIso(),
  );
  return id;
}

export async function minesValuationRun(input = {}) {
  const projectId = input.project_id;
  if (!projectId) return { status: 'error', error: 'project_id_required' };
  const db = getDb();
  const project = db.prepare('SELECT * FROM mines_projects WHERE id = ?').get(projectId);
  if (!project) return { status: 'error', error: 'not_found' };

  const tier = selectTier(projectId, { force_tier: input.force_tier });
  if (tier.status === 'error') return tier;

  const scenarios = [];
  let sensitivity = null;
  if (tier.tier === 't2_dcf') {
    const prod = safeJson(latestFact(projectId, 'production_schedule')?.value_json, {});
    const opex = safeJson(latestFact(projectId, 'opex')?.value_json, {});
    const prices = typeof input.commodity_prices_json === 'string' ? safeJson(input.commodity_prices_json, {}) : (input.commodity_prices_json || {});
    const assumptions = { ...prod, ...opex, price_au_usd_oz: prices.Au ?? prices.au ?? latestPrice('Au') ?? 2300, discount_rate: input.discount_rate ?? 0.08 };
    for (const scenario of ['base', 'downside', 'upside']) {
      const f = scenario === 'downside' ? 0.8 : scenario === 'upside' ? 1.2 : 1;
      const result = computeDcf({ ...assumptions, price_au_usd_oz: assumptions.price_au_usd_oz * f });
      scenarios.push({ scenario, valuation_tier: 't2_dcf', assumptions: { ...assumptions, scenario }, result });
    }
  } else {
    const base = buildT1Assumptions(project, input);
    for (const scenario of ['base', 'downside', 'upside']) {
      const assumptions = scenarioInput(base, scenario);
      const result = computeInSitu(assumptions);
      scenarios.push({ scenario, valuation_tier: 't1_in_situ', assumptions: { ...assumptions, scenario }, result });
      if (scenario === 'base') sensitivity = t1Sensitivity(assumptions, { grade_std_au_gpt: input.grade_std_au_gpt ?? 0.1, grade_std_cu_pct: input.grade_std_cu_pct ?? 0.05 });
    }
  }

  const valuationReady = hasValuationValue(scenarios);
  const artifactKind = valuationReady ? 'mines_valuation_report' : 'mines_valuation_readiness_report';
  const artifactTitle = valuationReady
    ? `Mines valuation report — ${project.name}`
    : `Valuation readiness report — ${project.name}`;
  const artifactId = insertArtifact({
    projectId,
    kind: artifactKind,
    title: artifactTitle,
    content: reportHtml(project, tier, scenarios, sensitivity),
    sourceMd: JSON.stringify({ tier, scenarios, sensitivity }, null, 2),
  });

  const scenarioIds = scenarios.map(s => insertScenario({
    projectId,
    tier,
    scenario: s.scenario,
    assumptions: s.assumptions,
    result: s.result,
    sensitivity: s.scenario === 'base' ? sensitivity : null,
    artifactId,
    confidence: tier.confidence === 'modelled' ? 0.8 : tier.confidence === 'moderate' ? 0.6 : 0.35,
  }));

  return {
    status: 'ok',
    project_id: projectId,
    tier: tier.tier,
    tier_selection: tier,
    t2_blocked: tier.t2_blocked,
    t2_blocking_reason: tier.t2_blocking_reason,
    scenario_ids: scenarioIds,
    artifact_id: artifactId,
    scenarios,
    sensitivity,
  };
}

export async function minesSensitivityRun(input = {}) {
  const scenarioId = input.scenario_id;
  if (!scenarioId) return { status: 'error', error: 'scenario_id_required' };
  const db = getDb();
  const row = db.prepare('SELECT * FROM mines_valuation_scenarios WHERE id = ?').get(scenarioId);
  if (!row) return { status: 'error', error: 'not_found' };
  if (row.valuation_tier !== 't1_in_situ') {
    return { status: 'error', error: 'sensitivity_not_implemented_for_t2' };
  }
  const assumptions = safeJson(row.assumptions_json, {});
  const sensitivity = t1Sensitivity(assumptions, {
    grade_std_au_gpt: input.grade_std_au_gpt ?? 0.1,
    grade_std_cu_pct: input.grade_std_cu_pct ?? 0.05,
  });
  db.prepare('UPDATE mines_valuation_scenarios SET sensitivity_json = ? WHERE id = ?').run(JSON.stringify(sensitivity), scenarioId);
  return { status: 'ok', scenario_id: scenarioId, sensitivity };
}
