// =================================================================
// 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. ----------
  const interpolate = (url, params) => {
    if (!params) return url;
    return url.replace(/\{(\w+)\}/g, (m, k) =>
      params[k] != null ? encodeURIComponent(params[k]) : m
    );
  };

  // ---------- 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 "health":           return { ok: true, source: "fixture" };
      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 "orgSubscription":  return data.subscription;
      case "orgCredits":       return data.creditLedger;
      case "creditLedger":     return data.creditLedger.entries || [];
      case "listScripts":      return data.scripts;
      case "listScriptVersions": return data.scriptVersions;
      case "listJobs":         return data.jobs;
      case "listAllJobs":      return data.jobs;
      case "getJob":           return data.jobs.find(j => j.id === params?.id);
      case "listArtifacts":    return data.artifacts.filter(a => !params?.id || a.jobId === params.id);
      case "listAllArtifacts": return data.artifacts;
      case "adminListPackages": return data.packages;
      case "adminGetPackage":  return data.packages.find(p => p.id === params?.id);
      case "adminPackageEvents": return data.packageEvents.filter(e => e.packageId === params?.id);
      case "packageAnalytics": return data.analyticsByScript;
      case "usageAnalytics":   return data.usageAnalytics;
      case "billingAnalytics": return data.usageAnalytics;
      case "failureAnalytics": return data.jobs.filter(j => j.status === "Failed");
      case "listClients":      return data.clients;
      case "supportQueue":     return data.supportQueue;
      case "createSupportTicket": return null;
      case "updateSupportTicket": return null;
      case "aiManagerTasks":   return data.aiManagerTasks;
      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 isLocalhost = ["localhost", "127.0.0.1", ""].includes(window.location.hostname);
    const isDevelopment =
      cfg.environment === "development" ||
      isLocalhost ||
      window.location.protocol === "file:";
    const canUseFixtureFallback =
      cfg.fixtureFallback === true ||
      cfg.fixtureFallback === "always" ||
      (cfg.fixtureFallback === "development-only" && isDevelopment);
    const useFixture = async (reason, details) => {
      if (!canUseFixtureFallback) {
        throw new ApiError(`Fixture fallback is disabled for ${name}.`, 0, details);
      }
      window.__apiFixtureFallback = { name, reason, at: new Date().toISOString() };
      window.dispatchEvent(new CustomEvent("api-fixture-fallback", { detail: window.__apiFixtureFallback }));
      await new Promise(r => setTimeout(r, 60));
      return fixtureFor(name, opts.data || window.__appData || {}, params);
    };
    const isLive =
      (cfg.mode === "live" || cfg.mode === "hybrid") &&
      def.url && cfg.baseUrl;

    // Fixture path
    if (!isLive || (cfg.disableLiveCallsOnLocalhost && isLocalhost && !opts.forceLive)) {
      return useFixture(!isLive ? "endpoint-not-mapped" : "localhost-live-disabled");
    }

    // Live path
    const url = cfg.baseUrl + interpolate(def.url, params);
    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.isRaw) {
        req.body = body;
        if (!opts.headers?.["Content-Type"]) delete req.headers["Content-Type"];
      } else 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) {
      if (cfg.mode === "hybrid" && canUseFixtureFallback) {
        console.warn(`[api] ${name} live call failed; using fixture fallback`, e);
        return useFixture("network-error", e);
      }
      throw new ApiError(`Network error: ${e.message}`, 0, e);
    }
    if (!res.ok) {
      let payload = null;
      try { payload = await res.json(); } catch (e) {}
      if (cfg.mode === "hybrid" && canUseFixtureFallback) {
        console.warn(`[api] ${name} returned ${res.status}; using fixture fallback`, payload);
        return useFixture(`http-${res.status}`, payload);
      }
      throw new ApiError(payload?.message || res.statusText, res.status, payload);
    }
    if (res.status === 204) return null;
    try {
      const payload = await res.json();
      return payload && Object.prototype.hasOwnProperty.call(payload, "data") ? payload.data : payload;
    } catch (e) { return null; }
  };

  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, setAuth, ApiError };
  window.useApi = useApi;
  window.useMutation = useMutation;
})();
