// lib/process/artifacts.js — artifact-first presentation surfaces for the
// process engine.
//
// PRODUCT INVARIANT: every workflow touchpoint that a user or colleague sees
// is an HTML artifact, not a bare dashboard form / JSON panel. Artifacts are
// the canonical presentation + audit medium of the engine.
//
// This module renders three kinds of artifacts that all flow through
// `lib/runtime.js::createArtifact` so they land in the project library
// automatically:
//
//   1. Intake form artifact      — the empty intake page shown at case start.
//   2. Intake submission artifact — receipt of the submitted payload, PII redacted.
//   3. Analysis artifact          — HTML representation of an agent_run output
//                                    (completeness / KYC etc.).
//
// Artifacts are *presentation evidence*, not the state machine source of
// truth. The engine remains the sole writer of `process_cases` state; this
// module only produces side-effect HTML rows under the case's project.

import { createArtifact } from '../runtime.js';
import { redactSensitiveId } from './redact.js';

// ── Helpers ─────────────────────────────────────────────────────────────────

function escapeHtml(s) {
  return String(s ?? '').replace(/[&<>"']/g, (c) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
  }[c]));
}

// Re-exported for any external callers that import from artifacts.js.
export { redactSensitiveId };

// Field names that are treated as sensitive identifiers and redacted in HTML
// artifacts. Adding new sensitive intake fields? Extend this set.
const SENSITIVE_INTAKE_FIELDS = new Set([
  'passport_number',
  'national_id',
  'ssn',
  'tax_id',
]);

function renderFieldValueHtml(field, value) {
  if (value == null || value === '') return '<em class="muted">— not provided —</em>';
  if (SENSITIVE_INTAKE_FIELDS.has(field?.name)) {
    return `<code>${escapeHtml(redactSensitiveId(value))}</code>`;
  }
  if (field?.type === 'shareholders' && Array.isArray(value)) {
    if (value.length === 0) return '<em class="muted">— none —</em>';
    const rows = value
      .map((s, i) => `
        <tr>
          <td>${i + 1}</td>
          <td>${escapeHtml(s?.name || '')}</td>
          <td>${escapeHtml(String(s?.percentage ?? ''))}%</td>
        </tr>`)
      .join('');
    return `
      <table class="art-table">
        <thead><tr><th>#</th><th>Name</th><th>Share</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>`;
  }
  if (Array.isArray(value)) {
    return `<ul>${value.map((v) => `<li>${escapeHtml(JSON.stringify(v))}</li>`).join('')}</ul>`;
  }
  if (typeof value === 'object') {
    return `<pre>${escapeHtml(JSON.stringify(value, null, 2))}</pre>`;
  }
  if (field?.type === 'textarea') {
    return `<p style="white-space:pre-wrap">${escapeHtml(value)}</p>`;
  }
  return escapeHtml(value);
}

// Minimal self-contained HTML document. Uses inline CSS only so the artifact
// renders identically wherever it is embedded (project library iframe, share
// link, mobile view). No external assets / fonts — matches the existing
// `renderDocumentHtml` philosophy.
function htmlShell({ title, kicker, bodyHtml }) {
  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>
<style>
  :root {
    color-scheme: light;
    --bg: #0e0d0c;
    --panel: #f7f4ee;
    --ink: #1a1817;
    --ink-soft: #5f5a55;
    --line: #d9d4cb;
    --accent: #b3261e;
    --pill-bg: #ece6d9;
  }
  body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font: 14px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
    padding: 32px 16px;
  }
  .art {
    max-width: 720px;
    margin: 0 auto;
    background: var(--panel);
    border-radius: 18px;
    box-shadow: 0 24px 60px rgba(0,0,0,0.35);
    padding: 40px 44px 48px;
  }
  .art-kicker {
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: 11px;
    color: var(--ink-soft);
    margin: 0 0 12px;
  }
  .art h1 {
    font-size: 26px;
    margin: 0 0 6px;
    letter-spacing: -0.01em;
  }
  .art h2 {
    font-size: 18px;
    margin: 28px 0 12px;
    border-top: 1px solid var(--line);
    padding-top: 18px;
  }
  .art .muted { color: var(--ink-soft); }
  .art .meta {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 16px;
    color: var(--ink-soft);
    font-size: 12px;
    margin-bottom: 24px;
  }
  .art .meta span { display: inline-flex; gap: 4px; }
  .art .pill {
    display: inline-block;
    padding: 3px 10px;
    background: var(--pill-bg);
    border-radius: 999px;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ink);
  }
  .art .pill.warn { background: #f1d6a8; }
  .art .pill.bad  { background: #f1c5c0; color: #6e1410; }
  .art .pill.ok   { background: #cfe7c8; color: #1e4a17; }
  .art .field {
    margin: 0 0 14px;
  }
  .art .field-label {
    display: block;
    font-weight: 600;
    color: var(--ink);
    margin-bottom: 2px;
  }
  .art .field-help {
    color: var(--ink-soft);
    font-size: 12px;
    margin: 2px 0 6px;
  }
  .art .field-value {
    color: var(--ink);
    font-size: 14px;
  }
  .art .req::after { content: " *"; color: var(--accent); }
  .art-table {
    width: 100%;
    border-collapse: collapse;
    margin: 6px 0 0;
  }
  .art-table th, .art-table td {
    text-align: left;
    padding: 6px 10px;
    border-bottom: 1px solid var(--line);
    font-size: 13px;
  }
  .art-table th {
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 11px;
  }
  .art .note {
    background: #fff;
    border-left: 3px solid var(--accent);
    padding: 10px 14px;
    margin: 18px 0;
    color: var(--ink-soft);
    font-size: 12px;
    border-radius: 4px;
  }
  .art pre {
    background: #181615;
    color: #f1ece1;
    padding: 12px 14px;
    border-radius: 8px;
    overflow-x: auto;
    font-size: 12px;
    line-height: 1.45;
  }
  .art code { font-size: 13px; }
  .art ul { padding-left: 20px; }
  .art .row { display: flex; gap: 24px; flex-wrap: wrap; }
  .art .row > div { flex: 1 1 220px; }
  .art-footer {
    margin-top: 28px;
    color: var(--ink-soft);
    font-size: 11px;
    border-top: 1px solid var(--line);
    padding-top: 14px;
  }
</style>
</head>
<body>
  <article class="art">
    ${kicker ? `<p class="art-kicker">${escapeHtml(kicker)}</p>` : ''}
    ${bodyHtml}
  </article>
</body>
</html>`;
}

// ── Renderers ───────────────────────────────────────────────────────────────

// Renders the branded interactive intake form — a single self-contained HTML
// document with REAL form controls that POSTs to `submit_endpoint`. This is the
// only intake-form shape as of v2.14.0; the legacy read-only "preview" renderer
// and its MADARX_BRANDED_INTAKE flag were removed.
export function renderIntakeFormHtml({ definition, schema, caseRow, settings = null, submit_endpoint = null, submit_token = null }) {
  return renderIntakeFormInteractiveHtml({ definition, schema, caseRow, settings, submit_endpoint, submit_token });
}

// ── Branded interactive renderer (Phase 2) ──────────────────────────────────
//
// Renders a single self-contained HTML document with REAL form controls. Input
// `name` attributes come straight from `schema.fields[].name` — the locked
// engine contract. The wrapping (cover, copy, accent) comes from `settings`.
// Submission goes through fetch() to `submit_endpoint` with `submit_token`
// in the body when present (anonymous share-link mode), or the page-issued
// session cookie otherwise (authenticated dashboard embed).
//
// JS budget ~3 KB: shareholders repeater (add/remove + percent total),
// mirrored client-side validation (presence + sum=100), submit handler.
// All DOM creation uses document.createElement — no innerHTML, no
// template-literal HTML in the browser JS.

function renderControlHtml(field) {
  const name = String(field.name || '').replace(/[^a-zA-Z0-9_-]/g, '');
  if (!name) return '';
  const req = field.required ? ' required' : '';
  const placeholder = field.placeholder ? ` placeholder="${escapeHtml(field.placeholder)}"` : '';
  if (field.type === 'select' && Array.isArray(field.options)) {
    const opts = ['<option value="">Pick one…</option>']
      .concat(field.options.map((o) => `<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`))
      .join('');
    return `<select class="bi-input" name="${escapeHtml(name)}"${req}>${opts}</select>`;
  }
  if (field.type === 'textarea') {
    return `<textarea class="bi-input bi-textarea" name="${escapeHtml(name)}" rows="5"${placeholder}${req}></textarea>`;
  }
  if (field.type === 'shareholders') {
    return `
      <div class="bi-shareholders" data-shareholders data-name="${escapeHtml(name)}">
        <div class="bi-shareholders-rows"></div>
        <button type="button" class="bi-add-shareholder">+ Add shareholder</button>
        <div class="bi-shareholders-total"><span>Total share</span><strong data-total>0%</strong></div>
        <p class="bi-shareholders-help">Percentages must total exactly 100.</p>
      </div>`;
  }
  if (field.type === 'number') {
    return `<input class="bi-input" type="number" name="${escapeHtml(name)}" step="any"${placeholder}${req} />`;
  }
  const inputType = field.type === 'email' ? 'email' : 'text';
  return `<input class="bi-input" type="${inputType}" name="${escapeHtml(name)}"${placeholder}${req} />`;
}

function renderIntakeFormInteractiveHtml({ definition, schema, caseRow, settings, submit_endpoint, submit_token }) {
  const fields = Array.isArray(schema?.fields) ? schema.fields : [];
  const brand = settings?.brand || {};
  const accent = brand.accent_hex || '#c2452a';
  const senderName = brand.sender_name || 'Madarx OS';
  const logoUrl = brand.logo_url || '';
  const coverImage = brand.cover_image_url || '';
  const coverTitle = brand.cover_copy?.title || schema?.title || `${definition?.name || 'Process'} — Intake`;
  const coverSubtitle = brand.cover_copy?.subtitle || schema?.description || 'Submit the details below to start.';

  const fieldsHtml = fields.map((f) => `
    <div class="bi-field">
      <label class="bi-label" for="bi-${escapeHtml(f.name)}">${escapeHtml(f.label || f.name)}${f.required ? ' <span class="bi-req">*</span>' : ''}</label>
      ${f.help ? `<p class="bi-help">${escapeHtml(f.help)}</p>` : ''}
      ${renderControlHtml(f)}
    </div>
  `).join('');

  const shareholderFieldName = fields.find((f) => f.type === 'shareholders')?.name || null;
  const fieldsJson = JSON.stringify(fields.map((f) => ({ name: f.name, type: f.type, required: !!f.required })));
  const endpointJson = JSON.stringify(submit_endpoint || '');
  const tokenJson = JSON.stringify(submit_token || null);
  const caseIdJson = JSON.stringify(caseRow?.id || '');
  const shareholderJson = JSON.stringify(shareholderFieldName);

  // Browser script. Builds shareholder rows via createElement only (no
  // innerHTML) so static HTML strings never round-trip through the DOM parser.
  const inlineScript = `
(function () {
  var FIELDS = ${fieldsJson};
  var ENDPOINT = ${endpointJson};
  var TOKEN = ${tokenJson};
  var CASE_ID = ${caseIdJson};
  var SHAREHOLDER_FIELD = ${shareholderJson};

  function el(tag, attrs, parent) {
    var n = document.createElement(tag);
    if (attrs) Object.keys(attrs).forEach(function (k) {
      if (k === 'class') n.className = attrs[k];
      else if (k === 'text') n.textContent = attrs[k];
      else n.setAttribute(k, attrs[k]);
    });
    if (parent) parent.appendChild(n);
    return n;
  }

  function addShareholderRow(container) {
    var rows = container.querySelector('.bi-shareholders-rows');
    var row = el('div', { class: 'bi-shareholders-row' });
    el('span', { class: 'bi-shareholders-index', text: String(rows.children.length + 1) }, row);
    el('input', { class: 'bi-input bi-shareholders-name', placeholder: 'Full name' }, row);
    el('input', { class: 'bi-input bi-shareholders-pct', type: 'number', min: '0', max: '100', step: 'any', placeholder: '%' }, row);
    var rm = el('button', { type: 'button', class: 'bi-shareholders-remove', 'aria-label': 'Remove shareholder', text: '\\u00d7' }, row);
    rows.appendChild(row);
    rm.addEventListener('click', function () {
      rows.removeChild(row);
      updateTotal(container);
      reindex(container);
    });
    row.querySelectorAll('input').forEach(function (i) { i.addEventListener('input', function () { updateTotal(container); }); });
  }

  function reindex(container) {
    var rs = container.querySelectorAll('.bi-shareholders-row');
    rs.forEach(function (r, i) { r.querySelector('.bi-shareholders-index').textContent = String(i + 1); });
  }

  function updateTotal(container) {
    var rs = container.querySelectorAll('.bi-shareholders-row');
    var total = 0;
    rs.forEach(function (r) { total += Number(r.querySelector('.bi-shareholders-pct').value || 0); });
    var elTotal = container.querySelector('[data-total]');
    elTotal.textContent = (Math.round(total * 100) / 100) + '%';
    elTotal.classList.toggle('bi-ok', Math.abs(total - 100) < 0.01);
    elTotal.classList.toggle('bi-bad', rs.length > 0 && Math.abs(total - 100) >= 0.01);
  }

  function collectShareholders(container) {
    var rs = container.querySelectorAll('.bi-shareholders-row');
    var out = [];
    rs.forEach(function (r) {
      var name = r.querySelector('.bi-shareholders-name').value.trim();
      var pct = Number(r.querySelector('.bi-shareholders-pct').value || 0);
      if (name || pct) out.push({ name: name, percentage: pct });
    });
    return out;
  }

  function showError(msg) {
    var elError = document.querySelector('[data-bi-error]');
    elError.textContent = msg || '';
    elError.style.display = msg ? 'block' : 'none';
  }

  document.querySelectorAll('[data-shareholders]').forEach(function (c) {
    c.querySelector('.bi-add-shareholder').addEventListener('click', function () { addShareholderRow(c); });
    addShareholderRow(c);
  });

  document.querySelector('[data-bi-form]').addEventListener('submit', function (ev) {
    ev.preventDefault();
    showError('');
    var btn = document.querySelector('[data-bi-submit]');
    btn.disabled = true;
    btn.textContent = 'Submitting…';

    var payload = {};
    FIELDS.forEach(function (f) {
      if (f.type === 'shareholders') return;
      var ctl = document.querySelector('[name="' + f.name + '"]');
      var v = ctl ? ctl.value : '';
      if (f.type === 'number') v = v === '' ? null : Number(v);
      payload[f.name] = v;
    });
    if (SHAREHOLDER_FIELD) {
      var c = document.querySelector('[data-shareholders]');
      payload[SHAREHOLDER_FIELD] = collectShareholders(c);
    }

    var missing = [];
    FIELDS.forEach(function (f) {
      if (!f.required) return;
      var v = payload[f.name];
      if (v == null || v === '' || (Array.isArray(v) && v.length === 0)) missing.push(f.name);
    });
    if (missing.length) {
      showError('Missing required fields: ' + missing.join(', '));
      btn.disabled = false;
      btn.textContent = 'Submit case';
      return;
    }
    if (SHAREHOLDER_FIELD) {
      var sh = payload[SHAREHOLDER_FIELD];
      var sum = sh.reduce(function (a, s) { return a + Number(s.percentage || 0); }, 0);
      if (sh.length > 0 && Math.abs(sum - 100) > 0.01) {
        showError('Shareholder percentages must total 100 (got ' + sum + ').');
        btn.disabled = false;
        btn.textContent = 'Submit case';
        return;
      }
    }

    var body = { payload: payload };
    if (TOKEN) body.token = TOKEN;
    fetch(ENDPOINT + (TOKEN ? '?token=' + encodeURIComponent(TOKEN) : ''), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      credentials: 'same-origin',
      body: JSON.stringify(body),
    }).then(function (r) {
      return r.json().then(function (j) { return { ok: r.ok, body: j }; });
    }).then(function (res) {
      if (!res.ok) throw new Error(res.body && res.body.error ? res.body.error : 'submit_failed');
      document.querySelector('[data-bi-form]').style.display = 'none';
      document.querySelector('[data-bi-thanks]').style.display = 'block';
      try { if (window.parent && window.parent !== window) window.parent.postMessage({ type: 'madarx_intake_submitted', case_id: CASE_ID }, '*'); } catch (e) {}
    }).catch(function (err) {
      showError(String((err && err.message) || err));
      btn.disabled = false;
      btn.textContent = 'Submit case';
    });
  });
})();
`;

  return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>${escapeHtml(coverTitle)}</title>
<style>
  :root { color-scheme: light; --bi-accent: ${escapeHtml(accent)}; --bi-ink: #1a1817; --bi-ink-soft: #5f5a55; --bi-bg: #f7f4ee; --bi-panel: #fff; --bi-line: #e2dccf; }
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; background: var(--bi-bg); color: var(--bi-ink); font: 14.5px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
  .bi-shell { max-width: 760px; margin: 0 auto; padding: 32px 18px 80px; }
  .bi-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; border-radius: 14px; background: var(--bi-panel); border: 1px solid var(--bi-line); margin-bottom: 18px; }
  .bi-header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
  .bi-header img { height: 28px; max-width: 140px; object-fit: contain; }
  .bi-header b { font-weight: 700; font-size: 14px; }
  .bi-header small { color: var(--bi-ink-soft); font-size: 11.5px; letter-spacing: 0.05em; text-transform: uppercase; }
  .bi-cover { border-radius: 16px; background: var(--bi-panel); border: 1px solid var(--bi-line); overflow: hidden; margin-bottom: 18px; }
  .bi-cover-image { width: 100%; aspect-ratio: 3 / 1; background: linear-gradient(135deg, var(--bi-accent) 0%, #2a1310 100%); background-size: cover; background-position: center; }
  .bi-cover-image[data-img] { background-image: var(--bi-cover-img); }
  .bi-cover-body { padding: 22px 24px 24px; }
  .bi-cover-body h1 { margin: 0 0 6px; font-size: 26px; letter-spacing: -0.01em; }
  .bi-cover-body p { margin: 0; color: var(--bi-ink-soft); font-size: 14px; }
  .bi-card { background: var(--bi-panel); border: 1px solid var(--bi-line); border-radius: 16px; padding: 22px 22px 26px; }
  .bi-card h2 { font-size: 17px; margin: 0 0 14px; }
  .bi-field { margin: 0 0 14px; }
  .bi-label { display: block; font-weight: 600; font-size: 13.5px; margin-bottom: 4px; color: var(--bi-ink); }
  .bi-req { color: var(--bi-accent); font-weight: 700; }
  .bi-help { margin: 0 0 6px; color: var(--bi-ink-soft); font-size: 12px; }
  .bi-input { width: 100%; padding: 9px 12px; border: 1px solid var(--bi-line); border-radius: 10px; font: inherit; background: #fff; color: var(--bi-ink); transition: border-color .12s, box-shadow .12s; }
  .bi-input:focus { outline: 0; border-color: var(--bi-accent); box-shadow: 0 0 0 3px rgba(194,69,42,.18); }
  .bi-textarea { resize: vertical; min-height: 90px; }
  .bi-shareholders { border: 1px dashed var(--bi-line); border-radius: 10px; padding: 12px 14px; }
  .bi-shareholders-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
  .bi-shareholders-row { display: grid; grid-template-columns: 24px 1fr 90px 28px; gap: 8px; align-items: center; }
  .bi-shareholders-index { font-size: 11.5px; color: var(--bi-ink-soft); text-align: center; }
  .bi-shareholders-remove { width: 28px; height: 28px; border: 1px solid var(--bi-line); border-radius: 8px; background: #fff; cursor: pointer; font-size: 14px; line-height: 1; color: var(--bi-ink-soft); }
  .bi-shareholders-remove:hover { background: #f5eee2; color: var(--bi-accent); }
  .bi-add-shareholder { border: 0; background: transparent; color: var(--bi-accent); font-weight: 600; cursor: pointer; padding: 4px 0; }
  .bi-shareholders-total { display: flex; justify-content: space-between; padding: 6px 0; border-top: 1px dashed var(--bi-line); margin-top: 6px; font-size: 13px; }
  .bi-shareholders-total .bi-ok { color: #1e4a17; }
  .bi-shareholders-total .bi-bad { color: var(--bi-accent); }
  .bi-shareholders-help { margin: 6px 0 0; color: var(--bi-ink-soft); font-size: 11.5px; }
  .bi-submit-row { margin-top: 22px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
  .bi-submit { background: var(--bi-ink); color: #fff; border: 0; border-radius: 999px; padding: 11px 22px; font: 600 14px/1 inherit; cursor: pointer; transition: transform .08s, background .12s; }
  .bi-submit:hover { background: var(--bi-accent); }
  .bi-submit:active { transform: scale(.97); }
  .bi-submit:disabled { background: var(--bi-ink-soft); cursor: not-allowed; }
  .bi-error { display: none; background: #fff2ef; border: 1px solid #f0c0b6; color: #6e1410; padding: 10px 12px; border-radius: 10px; font-size: 13px; margin-bottom: 14px; }
  .bi-thanks { display: none; padding: 36px 22px; text-align: center; color: var(--bi-ink); }
  .bi-thanks h2 { font-size: 22px; margin: 0 0 8px; }
  .bi-thanks p { color: var(--bi-ink-soft); margin: 0; }
  .bi-footer { margin-top: 22px; color: var(--bi-ink-soft); font-size: 11.5px; text-align: center; }
</style>
</head>
<body>
  <div class="bi-shell">
    <header class="bi-header">
      <div class="bi-header-left">
        ${logoUrl ? `<img src="${escapeHtml(logoUrl)}" alt="${escapeHtml(senderName)}" />` : `<b>${escapeHtml(senderName)}</b>`}
      </div>
      <small>Intake · ${escapeHtml(definition?.name || definition?.slug || 'Process')}</small>
    </header>
    <section class="bi-cover">
      <div class="bi-cover-image"${coverImage ? ` data-img style="--bi-cover-img: url('${escapeHtml(coverImage)}')"` : ''}></div>
      <div class="bi-cover-body">
        <h1>${escapeHtml(coverTitle)}</h1>
        <p>${escapeHtml(coverSubtitle)}</p>
      </div>
    </section>
    <form class="bi-card" data-bi-form>
      <div class="bi-error" data-bi-error role="alert"></div>
      ${fieldsHtml || '<p>No fields configured.</p>'}
      <div class="bi-submit-row">
        <small style="color: var(--bi-ink-soft);">Case <code>${escapeHtml(caseRow?.id || '')}</code></small>
        <button type="submit" class="bi-submit" data-bi-submit>Submit case</button>
      </div>
    </form>
    <div class="bi-card bi-thanks" data-bi-thanks>
      <h2>Thank you</h2>
      <p>We've received your submission. You can close this window.</p>
    </div>
    <p class="bi-footer">Powered by ${escapeHtml(senderName)} · case <code>${escapeHtml(caseRow?.id || '')}</code></p>
  </div>
  <script>${inlineScript}</script>
</body>
</html>`;
}

export function renderIntakeSubmissionHtml({ definition, schema, caseRow, intake }) {
  const fields = Array.isArray(schema?.fields) ? schema.fields : [];
  const fieldsHtml = fields
    .map((f) => {
      const v = intake ? intake[f.name] : undefined;
      return `
        <div class="field">
          <span class="field-label">${escapeHtml(f.label || f.name)}</span>
          <div class="field-value">${renderFieldValueHtml(f, v)}</div>
        </div>`;
    })
    .join('');

  const submittedAt = new Date().toISOString();
  const body = `
    <h1>${escapeHtml(`Intake submitted — ${caseRow?.title || definition?.name || ''}`)}</h1>
    <div class="meta">
      <span><strong>Process:</strong>&nbsp;${escapeHtml(definition?.name || definition?.slug || '')}</span>
      <span><strong>Case:</strong>&nbsp;${escapeHtml(caseRow?.title || '')}</span>
      <span><strong>Submitted:</strong>&nbsp;${escapeHtml(submittedAt)}</span>
      <span class="pill ok">submitted</span>
    </div>
    <div class="note">
      Sensitive identifiers (passport / national IDs) are <strong>redacted</strong> in this artifact.
      Full values are retained only inside the engine's <code>process_cases.intake_payload_json</code> row
      and are subject to the project's encryption + ACL policy.
    </div>
    <h2>Submitted fields</h2>
    ${fieldsHtml || '<p class="muted">No structured fields in this intake.</p>'}
    <div class="art-footer">
      Case <code>${escapeHtml(caseRow?.id || '')}</code> · definition <code>${escapeHtml(definition?.slug || '')}</code>
    </div>
  `;
  return htmlShell({
    title: `Intake submission — ${caseRow?.title || ''}`,
    kicker: 'Intake submission · receipt',
    bodyHtml: body,
  });
}

function outcomePill(value) {
  if (value == null) return '';
  const v = String(value).toLowerCase();
  if (['pass', 'ok', 'auto_pass', 'clear'].includes(v)) return `<span class="pill ok">${escapeHtml(v)}</span>`;
  if (['fail', 'reject', 'rejected', 'hit', 'denied'].includes(v)) return `<span class="pill bad">${escapeHtml(v)}</span>`;
  if (['needs_info', 'manual_review', 'review', 'warning'].includes(v)) return `<span class="pill warn">${escapeHtml(v)}</span>`;
  return `<span class="pill">${escapeHtml(v)}</span>`;
}

function renderCompletenessBody({ output, intake }) {
  const missing = Array.isArray(output?.missing_fields) ? output.missing_fields : [];
  const warnings = Array.isArray(output?.warnings) ? output.warnings : [];
  const company = intake?.proposed_company_name || '';
  return `
    <h1>Completeness analysis${company ? ` — ${escapeHtml(company)}` : ''}</h1>
    <div class="meta">
      <span><strong>Outcome:</strong></span>${outcomePill(output?.outcome)}
      <span><strong>Missing fields:</strong>&nbsp;${missing.length}</span>
      <span><strong>Warnings:</strong>&nbsp;${warnings.length}</span>
    </div>
    <h2>Summary</h2>
    <p>${escapeHtml(output?.summary || 'No summary provided.')}</p>
    ${missing.length ? `<h2>Missing fields</h2><ul>${missing.map((m) => `<li><code>${escapeHtml(m)}</code></li>`).join('')}</ul>` : ''}
    ${warnings.length ? `<h2>Warnings</h2><ul>${warnings.map((w) => `<li>${escapeHtml(w)}</li>`).join('')}</ul>` : ''}
    <h2>Raw output</h2>
    <pre>${escapeHtml(JSON.stringify(output, null, 2))}</pre>
  `;
}

function renderKycRiskBody({ output, intake }) {
  const score = typeof output?.risk_score === 'number' ? output.risk_score : null;
  const action = output?.recommended_action;
  const company = intake?.proposed_company_name || '';
  return `
    <h1>KYC / risk analysis${company ? ` — ${escapeHtml(company)}` : ''}</h1>
    <div class="meta">
      <span><strong>Risk score:</strong>&nbsp;${score != null ? score.toFixed(2) : '—'}</span>
      <span><strong>Recommended action:</strong></span>${outcomePill(action)}
      <span><strong>PEP check:</strong></span>${outcomePill(output?.pep_check || 'clear')}
    </div>
    <div class="note">
      Demo mode — no external KYC services were contacted. Risk score is a deterministic
      function of the intake payload.
    </div>
    <h2>Rationale</h2>
    <p>${escapeHtml(output?.rationale || 'No rationale provided.')}</p>
    ${Array.isArray(output?.flags) && output.flags.length ? `<h2>Flags</h2><ul>${output.flags.map((f) => `<li>${escapeHtml(f)}</li>`).join('')}</ul>` : ''}
    ${Array.isArray(output?.sanctioned_list_hits) && output.sanctioned_list_hits.length ? `<h2>Sanctioned-list hits</h2><ul>${output.sanctioned_list_hits.map((f) => `<li>${escapeHtml(JSON.stringify(f))}</li>`).join('')}</ul>` : ''}
    <h2>Raw output</h2>
    <pre>${escapeHtml(JSON.stringify(output, null, 2))}</pre>
  `;
}

function renderGenericAgentBody({ stage, output }) {
  return `
    <h1>${escapeHtml(stage?.name || 'Agent analysis')}</h1>
    <div class="meta">
      <span><strong>Stage:</strong>&nbsp;${escapeHtml(stage?.slug || '')}</span>
      <span><strong>Outcome:</strong></span>${outcomePill(output?.outcome || 'pass')}
    </div>
    ${output?.summary ? `<h2>Summary</h2><p>${escapeHtml(output.summary)}</p>` : ''}
    <h2>Raw output</h2>
    <pre>${escapeHtml(JSON.stringify(output, null, 2))}</pre>
  `;
}

export function renderAnalysisHtml({ stage, output, intake, agentSlug }) {
  let body;
  if (agentSlug === 'completeness-agent') body = renderCompletenessBody({ output, intake });
  else if (agentSlug === 'kyc-risk-verifier') body = renderKycRiskBody({ output, intake });
  else body = renderGenericAgentBody({ stage, output });

  return htmlShell({
    title: `${stage?.name || 'Analysis'} — ${intake?.proposed_company_name || ''}`,
    kicker: `${stage?.kind?.replace('_', ' ') || 'agent'} · ${stage?.slug || ''}`,
    bodyHtml: body,
  });
}

// ── Persistence ─────────────────────────────────────────────────────────────

function safeCreate(opts) {
  try {
    const result = createArtifact(opts);
    return result?.artifact_id || null;
  } catch {
    // Artifact failure is observation-only — the engine state remains correct.
    return null;
  }
}

export function createIntakeFormArtifact({ definition, schema, caseRow, settings = null, submit_endpoint = null, submit_token = null }) {
  if (!definition || !caseRow) return null;
  const html = renderIntakeFormHtml({ definition, schema, caseRow, settings, submit_endpoint, submit_token });
  const title = `${definition.name || definition.slug || 'Process'} — intake form — ${caseRow.title || caseRow.id.slice(-8)}`;
  return safeCreate({
    title,
    kind: 'html',
    content: html,
    project_id: caseRow.project_id,
    // Dedupe keyed on case so re-rendering with a fresh token (share-link mint)
    // replaces rather than spawns a new artifact.
    dedupe_key: `process_intake_form:${caseRow.id}`,
  });
}

export function createIntakeSubmissionArtifact({ definition, schema, caseRow, intake }) {
  if (!definition || !caseRow) return null;
  const html = renderIntakeSubmissionHtml({ definition, schema, caseRow, intake });
  const title = `${definition.name || definition.slug || 'Process'} — intake submission — ${caseRow.title || caseRow.id.slice(-8)}`;
  return safeCreate({
    title,
    kind: 'html',
    content: html,
    project_id: caseRow.project_id,
    dedupe_key: `process_intake_submission:${caseRow.id}`,
  });
}

export function createAnalysisArtifact({ caseRow, stage, output, agentSlug }) {
  if (!caseRow || !stage) return null;
  const html = renderAnalysisHtml({
    stage,
    output,
    intake: caseRow.intake_payload || {},
    agentSlug,
  });
  const title = `${stage.name || stage.slug} — analysis — ${caseRow.title || caseRow.id.slice(-8)}`;
  return safeCreate({
    title,
    kind: 'html',
    content: html,
    project_id: caseRow.project_id,
    // Per-stage-per-case so retries of the same stage produce distinct artifacts
    // (preserves audit trail). Embedding stage slug avoids per-day collision
    // with other stages of the same case.
    dedupe_key: `process_analysis:${caseRow.id}:${stage.slug}:${Date.now()}`,
  });
}

export const ARTIFACT_DEDUPE_PREFIX = {
  intake_form: 'process_intake_form:',
  intake_submission: 'process_intake_submission:',
  analysis: 'process_analysis:',
};
