// src/api.jsx — V3.5 intake API wiring
// Per INGESTION-API-WIRING-BRIEF.md (2026-05-25):
//  - POST /api/v1/intake/lead   (Q2 capture; fire-and-forget — proceed regardless)
//  - POST /api/v1/intake/order  (checkout reserve; gates confirmation)
//
// No visual/CSS changes implied by this layer.
//
// Headers:
//   Content-Type: application/json
//   Idempotency-Key: <uuid-v4>     // ONE per logical submission. Reuse on network retry.
//   x-turnstile-token: <token>      // when CF Turnstile is on
//
// Response handling:
//   202 → "received" (only success)
//   403 → bad Origin (shouldn't happen prod; log + generic err)
//   409 → key reused with different body → new key + retry
//   422 → validation (don't surface raw detail)
//   429 → rate-limited; small backoff + retry SAME key
//   503 → transient; retry SAME key

const { useState: useStateAPI, useEffect: useEffectAPI, useRef: useRefAPI, useMemo: useMemoAPI } = React;

// ─────────────────────────────────────────────────────────────────────────────
// CONFIG
// ─────────────────────────────────────────────────────────────────────────────
const CONSENT_POLICY_VERSION = '2026-05-24';

function isApiDevHost() {
  if (typeof window === 'undefined') return false;
  const host = window.location.hostname;
  const loopback = [127, 0, 0, 1].join('.');
  return host === 'localhost' || host === loopback || host === '::1';
}

// Production uses the same-origin API. Mock/custom bases are local-dev only.
const API_DEFAULT_BASE = '/api';

// Dev override is in-memory first; production ignores override state entirely.
let _apiBaseSessionOverride = null;

function getApiBase() {
  if (!isApiDevHost()) return API_DEFAULT_BASE;
  // In-memory override (Tweaks panel within the SAME session) wins first.
  if (_apiBaseSessionOverride !== null) return _apiBaseSessionOverride;
  try {
    const v = localStorage.getItem('zrx_api_base');
    // Persist only the same-origin API marker.
    if (v === '/api') {
      return v;
    }
  } catch {}
  return API_DEFAULT_BASE;
}
// Fire-and-forget funnel-event tracker. Reuses getQuizSessionId() so events
// tie back to the same session_id the lead/order POSTs already use: single
// session_id = single attribution chain across the funnel.
//
// Fire-and-forget: NEVER blocks the user flow. Errors are silently swallowed —
// funnel telemetry is best-effort, NOT load-bearing for the user experience.
// Backend route: POST /api/v1/intake/event { event_type, session_id, brand_id?,
// timestamp?, page? } -> 202 { status: "received" }.
async function sendFunnelEvent(eventType, opts) {
  opts = opts || {};
  // session_id MUST be a UUID. getQuizSessionId returns a UUID (string).
  let sessionId = null;
  try {
    sessionId = (typeof getQuizSessionId === 'function') ? getQuizSessionId() : null;
  } catch (_e) {
    sessionId = null;
  }
  if (!sessionId) return;  // no session = no funnel event
  const body = {
    event_type: eventType,
    session_id: sessionId,
    timestamp: new Date().toISOString(),
  };
  if (opts.page) body.page = String(opts.page).slice(0, 200);
  try {
    await postIntake('/v1/intake/event', body);
  } catch (_e) {
    // Fire-and-forget: never propagate funnel telemetry errors to the user flow.
  }
}

function setApiBase(v) {
  if (!isApiDevHost()) {
    _apiBaseSessionOverride = null;
    try { localStorage.removeItem('zrx_api_base'); } catch {}
    return;
  }
  // V3 F9: store in-memory FIRST so the current session honors the value, then mirror
  // to localStorage ONLY if it's one of the persistable allowlisted values. Mock and
  // arbitrary URLs stay in-memory only — never persisted (XSS-amplification immunity).
  _apiBaseSessionOverride = v || null;
  try {
    if (v === '/api') {
      localStorage.setItem('zrx_api_base', v);
    } else {
      // 'mock' / arbitrary URL / null / '' — clear any prior persisted value.
      localStorage.removeItem('zrx_api_base');
    }
  } catch {}
  // notify listeners (e.g. operator cockpit)
  try { window.dispatchEvent(new CustomEvent('zrx:api-base', { detail: v })); } catch {}
}

// ─────────────────────────────────────────────────────────────────────────────
// UUID v4 (crypto-safe; works in modern browsers + Node test envs)
// ─────────────────────────────────────────────────────────────────────────────
function uuidv4() {
  if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
    const bytes = new Uint8Array(16);
    crypto.getRandomValues(bytes);
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'));
    return `${hex[0]}${hex[1]}${hex[2]}${hex[3]}-${hex[4]}${hex[5]}-${hex[6]}${hex[7]}-${hex[8]}${hex[9]}-${hex[10]}${hex[11]}${hex[12]}${hex[13]}${hex[14]}${hex[15]}`;
  }
  throw new Error('Secure random unavailable');
}

// ─────────────────────────────────────────────────────────────────────────────
// UTM parameter capture + persistence
// We grab UTMs from URL on first load, store in sessionStorage, surface to POSTs.
// ─────────────────────────────────────────────────────────────────────────────
const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];

function captureUtmFromUrl() {
  try {
    const params = new URLSearchParams(window.location.search);
    const out = {};
    let any = false;
    for (const k of UTM_KEYS) {
      const v = params.get(k);
      if (v) { out[k] = String(v).slice(0, 200); any = true; }
    }
    if (any) sessionStorage.setItem('zrx_utm', JSON.stringify(out));
  } catch {}
}
function getUtm() {
  try {
    const raw = sessionStorage.getItem('zrx_utm');
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    return Object.keys(parsed).length ? parsed : null;
  } catch { return null; }
}

// ─────────────────────────────────────────────────────────────────────────────
// QUIZ SESSION ID (persists across page reloads inside a session)
// ─────────────────────────────────────────────────────────────────────────────
function getQuizSessionId() {
  try {
    let id = sessionStorage.getItem('zrx_quiz_session_id');
    if (!id) {
      id = uuidv4();
      sessionStorage.setItem('zrx_quiz_session_id', id);
    }
    return id;
  } catch { return uuidv4(); }
}

// ─────────────────────────────────────────────────────────────────────────────
// MOUNT TIMESTAMP — t0_ms for bot defense
// Returns a fn () => msSinceMount so consumers don't snapshot at render time.
// ─────────────────────────────────────────────────────────────────────────────
function useFormMountTs() {
  const mountRef = useRefAPI(Date.now());
  return () => Math.max(0, Date.now() - mountRef.current);
}

// ─────────────────────────────────────────────────────────────────────────────
// HONEYPOT — hidden input. A real user leaves it empty.
// Renders an off-screen + aria-hidden + autocomplete-off text input.
// The caller wires `value` + `onChange` into form state, then passes value to POST.
// ─────────────────────────────────────────────────────────────────────────────
function HoneypotField({ value, onChange, name = 'company_name' }) {
  // Visually + screenreader hidden but still in DOM (so bots that fill all inputs trigger it).
  return (
    <div aria-hidden="true" style={{
      position: 'absolute', left: '-9999px', top: 'auto',
      width: 1, height: 1, overflow: 'hidden', opacity: 0,
    }}>
      <label>
        Do not fill this field
        <input
          type="text"
          name={name}
          tabIndex={-1}
          autoComplete="off"
          value={value}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// CORE POST — handles all status codes per brief
// ─────────────────────────────────────────────────────────────────────────────
async function postIntake(path, body, opts = {}) {
  const base = getApiBase();
  const url  = `${base.replace(/\/+$/, '')}${path}`;
  const idemKey = opts.idempotencyKey || uuidv4();
  const turnstile = opts.turnstileToken || null;

  // Mock branch: in-process synthetic responder.
  if (base === 'mock') {
    return mockPost(path, body, { idemKey, turnstileToken: turnstile });
  }

  const headers = {
    'Content-Type': 'application/json',
    'Idempotency-Key': idemKey,
  };
  if (turnstile) headers['x-turnstile-token'] = turnstile;

  let attempt = 0;
  let currentKey = idemKey;
  const maxRetries = opts.maxRetries ?? 3;

  while (true) {
    attempt += 1;
    let res;
    try {
      res = await fetch(url, {
        method: 'POST',
        credentials: 'same-origin',
        headers: { ...headers, 'Idempotency-Key': currentKey },
        body: JSON.stringify(body),
      });
    } catch (err) {
      if (attempt < maxRetries) {
        await sleep(backoffMs(attempt));
        continue;
      }
      return { ok: false, status: 0, reason: 'network', idemKey: currentKey, error: err?.message || 'network_error' };
    }

    const status = res.status;
    logApi({ path, status, attempt, idemKey: currentKey });

    if (status === 202) {
      return { ok: true, status: 202, idemKey: currentKey };
    }
    if (status === 409) {
      // Key reused with different body — regenerate + retry once.
      if (attempt < maxRetries) { currentKey = uuidv4(); continue; }
      return { ok: false, status, reason: 'idempotency_conflict', idemKey: currentKey };
    }
    if (status === 429 || status === 503) {
      if (attempt < maxRetries) {
        await sleep(backoffMs(attempt));
        continue;
      }
      return { ok: false, status, reason: status === 429 ? 'rate_limited' : 'transient', idemKey: currentKey };
    }
    if (status === 422) {
      return { ok: false, status, reason: 'validation', idemKey: currentKey };
    }
    if (status === 403) {
      return { ok: false, status, reason: 'bad_origin', idemKey: currentKey };
    }
    return { ok: false, status, reason: 'unknown', idemKey: currentKey };
  }
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function backoffMs(attempt) {
  // 600ms, 1500ms, 3500ms with jitter
  const base = [600, 1500, 3500][Math.min(attempt - 1, 2)];
  return base + Math.floor(Math.random() * 400);
}

// ─────────────────────────────────────────────────────────────────────────────
// API CALL LOG — operator dev mode reads from window.__ZRX_API_LOG
// ─────────────────────────────────────────────────────────────────────────────
function logApi(entry) {
  try {
    if (!isApiDevHost()) return;
    if (!window.__ZRX_API_LOG) window.__ZRX_API_LOG = [];
    window.__ZRX_API_LOG.unshift({ ...entry, t: Date.now() });
    window.__ZRX_API_LOG = window.__ZRX_API_LOG.slice(0, 50);
    window.dispatchEvent(new CustomEvent('zrx:api-log', { detail: entry }));
  } catch {}
}

// ─────────────────────────────────────────────────────────────────────────────
// MOCK RESPONDER — emulates the API when base === 'mock'
// Used for prototype walkthroughs before the /api/ deploy lands.
// ─────────────────────────────────────────────────────────────────────────────
const MOCK_STATE = { idempotencyTable: new Map() };

async function mockPost(path, body, { idemKey }) {
  await sleep(420 + Math.floor(Math.random() * 600));

  // Honeypot trip → 422 (silent treatment in UI; brief says don't surface raw detail)
  if (body.hp && body.hp.length > 0) {
    logApi({ path, status: 422, attempt: 1, idemKey, mock: true, reason: 'honeypot' });
    return { ok: false, status: 422, reason: 'validation', idemKey };
  }
  // Minimal field validation
  if (!body.email || !/\S+@\S+\.\S+/.test(body.email)) {
    logApi({ path, status: 422, attempt: 1, idemKey, mock: true, reason: 'bad_email' });
    return { ok: false, status: 422, reason: 'validation', idemKey };
  }
  if (path.includes('/intake/order')) {
    if (!body.full_name || !body.protocol_code || !body.shipping?.line1) {
      logApi({ path, status: 422, attempt: 1, idemKey, mock: true, reason: 'missing_order_field' });
      return { ok: false, status: 422, reason: 'validation', idemKey };
    }
  }
  if (!body.consent_policy_version) {
    logApi({ path, status: 422, attempt: 1, idemKey, mock: true, reason: 'missing_consent' });
    return { ok: false, status: 422, reason: 'validation', idemKey };
  }

  // Idempotency emulation
  const seen = MOCK_STATE.idempotencyTable.get(idemKey);
  if (seen) {
    const sameBody = JSON.stringify(seen) === JSON.stringify(body);
    if (!sameBody) {
      logApi({ path, status: 409, attempt: 1, idemKey, mock: true });
      return { ok: false, status: 409, reason: 'idempotency_conflict', idemKey };
    }
  } else {
    MOCK_STATE.idempotencyTable.set(idemKey, body);
  }

  // Inject a 1-in-30 transient for dev flake-testing (operator-controlled via tweak)
  if (isApiDevHost() && window.__ZRX_FORCE_TRANSIENT) {
    logApi({ path, status: 503, attempt: 1, idemKey, mock: true });
    return { ok: false, status: 503, reason: 'transient', idemKey };
  }

  logApi({ path, status: 202, attempt: 1, idemKey, mock: true });
  return { ok: true, status: 202, idemKey, mock: true };
}

// ─────────────────────────────────────────────────────────────────────────────
// HIGH-LEVEL HELPERS — call these from screens
// ─────────────────────────────────────────────────────────────────────────────

// Q2 lead capture (fire-and-forget; UX should proceed regardless)
async function submitLead({ email, firstName, cohortKey, hp = '', t0_ms, consent_marketing, consent_data_use, idempotencyKey }) {
  const body = {
    email: String(email || '').trim().slice(0, 254),
    consent_policy_version: CONSENT_POLICY_VERSION,
    consent_marketing: !!consent_marketing,
    consent_data_use: !!consent_data_use,
    hp: hp || '',
  };
  if (firstName) body.first_name = String(firstName).slice(0, 100);
  if (cohortKey) body.cohort_key = String(cohortKey).slice(0, 64);
  body.quiz_session_id = getQuizSessionId();
  if (typeof t0_ms === 'number' && t0_ms >= 0) body.t0_ms = t0_ms;
  const utm = getUtm();
  if (utm) body.utm = utm;
  return postIntake('/v1/intake/lead', body, { idempotencyKey });
}

// Q11 clinical-screening disposition (fire-and-forget; UX should proceed regardless)
async function submitClinicalDisposition(conditions) {
  const rawConditions = Array.isArray(conditions) && conditions.length ? conditions : ['none'];
  const body = {
    quiz_session_id: getQuizSessionId(),
    conditions: rawConditions,
  };
  try {
    return await postIntake('/v1/intake/clinical-disposition', body, { idempotencyKey: uuidv4() });
  } catch (_e) {
    return { ok: false, status: 0, reason: 'swallowed' };
  }
}

// Checkout "reserve" - gates the confirmation transition
async function submitOrder({ email, fullName, phone, shipping, protocolCode, cohortKey,
                             cardLast4, cardBrand, cardZip, conditions, idempotencyKey }) {
  // cont.12 Phase A1 (P8 PCI-safe pivot — operator-ratified 2026-05-27):
  //   phone REQUIRED (operator anchor: "get phone there as well")
  //   card_last4 + card_brand + card_zip OPTIONAL PCI-safe partial (NEVER full PAN/CVC)
  //   Backend rejects raw PAN/CVC via extra='forbid' on ReservedOrderV1 schema.
  const body = {
    email: String(email || '').trim().slice(0, 254),
    full_name: String(fullName || '').trim().slice(0, 120),
    phone: String(phone || '').trim().slice(0, 20),
    shipping: {
      line1: String(shipping?.line1 || '').slice(0, 120),
      line2: shipping?.line2 ? String(shipping.line2).slice(0, 120) : null,
      city: String(shipping?.city || '').slice(0, 80),
      region: String(shipping?.region || '').slice(0, 32),
      postal: String(shipping?.postal || '').slice(0, 16),
      country: String(shipping?.country || 'US').slice(0, 2).toUpperCase(),
    },
    protocol_code: String(protocolCode || '').toUpperCase().slice(0, 32),
    consent_policy_version: CONSENT_POLICY_VERSION,
  };
  if (cohortKey) body.cohort_key = String(cohortKey).slice(0, 64);
  if (Array.isArray(conditions) && conditions.length) body.conditions = conditions;
  // PCI-safe card partial — only attach if all required pieces are present + last4 is exactly 4 digits.
  if (cardLast4 && /^\d{4}$/.test(cardLast4)) {
    body.card_last4 = cardLast4;
    if (cardBrand) body.card_brand = String(cardBrand).toLowerCase().slice(0, 20);
    if (cardZip) body.card_zip = String(cardZip).slice(0, 12);
  }
  return postIntake('/v1/intake/order', body, { idempotencyKey });
}

// Capture UTMs at first load (caller invokes once at app mount).
captureUtmFromUrl();

Object.assign(window, {
  CONSENT_POLICY_VERSION,
  getApiBase, setApiBase,
  uuidv4, useFormMountTs, HoneypotField,
  submitLead, submitClinicalDisposition, submitOrder, getQuizSessionId,
  // cont.11 Phase 1E (V2 F2): public sendFunnelEvent helper for landing/quiz/checkout
  // fire-and-forget telemetry. Backend route POST /api/v1/intake/event.
  sendFunnelEvent,
});
