// DAEV API client — added in Slice 6.4.5 wiring brief.
// Slice 6.4.8 Phase 2 (F-213): adds anonymous-token flow so the demo UI
// runs end-to-end on a fresh browser without an API key.
//
// Exposes window.DAEV_API for fixture hydration + live audits.
//
// Auth model (per Probe Pack A + slice 6.4.8 anon extension):
//   /audits, /audits/{id}      → demo-token JWT (master-key holders only).
//                                 Anon callers see 401 here — anon iss=daev-anon
//                                 is rejected by require_scopes which pins
//                                 iss=daev-api (deferred to a later slice).
//   /switzerland/gate          → master X-DAEV-API-Key OR anon Bearer JWT
//   /audits/from-github        → master X-DAEV-API-Key OR anon Bearer JWT
//   /demo/warm                 → master X-DAEV-API-Key OR anon Bearer JWT
//   /council/graph-stream/{id} → API key (no JWT needed; query-param fallback for EventSource)
//   /auth/demo-token           → API key (mints 120 s JWT)
//   /auth/anon-token           → no auth (mints 600 s anon JWT keyed by IP-derived tenant)
//
// Resolution order in ensureAuth(): master key wins if set; otherwise mint
// (or replay) an anon token. Master-key holders are not subject to the
// per-tenant 10/hour cap; anon callers are.
//
// All errors logged with `[daev-api]` prefix. No secrets in logs.

(function() {
  const API_BASE = 'https://daev-api.fly.dev/api/v1';

  // ─── Storage helpers ───────────────────────────────────────────
  function getApiKey() {
    // Priority: query param → localStorage → sessionStorage
    try {
      const p = new URLSearchParams(window.location.search);
      const fromUrl = p.get('key');
      if (fromUrl) {
        try { localStorage.setItem('daev.api_key', fromUrl); } catch(_){}
        return fromUrl;
      }
      return localStorage.getItem('daev.api_key') || sessionStorage.getItem('daev.api_key') || null;
    } catch(e) {
      return null;
    }
  }

  function setApiKey(key) {
    try { localStorage.setItem('daev.api_key', key); } catch(_){}
  }

  // ─── JWT cache (sessionStorage; refresh on expiry) ─────────────
  function decodeJwt(token) {
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      return payload;
    } catch(e) { return null; }
  }

  function isJwtExpired(token, leewaySeconds = 30) {
    const claims = decodeJwt(token);
    if (!claims || !claims.exp) return true;
    return (Date.now() / 1000) > (claims.exp - leewaySeconds);
  }

  async function mintDemoToken() {
    const key = getApiKey();
    if (!key) throw new Error('[daev-api] No API key set; cannot mint demo token');
    const r = await fetch(`${API_BASE}/auth/demo-token`, {
      method: 'POST',
      headers: { 'X-DAEV-API-Key': key },
    });
    if (!r.ok) {
      const body = await r.text().catch(() => '');
      throw new Error(`[daev-api] mintDemoToken failed: ${r.status} ${body.slice(0, 200)}`);
    }
    const data = await r.json();
    const token = data.token || data.access_token || data.jwt;
    if (!token) throw new Error('[daev-api] mintDemoToken: no token in response');
    try { sessionStorage.setItem('daev.jwt', token); } catch(_){}
    return token;
  }

  async function getCachedJwt() {
    let token = null;
    try { token = sessionStorage.getItem('daev.jwt'); } catch(_){}
    if (!token || isJwtExpired(token)) {
      token = await mintDemoToken();
    }
    return token;
  }

  // ─── Anon token (Slice 6.4.8 Phase 2 / F-213) ──────────────────
  // The mint endpoint is unauthenticated and the response body carries the
  // JWT. Cache in localStorage so multiple tabs share the same anon identity
  // (the backend derives tenant_id from the salted client IP, so all tabs
  // from the same browser already collapse to the same tenant — caching
  // just avoids the per-tab mint cost). 30 s expiry buffer mirrors the
  // demo-token cache so we never present a token that's about to expire.
  const ANON_TOKEN_KEY = 'daev.anon_token';
  const ANON_TOKEN_EXP_KEY = 'daev.anon_token_exp';

  async function mintAnonToken() {
    const r = await fetch(`${API_BASE}/auth/anon-token`, { method: 'POST' });
    if (!r.ok) {
      const body = await r.text().catch(() => '');
      throw new Error(`[daev-api] mintAnonToken failed: ${r.status} ${body.slice(0, 200)}`);
    }
    const data = await r.json();
    const token = data.token;
    if (!token) throw new Error('[daev-api] mintAnonToken: no token in response');
    const expSec = Math.floor(Date.now() / 1000) + (data.expires_in || 600);
    try {
      localStorage.setItem(ANON_TOKEN_KEY, token);
      localStorage.setItem(ANON_TOKEN_EXP_KEY, String(expSec));
    } catch(_){}
    return token;
  }

  async function getCachedAnonToken() {
    let token = null;
    let expSec = 0;
    try {
      token = localStorage.getItem(ANON_TOKEN_KEY);
      expSec = parseInt(localStorage.getItem(ANON_TOKEN_EXP_KEY) || '0', 10);
    } catch(_){}
    // Re-mint if missing, signature-expired, or within 30 s of expiry.
    const nowSec = Date.now() / 1000;
    if (!token || isJwtExpired(token) || expSec <= nowSec + 30) {
      token = await mintAnonToken();
    }
    return token;
  }

  // ensureAuth resolves the auth context for a request that accepts either
  // master X-DAEV-API-Key OR anon Bearer JWT (notably /switzerland/gate,
  // /audits/from-github, /demo/warm). Master key wins when present.
  async function ensureAuth() {
    const masterKey = getApiKey();
    if (masterKey) return { mode: 'master', key: masterKey };
    const token = await getCachedAnonToken();
    return { mode: 'anon', token: token };
  }

  // Surface for the DemoModeBanner — sync-style snapshot. Returns 'master'
  // when an API key is set in URL/localStorage/sessionStorage, else 'anon'.
  // This deliberately does NOT trigger a mint: the banner just needs to know
  // whether to render, and the actual mint will happen on the next request.
  function getAuthModeSync() {
    return getApiKey() ? 'master' : 'anon';
  }

  // ─── Fetch wrappers ────────────────────────────────────────────
  async function authedFetchJWT(path, init = {}) {
    const token = await getCachedJwt();
    const headers = Object.assign({}, init.headers || {}, {
      'Authorization': `Bearer ${token}`,
    });
    const r = await fetch(`${API_BASE}${path}`, Object.assign({}, init, { headers }));
    if (!r.ok) {
      // If 401, try one re-mint
      if (r.status === 401) {
        try { sessionStorage.removeItem('daev.jwt'); } catch(_){}
        const fresh = await mintDemoToken();
        const headers2 = Object.assign({}, init.headers || {}, {
          'Authorization': `Bearer ${fresh}`,
        });
        const r2 = await fetch(`${API_BASE}${path}`, Object.assign({}, init, { headers: headers2 }));
        if (!r2.ok) throw new Error(`[daev-api] ${path} retry-failed: ${r2.status}`);
        return r2.json();
      }
      const body = await r.text().catch(() => '');
      throw new Error(`[daev-api] ${path} failed: ${r.status} ${body.slice(0, 200)}`);
    }
    return r.json();
  }

  // Slice 6.4.8 Phase 2 (F-213): authedFetchKEY accepts either path. Master
  // key wins when set; otherwise an anon Bearer is minted (or replayed
  // from localStorage cache). On 401 in anon mode we drop the cached
  // token and retry once — covers a token that expired in flight or a
  // backend salt rotation that invalidated all anon identities.
  async function authedFetchKEY(path, init = {}) {
    const auth = await ensureAuth();
    const headers = Object.assign({}, init.headers || {});
    if (auth.mode === 'master') {
      headers['X-DAEV-API-Key'] = auth.key;
    } else {
      headers['Authorization'] = `Bearer ${auth.token}`;
    }
    const r = await fetch(`${API_BASE}${path}`, Object.assign({}, init, { headers }));
    if (!r.ok) {
      if (r.status === 401 && auth.mode === 'anon') {
        try {
          localStorage.removeItem(ANON_TOKEN_KEY);
          localStorage.removeItem(ANON_TOKEN_EXP_KEY);
        } catch(_){}
        const fresh = await mintAnonToken();
        const headers2 = Object.assign({}, init.headers || {}, {
          'Authorization': `Bearer ${fresh}`,
        });
        const r2 = await fetch(`${API_BASE}${path}`, Object.assign({}, init, { headers: headers2 }));
        if (!r2.ok) {
          const body = await r2.text().catch(() => '');
          throw new Error(`[daev-api] ${path} retry-failed: ${r2.status} ${body.slice(0, 200)}`);
        }
        return r2.json();
      }
      const body = await r.text().catch(() => '');
      throw new Error(`[daev-api] ${path} failed: ${r.status} ${body.slice(0, 200)}`);
    }
    return r.json();
  }

  // ─── Public API surface ────────────────────────────────────────
  async function warmDemo() {
    return authedFetchKEY('/demo/warm', { method: 'POST' });
  }

  async function fetchAudits(cursor = null, limit = 20) {
    const qs = new URLSearchParams();
    qs.set('limit', String(limit));
    if (cursor) qs.set('cursor', cursor);
    return authedFetchJWT(`/audits?${qs.toString()}`);
  }

  async function fetchAuditDetail(audit_id) {
    if (!audit_id) throw new Error('[daev-api] fetchAuditDetail: audit_id required');
    return authedFetchJWT(`/audits/${audit_id}`);
  }

  async function fetchAuditSource(audit_id) {
    if (!audit_id) throw new Error('[daev-api] fetchAuditSource: audit_id required');
    try {
      return await authedFetchJWT(`/audits/${audit_id}/source`);
    } catch(e) {
      // Source endpoint may 404 for audits without retained source — tolerate
      if (e.message && e.message.includes('404')) return null;
      throw e;
    }
  }

  // Slice 6.4.5.2 Phase 10 (BUG-78): per-tool SAST output.
  // Always returns 200 — legacy audits get {available: false}.
  async function fetchAuditSast(audit_id) {
    if (!audit_id) throw new Error('[daev-api] fetchAuditSast: audit_id required');
    try {
      return await authedFetchJWT(`/audits/${audit_id}/sast`);
    } catch(e) {
      if (e.message && e.message.includes('404')) return null;
      throw e;
    }
  }

  async function submitInlineAudit(source_code, language = 'python', mode = 'fast') {
    return authedFetchKEY('/switzerland/gate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ source_code, language, mode }),
    });
  }

  async function submitGithubAudit(repo_url, token = null) {
    const body = { repo_url };
    if (token) body.token = token;
    return authedFetchKEY('/audits/from-github', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
  }

  // Polls until report.findings is populated OR timeout.
  // Yields intermediate status via onProgress callback.
  //
  // Slice 6.4.5.3 Phase 6 (Item 8) hardening per Q1 amendment:
  //   * Default timeoutMs bumped 180_000 → 300_000 (bandit-sized repos can
  //     take 70-90s queue + analysis on cold-start machines)
  //   * On timeout, RETURNS { timedOut: true, audit_id, elapsed } instead
  //     of throwing — caller must check the marker and degrade gracefully.
  //     Throwing was breaking the UI by leaving LiveBuild in a stuck state.
  //   * Heuristic for "ready" is now also satisfied by a populated
  //     report.metadata, so SAFE-verdict audits (no findings array entries)
  //     don't poll forever.
  //   * 401 mid-poll is already handled transparently by authedFetchJWT
  //     (mints fresh JWT and retries once).
  async function pollAuditUntilReady(audit_id, opts = {}) {
    const { intervalMs = 5000, timeoutMs = 300000, onProgress = null } = opts;
    const startedAt = Date.now();
    while (Date.now() - startedAt < timeoutMs) {
      try {
        const detail = await fetchAuditDetail(audit_id);
        const report = detail && detail.report ? detail.report : null;
        const findingsLoaded = report && Array.isArray(report.findings);
        const metadataPopulated = report && report.metadata && Object.keys(report.metadata).length > 0;
        const totalNodes = (report && typeof report.total_nodes_analyzed === 'number') ? report.total_nodes_analyzed : 0;
        // Audit is "ready" when the worker has populated metadata AND either
        // findings exist or total_nodes > 0 (handles SAFE-verdict github audits).
        const ready = findingsLoaded && (metadataPopulated || totalNodes > 0);
        if (onProgress) onProgress({
          ready,
          elapsed: Date.now() - startedAt,
          total_nodes: totalNodes,
          finding_count: (report && Array.isArray(report.findings)) ? report.findings.length : 0,
          detail,
        });
        if (ready) return detail;
      } catch(e) {
        if (onProgress) onProgress({ error: String(e), elapsed: Date.now() - startedAt });
      }
      await new Promise(r => setTimeout(r, intervalMs));
    }
    // Slice 6.4.5.3 Q1 amendment: do not throw on timeout. Caller renders
    // the queue receipt + "still processing" placeholder so the UI doesn't
    // freeze in the LiveBuild screen.
    console.warn(`[daev-api] pollAuditUntilReady timeout after ${timeoutMs}ms for ${audit_id}`);
    return { timedOut: true, audit_id, elapsed: Date.now() - startedAt };
  }

  function streamGraphBuild(audit_id, onEvent, onError) {
    const key = getApiKey();
    if (!key) {
      if (onError) onError(new Error('No API key'));
      return null;
    }
    // EventSource cannot set headers; pass key as query param (backend supports per Probe A6).
    const url = `${API_BASE}/council/graph-stream/${audit_id}?api_key=${encodeURIComponent(key)}`;
    const es = new EventSource(url);
    es.addEventListener('node_discovered', (ev) => {
      try { onEvent && onEvent({ type: 'node_discovered', data: JSON.parse(ev.data) }); } catch(_){}
    });
    es.addEventListener('edge_discovered', (ev) => {
      try { onEvent && onEvent({ type: 'edge_discovered', data: JSON.parse(ev.data) }); } catch(_){}
    });
    es.addEventListener('done', (ev) => {
      try { onEvent && onEvent({ type: 'done', data: JSON.parse(ev.data || '{}') }); } catch(_){}
      es.close();
    });
    es.onerror = (err) => {
      if (onError) onError(err);
      es.close();
    };
    return es;
  }

  // ─── Expose ────────────────────────────────────────────────────
  window.DAEV_API = {
    API_BASE,
    getApiKey, setApiKey,
    mintDemoToken, getCachedJwt,
    // Slice 6.4.8 Phase 2 (F-213): anon-flow surface.
    mintAnonToken, getCachedAnonToken, ensureAuth, getAuthMode: getAuthModeSync,
    warmDemo,
    fetchAudits, fetchAuditDetail, fetchAuditSource, fetchAuditSast,
    submitInlineAudit, submitGithubAudit, pollAuditUntilReady,
    streamGraphBuild,
  };

  console.log('[daev-api] client module loaded');
})();
