const EXTERNAL_API_BASE = "/api/v1/external";
const CSRF_COOKIE_NAME = "hyapp_external_csrf";
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);

let unauthorizedHandler = null;
let inMemoryCsrfToken = "";

export class ExternalApiError extends Error {
  constructor(message, { code = "", status = 0, details = null } = {}) {
    super(message);
    this.name = "ExternalApiError";
    this.code = code;
    this.status = status;
    this.details = details;
  }
}

export function setExternalUnauthorizedHandler(handler) {
  unauthorizedHandler = typeof handler === "function" ? handler : null;
  return () => {
    if (unauthorizedHandler === handler) {
      unauthorizedHandler = null;
    }
  };
}

export function rememberExternalCsrfToken(token) {
  // Cookie remains the source of truth. The memory copy only covers deployments where Cookie Path prevents page JavaScript from reading it.
  inMemoryCsrfToken = String(token || "").trim();
}

export function readExternalCsrfToken(cookieText = globalThis.document?.cookie || "") {
  const prefix = `${CSRF_COOKIE_NAME}=`;
  const cookieValue = String(cookieText)
    .split(";")
    .map((part) => part.trim())
    .find((part) => part.startsWith(prefix));

  if (!cookieValue) {
    return inMemoryCsrfToken;
  }

  try {
    return decodeURIComponent(cookieValue.slice(prefix.length));
  } catch {
    return cookieValue.slice(prefix.length);
  }
}

export async function externalRequest(path, options = {}) {
  const method = String(options.method || "GET").toUpperCase();
  const headers = new Headers(options.headers || {});
  const body = createBody(options.body, headers);

  if (!SAFE_METHODS.has(method)) {
    const csrfToken = readExternalCsrfToken();
    if (csrfToken) {
      headers.set("X-CSRF-Token", csrfToken);
    }
  }

  const response = await fetch(buildUrl(path, options.query), {
    body,
    credentials: "include",
    headers,
    method,
    signal: options.signal
  });
  const payload = await readPayload(response);

  if (response.status === 401 && options.notifyUnauthorized !== false) {
    unauthorizedHandler?.();
  }

  if (!response.ok) {
    throw apiError(payload, response.status, response.statusText || "Request failed");
  }

  if (isEnvelopeFailure(payload)) {
    throw apiError(payload, response.status, "Request failed");
  }

  const data = unwrapPayload(payload);
  if (data && typeof data === "object" && "csrfToken" in data) {
    rememberExternalCsrfToken(data.csrfToken);
  }
  return data;
}

function buildUrl(path, query) {
  const normalizedPath = String(path || "").startsWith("/") ? path : `/${path}`;
  const params = new URLSearchParams();

  Object.entries(query || {}).forEach(([key, value]) => {
    if (value === undefined || value === null || value === "") {
      return;
    }
    (Array.isArray(value) ? value : [value]).forEach((item) => params.append(key, String(item)));
  });

  const search = params.toString();
  return `${EXTERNAL_API_BASE}${normalizedPath}${search ? `?${search}` : ""}`;
}

function createBody(body, headers) {
  if (body === undefined || body === null) {
    return undefined;
  }
  if (body instanceof FormData || typeof body === "string" || body instanceof Blob) {
    return body;
  }
  headers.set("Content-Type", "application/json");
  return JSON.stringify(body);
}

async function readPayload(response) {
  if (response.status === 204) {
    return null;
  }
  const contentType = response.headers.get("content-type") || "";
  if (contentType.includes("application/json")) {
    return response.json();
  }
  const text = await response.text();
  return text ? { message: text } : null;
}

function isEnvelopeFailure(payload) {
  if (!payload || typeof payload !== "object" || !("code" in payload)) {
    return false;
  }
  return ![0, 200, "0", "200", "OK", "SUCCESS"].includes(payload.code);
}

function unwrapPayload(payload) {
  if (!payload || typeof payload !== "object") {
    return payload;
  }
  return "data" in payload ? payload.data : payload;
}

function apiError(payload, status, fallback) {
  const source = payload && typeof payload === "object" ? payload : {};
  return new ExternalApiError(source.message || source.msg || fallback, {
    code: source.code,
    details: source.details || source.errors || null,
    status
  });
}
