/* api.jsx — auth state + fetch helpers.
   Lives at the top of the script load order so every screen has access.
   Also exposes React hooks once at the global level so individual
   screen files can use useState/useEffect/etc. without re-declaring
   them (Babel transpiles every text/babel script into shared scope,
   so `const { useState } = React` collides if declared more than once). */

const { useState, useEffect, useMemo, useCallback, useRef, Fragment } = React;

const TOKEN_KEY = 'aquaos.token';
const USER_KEY = 'aquaos.user';

/* Role catalogue mirrored from src/middleware/auth.js + each route's
   authorize() call. Backend is the source of truth; this just lets the
   UI hide buttons that would 403 anyway. */
const ROLES = {
  AQUAOS_ADMIN:    'aquaos_admin',     /* Platform admin — sees every org */
  ORG_ADMIN:       'org_admin',        /* Tenant admin — full org access */
  SITE_MANAGER:    'site_manager',     /* Manages a single site */
  MARKETING_ADMIN: 'marketing_admin',  /* Cross-site campaigns + assets */
  DESIGNER:        'designer',         /* Asset upload + AI copy gen */
  OPERATOR:        'operator',         /* Read-only + AI field regen */
  /* Legacy 'admin' role kept for backward compat with old tokens. */
  LEGACY_ADMIN:    'admin',
};

const Auth = {
  getToken() { return localStorage.getItem(TOKEN_KEY); },
  getUser() {
    const raw = localStorage.getItem(USER_KEY);
    try { return raw ? JSON.parse(raw) : null; } catch (_) { return null; }
  },
  setSession(token, user) {
    localStorage.setItem(TOKEN_KEY, token);
    localStorage.setItem(USER_KEY, JSON.stringify(user));
  },
  clear() {
    localStorage.removeItem(TOKEN_KEY);
    localStorage.removeItem(USER_KEY);
    // Clear any leftover impersonation stash too. Bug feedback May 2026
    // (Oli): after logging out + back in, the avatar dropdown showed
    // "Session expired" because `aquaos.original_session` survived the
    // sign-out and the sidebar's role-switcher fetch was preferring
    // the stale stashed token (issued under a previous JWT secret /
    // long-since expired) over the freshly-issued cookie/token.
    // ORIGINAL only has meaning while actively mid-impersonation;
    // there's no scenario where it should outlive a sign-out.
    try { localStorage.removeItem('aquaos.original_session'); } catch (_) {}
  },

  /* Role predicates — each one mirrors a backend authorize() invocation.
     Use these to hide UI that the user can't act on. */
  hasRole(...roles) {
    const u = this.getUser();
    if (!u) return false;
    return roles.includes(u.role);
  },
  isAquaOSAdmin()    { return this.hasRole(ROLES.AQUAOS_ADMIN); },
  isOrgAdmin()       { return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN); },
  isSiteManager()    { return this.hasRole(ROLES.SITE_MANAGER); },
  isMarketingAdmin() { return this.hasRole(ROLES.MARKETING_ADMIN, ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN); },
  isDesigner()       { return this.hasRole(ROLES.DESIGNER, ROLES.MARKETING_ADMIN, ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN); },
  isOperator()       { return this.hasRole(ROLES.OPERATOR); },

  /* Capability predicates — group roles by what they can actually do.
     Prefer these in JSX gating: clearer intent and easier to keep in
     sync with backend authorize() calls. */
  canManageUsers() {
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN, ROLES.SITE_MANAGER, ROLES.LEGACY_ADMIN);
  },
  canManageOrg() {
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN);
  },
  canApprove() {
    /* Approving campaigns / species edits is reviewer-tier.
       v82 (2026-05-19): site_manager dropped — they publish their
       own site's campaigns directly (campaigns.publish), they don't
       review anyone else's. Keeping them in this list made the UI
       show approve buttons that 403'd at the backend. */
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN, ROLES.MARKETING_ADMIN);
  },
  canEditCampaigns() {
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN, ROLES.MARKETING_ADMIN, ROLES.SITE_MANAGER, ROLES.DESIGNER);
  },
  canEditSpecies() {
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN, ROLES.MARKETING_ADMIN, ROLES.SITE_MANAGER, ROLES.DESIGNER);
  },
  canUploadAssets() {
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN, ROLES.MARKETING_ADMIN, ROLES.SITE_MANAGER, ROLES.DESIGNER);
  },
  canSeeBilling() {
    return this.hasRole(ROLES.ORG_ADMIN, ROLES.AQUAOS_ADMIN);
  },
  canSeePlatform() {
    /* aquaos_admin-only surfaces (orgs grid, fleet-wide analytics) */
    return this.hasRole(ROLES.AQUAOS_ADMIN);
  },

  /* ── Capability checks (perms v2) ───────────────────────────────────
     Atomic capability lookup. The user object embeds a `capabilities[]`
     array when the backend uses fine-grained perms; if the array is
     absent we fall back to the role-default mapping baked in below
     (mirrors src/capabilities.js ROLE_TO_CAPS). aquaos_admin always
     wins. Use these for any feature gating that an admin might want
     to override per user — e.g. let one designer see analytics. */
  hasCapability(capId) {
    const u = this.getUser();
    if (!u) return false;
    if (u.role === ROLES.AQUAOS_ADMIN) return true;
    const caps = u.capabilities;
    if (Array.isArray(caps) && caps.length > 0) return caps.includes(capId);
    /* Role-default fallback. Subset of src/capabilities.js — only the
       analytics caps are mirrored client-side since that's the only
       surface that asks. Extend per-feature as you need it. */
    const ROLE_DEFAULTS = {
      // v80 (2026-05-18): mirror campaigns.share into the client-side
      // fallback so the editor's Sharing panel renders for org_admin /
      // marketing_admin / admin tokens that don't carry a fine-grained
      // capabilities[] array. site_manager intentionally doesn't get
      // this — they can't grant share access to themselves.
      org_admin:        ['analytics.view_org', 'analytics.view_site', 'analytics.view_revenue', 'analytics.view_alerts', 'analytics.manage_alerts', 'analytics.export', 'analytics.share_views', 'campaigns.share', 'campaigns.publish'],
      marketing_admin:  ['analytics.view_org', 'analytics.view_site', 'analytics.view_revenue', 'analytics.view_alerts', 'analytics.export', 'analytics.share_views', 'campaigns.share', 'campaigns.publish'],
      site_manager:     ['analytics.view_site', 'analytics.view_revenue', 'analytics.view_alerts', 'analytics.manage_alerts', 'analytics.export', 'campaigns.publish'],
      designer:         ['analytics.view_site'],
      curator:          ['analytics.view_site'],
      operator:         ['analytics.view_site'],
      viewer:           ['analytics.view_site'],
      admin:            ['analytics.view_org', 'analytics.view_site', 'analytics.view_revenue', 'analytics.view_alerts', 'analytics.manage_alerts', 'analytics.export', 'analytics.share_views', 'campaigns.share', 'campaigns.publish'],
    };
    return (ROLE_DEFAULTS[u.role] || []).includes(capId);
  },
  /* Convenience analytics helpers — JSX reads cleaner than long
     hasCapability() chains. */
  canViewAnalytics() {
    return this.hasCapability('analytics.view_org')
        || this.hasCapability('analytics.view_site')
        || this.hasCapability('analytics.view_platform');
  },
  canViewAnalyticsRevenue() { return this.hasCapability('analytics.view_revenue'); },
  canViewAnalyticsAlerts()  { return this.hasCapability('analytics.view_alerts'); },
  canManageAnalyticsAlerts(){ return this.hasCapability('analytics.manage_alerts'); },
  canExportAnalytics()      { return this.hasCapability('analytics.export'); },
  canShareAnalyticsViews()  { return this.hasCapability('analytics.share_views'); },
  canViewPlatformAnalytics(){ return this.hasCapability('analytics.view_platform'); },

  /* ── Campaign sharing (v80) ──────────────────────────────────────────
     canShareCampaigns(): can the caller flip a campaign or group's
       shareable_org_wide flag and manage the per-site share list?
       Org/marketing admins + aquaos_admin by default.
     canApplyCampaign(c): is the caller allowed to add/remove targets
       on this campaign? True for owners (permission==='edit') AND for
       site_managers who got use-only access via a share. */
  canShareCampaigns() { return this.hasCapability('campaigns.share'); },
  /* v82 (2026-05-19): publish gate. site_manager now holds this
     (limited server-side to their own site by getCampaignForWrite).
     The UI uses it to decide whether to surface the Publish action
     on draft / approved / paused campaigns. */
  canPublishCampaigns() { return this.hasCapability('campaigns.publish'); },
  canApplyCampaign(c) {
    if (!c) return false;
    if (c.permission === 'edit' || c.permission === 'use') return true;
    // No permission field on the row — fall back to role-based gate
    // (older endpoints that haven't been re-fetched since the v80
    // deploy may serve rows without the field).
    return this.canEditCampaigns();
  },

  /* Site scope — site_manager and operator are pinned to req.user.site_id;
     other roles see all sites in their org. */
  isSiteScoped() {
    return this.hasRole(ROLES.SITE_MANAGER, ROLES.OPERATOR);
  },
  getSiteId() {
    const u = this.getUser();
    return u && u.site_id;
  },

  /* Backward-compat shim for code that still calls Auth.isAdmin().
     Maps to "can manage users" — closest-to-original semantic. */
  isAdmin() {
    return this.canManageUsers();
  },
};

async function apiFetch(url, options = {}) {
  const token = Auth.getToken();
  const headers = Object.assign(
    { 'Content-Type': 'application/json' },
    options.headers || {},
    token ? { Authorization: `Bearer ${token}` } : {}
  );
  const res = await fetch(url, { ...options, headers });
  if (res.status === 401) {
    Auth.clear();
    window.location.hash = '#login';
    throw new Error('Unauthorized');
  }
  if (!res.ok) {
    let msg = `${res.status} ${res.statusText}`;
    try { const body = await res.json(); if (body.error) msg = body.error; } catch (_) {}
    throw new Error(msg);
  }
  if (res.status === 204) return null;
  return res.json();
}

/* Site-scoped fetch — auto-appends ?site_id= when the user is pinned
   to a single site (site_manager / operator). Other roles get the
   un-augmented URL and the backend filters per its own logic. */
async function apiFetchSiteScoped(url, options = {}) {
  if (Auth.isSiteScoped() && Auth.getSiteId()) {
    const sep = url.includes('?') ? '&' : '?';
    url = `${url}${sep}site_id=${encodeURIComponent(Auth.getSiteId())}`;
  }
  return apiFetch(url, options);
}

window.ROLES = ROLES;
window.Auth = Auth;
window.apiFetch = apiFetch;
window.apiFetchSiteScoped = apiFetchSiteScoped;
