// =================================================================
// api.jsx — Data layer.
//
// One place to wire backend endpoints into the UI. The layer reads
// API_CONFIG to decide whether to hit a real URL or fall back to the
// bundled fixtures. Screens never call fetch() directly — they ask
// the data layer.
//
// Usage in a screen:
//
//   const { data, loading, error, refresh } = useApi("listProjects");
//   const { mutate } = useMutation("createProject");
//   await mutate({ name: "my-project" });
//
// Or imperatively:
//
//   const list = await api.call("listProjects");
//   const newOne = await api.call("createProject", null, { name: "p1" });
//   const one = await api.call("getProject", { id: "p_1" });
//
// =================================================================

(() => {
  const cfg = window.API_CONFIG;
  if (!cfg) { console.warn("[api.jsx] API_CONFIG missing"); return; }

  // ---------- Auth ----------
  let _authToken = null;
  const setAuth = (token) => { _authToken = token; };
  const getAuthHeader = async () => {
    if (cfg.auth.strategy === "bearer") {
      const t = _authToken || (cfg.auth.getAuthToken ? await cfg.auth.getAuthToken() : null);
      return t ? { Authorization: `Bearer ${t}` } : {};
    }
    return {};
  };

  // ---------- URL interpolation: replace {id}, {projectId}, etc. ----------
  // Throws on a missing param rather than leaving a literal "{param}" in the URL
  // (which would silently send a malformed request).
  const interpolate = (url, params) =>
    url.replace(/\{(\w+)\}/g, (m, k) => {
      const v = params ? params[k] : undefined;
      if (v == null) throw new ApiError(`Missing URL parameter "${k}"`, 0, null);
      return encodeURIComponent(v);
    });

  // ---------- Resolve fixture for a given endpoint name ----------
  // The data argument is the App's `data` state — passed in by hooks below.
  const fixtureFor = (name, data, params) => {
    switch (name) {
      case "me":               return data.user;
      case "org":              return data.org;
      case "dashboardStats":   return { /* derived inline by screens — return empty */ };
      case "spendByDay":       return data.spendByDay;
      case "spendByModel":     return data.spendByModel;
      case "billing":          return {
        monthlySpend: data.monthlySpend, credits: data.credits,
        autoRecharge: data.autoRecharge, autoRechargeThreshold: data.autoRechargeThreshold,
        autoRechargeAmount: data.autoRechargeAmount, paymentMethod: data.paymentMethod,
        invoiceAddress: data.invoiceAddress, taxId: data.taxId, taxIdType: data.taxIdType,
      };
      case "invoices":         return data.invoices;
      case "listProjects":     return data.projects;
      case "getProject":       return data.projects.find(p => p.id === params?.id);
      case "listMembers":      return data.members;
      case "listApiKeys":      return data.apiKeys;
      case "listFiles":        return data.files;
      case "listSshKeys":      return data.sshKeys;
      case "listIntegrations": return data.integrations;
      case "listNotifications":return data.notifications || [];
      default:                 return null;
    }
  };

  // ---------- Fetch with timeout ----------
  const fetchWithTimeout = (url, opts, ms) => {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), ms);
    return fetch(url, { ...opts, signal: ctrl.signal })
      .finally(() => clearTimeout(t));
  };

  // ---------- Core call ----------
  // name: endpoint key in API_CONFIG.endpoints
  // params: path params (interpolated into URL)
  // body: request body for POST/PUT/PATCH
  // opts: { data } — pass the App's `data` for fixture fallback
  const call = async (name, params = null, body = null, opts = {}) => {
    const def = cfg.endpoints[name];
    if (!def) throw new Error(`[api] Unknown endpoint "${name}"`);

    const isLive =
      (cfg.mode === "live" || cfg.mode === "hybrid") &&
      def.url && cfg.baseUrl;

    // Fixture path
    if (!isLive) {
      // tiny latency to mimic a real call, so UIs render loading states
      await new Promise(r => setTimeout(r, 60));
      return fixtureFor(name, opts.data || window.__appData || {}, params);
    }

    // Live path (opts.qs: optional query string, e.g. "?page=2")
    const url = cfg.baseUrl + interpolate(def.url, params) + (opts.qs || "");
    const authHeaders = await getAuthHeader();
    const req = {
      method: def.method,
      credentials: cfg.defaults.credentials,
      headers: { ...cfg.defaults.headers, ...authHeaders, ...(opts.headers || {}) },
    };
    if (body != null && def.method !== "GET") {
      if (def.isMultipart && body instanceof FormData) {
        delete req.headers["Content-Type"]; // browser sets boundary
        req.body = body;
      } else {
        req.body = JSON.stringify(body);
      }
    }

    let res;
    try {
      res = await fetchWithTimeout(url, req, cfg.defaults.timeoutMs);
    } catch (e) {
      throw new ApiError(`Network error: ${e.message}`, 0, e);
    }

    // 401 → try one token refresh, then retry the original request once.
    if (res.status === 401 && name !== "login" && name !== "refresh" && !opts._retried) {
      const refreshed = await tryRefresh();
      if (refreshed) {
        return call(name, params, body, { ...opts, _retried: true });
      }
      // Refresh failed — session is dead, send user to sign-in.
      if (window.session) window.session.clear();
      history.replaceState(null, "", "/sign-in"); window.location.reload();
    }

    if (!res.ok) {
      let payload = null;
      try { payload = await res.json(); } catch (e) {}
      throw new ApiError(payload?.detail || payload?.message || res.statusText, res.status, payload);
    }
    if (res.status === 204) return null;
    try { return await res.json(); } catch (e) { return null; }
  };

  // ---------- Token refresh (single-flight: concurrent 401s share one refresh) ----------
  let _refreshPromise = null;
  const tryRefresh = () => {
    if (_refreshPromise) return _refreshPromise;
    const refreshToken = localStorage.getItem("refresh_token");
    if (!refreshToken) return Promise.resolve(false);

    _refreshPromise = (async () => {
      try {
        const def = cfg.endpoints.refresh;
        const res = await fetchWithTimeout(cfg.baseUrl + def.url, {
          method: "POST",
          headers: { "Content-Type": "application/json", "Accept": "application/json" },
          body: JSON.stringify({ refresh_token: refreshToken }),
        }, cfg.defaults.timeoutMs);
        if (!res.ok) return false;
        const data = await res.json();
        if (data.access_token)  localStorage.setItem("access_token",  data.access_token);
        if (data.refresh_token) localStorage.setItem("refresh_token", data.refresh_token);
        return true;
      } catch {
        return false;
      } finally {
        setTimeout(() => { _refreshPromise = null; }, 0);
      }
    })();
    return _refreshPromise;
  };

  // ---------- Tenant-scoped call: injects tenantId from session ----------
  const tcall = (name, params = null, body = null, opts = {}) => {
    const tenantId = window.session?.tenantId;
    if (!tenantId) {
      return Promise.reject(new ApiError("No active tenant selected", 0, null));
    }
    return call(name, { tenantId, ...(params || {}) }, body, opts);
  };

  class ApiError extends Error {
    constructor(msg, status, body) { super(msg); this.name = "ApiError"; this.status = status; this.body = body; }
  }

  // ---------- React hooks ----------
  // Useful when a component owns its own data; for the rest of the
  // template we hand the App's `data` state to call() directly so the
  // existing screens keep working.
  const useApi = (name, params = null, deps = []) => {
    const [state, setState] = React.useState({ data: null, loading: true, error: null });
    const refresh = React.useCallback(async () => {
      setState(s => ({ ...s, loading: true, error: null }));
      try {
        const data = await call(name, params, null, { data: window.__appData || {} });
        setState({ data, loading: false, error: null });
      } catch (e) {
        setState({ data: null, loading: false, error: e });
      }
    // eslint-disable-next-line
    }, deps);
    React.useEffect(() => { refresh(); }, [refresh]);
    return { ...state, refresh };
  };

  const useMutation = (name) => {
    const [state, setState] = React.useState({ loading: false, error: null });
    const mutate = async (body, params = null) => {
      setState({ loading: true, error: null });
      try {
        const res = await call(name, params, body, { data: window.__appData || {} });
        setState({ loading: false, error: null });
        return res;
      } catch (e) {
        setState({ loading: false, error: e });
        throw e;
      }
    };
    return { mutate, ...state };
  };

  // ---------- Export ----------
  window.api = { call, tcall, setAuth, ApiError };
  window.useApi = useApi;
  window.useMutation = useMutation;
})();
