/* aiso-api.jsx — single client for the AISO LLM proxy.
 *
 * Mode is driven by `window.AISO_API_BASE`:
 *   - Set (e.g. "https://api.aiso.app" or "/api"): MANAGED mode.
 *     Every LLM call routes through your server, which holds the real
 *     provider keys in env vars. The user never sees a "wire keys" panel.
 *   - Unset: LEGACY / BYO mode. Calls go direct to provider APIs from the
 *     browser using user-pasted keys (the current prototype behaviour).
 *
 * Set it in AISO.html before the React app boots:
 *   <script>window.AISO_API_BASE = "https://api.aiso.app";</script>
 *
 * Endpoint contract (your server implements this):
 *
 *   POST  {AISO_API_BASE}/llm
 *   Auth: Bearer <supabase_jwt>   (omitted when user not signed in;
 *                                  server may still allow free-tier limits)
 *   Body: { provider: "anthropic" | "openai" | "google" | "perplexity" | "xai" | "deepseek",
 *           prompt: string,
 *           maxTokens?: number,
 *           system?: string }
 *   200:  { text, model, providerId, ms, requestId, mentioned?: boolean,
 *           quotaRemaining?: number, quotaResetAt?: number }
 *   402:  { error: "quota_exceeded", quotaResetAt, plan }
 *   401:  { error: "unauthenticated" }
 *   429:  { error: "rate_limited", retryAt }
 *   5xx:  { error: "upstream_failed", providerError }
 *
 *   GET   {AISO_API_BASE}/usage          → { used, limit, resetAt, plan }
 *
 * The server is also responsible for:
 *   - Caching identical (domain, prompt) pairs across users
 *   - Streaming responses (we use SSE for the query inspector)
 *   - Rotating provider keys / fallback chains
 *   - Logging + abuse signals
 */

const DEFAULT_TIMEOUT_MS = 45_000;

function aisoEnabled() {
  return typeof window !== 'undefined' && !!window.AISO_API_BASE;
}

function getAuthHeader() {
  // Supabase session if available
  try {
    const supa = window.supaLoad ? window.supaLoad() : null;
    const token = supa?.session?.access_token;
    if (token) return { 'Authorization': `Bearer ${token}` };
  } catch {}
  return {};
}

function withTimeout(promise, ms = DEFAULT_TIMEOUT_MS) {
  return Promise.race([
    promise,
    new Promise((_, rj) => setTimeout(() => rj(new Error(`AISO API timeout after ${ms}ms`)), ms)),
  ]);
}

/* Primary entry point: route an LLM call through the AISO server. */
async function aisoCall(provider, prompt, opts = {}) {
  if (!aisoEnabled()) {
    // Sandbox fallback: use the built-in Claude helper if the caller asked for anthropic,
    // otherwise this is a usage error in the new architecture.
    if (provider === 'anthropic' && typeof window.claude?.complete === 'function') {
      const t0 = performance.now();
      const text = await withTimeout(window.claude.complete(opts.system ? `${opts.system}\n\n${prompt}` : prompt));
      return {
        text,
        model: 'claude-haiku-4-5',
        providerId: 'anthropic',
        ms: Math.round(performance.now() - t0),
        via: 'sandbox-helper',
      };
    }
    throw new Error('AISO server is not configured (window.AISO_API_BASE unset) and no sandbox helper available for ' + provider);
  }

  const base = window.AISO_API_BASE.replace(/\/$/, '');
  const url = `${base}/llm`;
  /* Primary entry point: route an LLM call through the AISO server.
   * If the server is unreachable (404/network — e.g. running the static file
   * with no /api functions), gracefully fall back to window.claude.complete
   * for Anthropic so the sandbox prototype keeps working.
   */
  const body = {
    provider,
    prompt,
    ...(opts.maxTokens ? { maxTokens: opts.maxTokens } : {}),
    ...(opts.system ? { system: opts.system } : {}),
    ...(opts.brandDomain ? { brandDomain: opts.brandDomain } : {}), // helps server cache by domain
    ...(opts.model ? { model: opts.model } : {}), // allow a per-call model override (e.g. haiku for speed)
  };

  const t0 = performance.now();
  let r;
  try {
    r = await withTimeout(fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
      body: JSON.stringify(body),
      signal: opts.signal,
    }), opts.timeoutMs);
  } catch (e) {
    // Network error reaching the proxy — fall back to sandbox helper for Anthropic
    if (provider === 'anthropic' && typeof window.claude?.complete === 'function') {
      const t1 = performance.now();
      const text = await withTimeout(window.claude.complete(opts.system ? `${opts.system}\n\n${prompt}` : prompt));
      return { text, model: 'claude-haiku-4-5', providerId: 'anthropic', ms: Math.round(performance.now() - t1), via: 'sandbox-fallback' };
    }
    throw e;
  }

  // 404 = endpoint missing (e.g. static-only deploy); same fallback
  if (r.status === 404 && provider === 'anthropic' && typeof window.claude?.complete === 'function') {
    const t1 = performance.now();
    const text = await withTimeout(window.claude.complete(opts.system ? `${opts.system}\n\n${prompt}` : prompt));
    return { text, model: 'claude-haiku-4-5', providerId: 'anthropic', ms: Math.round(performance.now() - t1), via: 'sandbox-fallback' };
  }

  if (!r.ok) {
    let payload = null;
    try { payload = await r.json(); } catch {}
    if (r.status === 402) {
      const err = new Error('AISO quota exceeded for this plan');
      err.code = 'quota_exceeded';
      err.quotaResetAt = payload?.quotaResetAt;
      throw err;
    }
    if (r.status === 401) {
      const err = new Error('Sign in to use AISO');
      err.code = 'unauthenticated';
      throw err;
    }
    if (r.status === 429) {
      const err = new Error('Rate limited — try again shortly');
      err.code = 'rate_limited';
      err.retryAt = payload?.retryAt;
      throw err;
    }
    throw new Error(payload?.error || payload?.message || `AISO API HTTP ${r.status}`);
  }

  const data = await r.json();
  return {
    text: data.text,
    model: data.model,
    providerId: data.providerId || provider,
    ms: Math.round(performance.now() - t0),
    requestId: data.requestId,
    quotaRemaining: data.quotaRemaining,
    quotaResetAt: data.quotaResetAt,
    via: 'aiso-server',
  };
}

/* Plan & usage snapshot. Returns null if managed mode is off or call fails. */
async function aisoUsage() {
  if (!aisoEnabled()) return null;
  try {
    const base = window.AISO_API_BASE.replace(/\/$/, '');
    const r = await withTimeout(fetch(`${base}/usage`, {
      headers: getAuthHeader(),
    }), 8000);
    if (!r.ok) return null;
    return await r.json();
  } catch { return null; }
}

/* Are we in managed mode AND does the user (or anon session) have access? */
async function aisoHealth() {
  if (!aisoEnabled()) return { ok: false, mode: 'byo' };
  try {
    const base = window.AISO_API_BASE.replace(/\/$/, '');
    const r = await withTimeout(fetch(`${base}/health`, { headers: getAuthHeader() }), 5000);
    if (!r.ok) return { ok: false, mode: 'managed', status: r.status };
    const data = await r.json().catch(() => ({}));
    return { ok: true, mode: 'managed', ...data };
  } catch (e) {
    return { ok: false, mode: 'managed', error: e.message };
  }
}

/* ---------- managed-provider cache ----------
 * Populated by aisoBootstrap(). Components subscribe to the 'aiso:managed-ready'
 * event to re-render when the health check resolves.
 */
window.__aisoManaged = window.__aisoManaged || { ready: false, providers: new Set() };

async function aisoBootstrap() {
  if (!aisoEnabled()) {
    window.__aisoManaged = { ready: true, providers: new Set() };
    window.dispatchEvent(new CustomEvent('aiso:managed-ready'));
    return window.__aisoManaged;
  }
  const h = await aisoHealth();
  const providers = new Set();
  if (h?.ok && h?.providers) {
    for (const [id, on] of Object.entries(h.providers)) if (on) providers.add(id);
  }
  window.__aisoManaged = { ready: true, providers, mode: h?.mode || 'managed', error: h?.error };
  window.dispatchEvent(new CustomEvent('aiso:managed-ready', { detail: window.__aisoManaged }));
  return window.__aisoManaged;
}

function getManagedProviders() {
  return window.__aisoManaged?.providers || new Set();
}

/* Union of BYO keys + server-managed providers — the canonical "is this wired" check */
function getWiredProviders(profile) {
  const out = [];
  const managed = getManagedProviders();
  for (const p of (window.PROVIDERS || [])) {
    if (profile?.keys?.[p.id] || managed.has(p.id)) out.push(p);
  }
  return out;
}

function isProviderWired(profile, providerId) {
  return !!profile?.keys?.[providerId] || getManagedProviders().has(providerId);
}

// Kick the bootstrap off ASAP
if (typeof window !== 'undefined') {
  aisoBootstrap().catch(() => {});
}

Object.assign(window, {
  aisoCall,
  aisoUsage,
  aisoHealth,
  aisoEnabled,
  aisoBootstrap,
  getManagedProviders,
  getWiredProviders,
  isProviderWired,
});

/* React hook: returns the current set of wired providers (BYO ∪ managed),
 * subscribes to the bootstrap event so components re-render once the health
 * check completes. Safe to call even before React is available — it's a noop.
 */
if (typeof window !== 'undefined' && typeof React !== 'undefined') {
  window.useWiredProviders = function useWiredProviders(profile) {
    const [tick, setTick] = React.useState(0);
    React.useEffect(() => {
      const on = () => setTick(t => t + 1);
      window.addEventListener('aiso:managed-ready', on);
      return () => window.removeEventListener('aiso:managed-ready', on);
    }, []);
    return React.useMemo(() => getWiredProviders(profile), [profile, tick]);
  };
}
