// Typed fetch wrapper for the GuruConnect API.
//
// Responsibilities:
//  - Resolve the base URL (VITE_API_URL, default same-origin).
//  - Attach `Authorization: Bearer <token>` from a pluggable token provider.
//  - Normalize the *two* inconsistent server error envelopes into one
//    ApiError shape so callers/UI never have to branch on which one came back.

const BASE_URL = (import.meta.env.VITE_API_URL ?? "").replace(/\/$/, "");

/** A normalized API error. `code` is present only for the structured envelope. */
export class ApiError extends Error {
  readonly status: number;
  readonly code?: string;

  constructor(message: string, status: number, code?: string) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.code = code;
  }
}

// The token lives in memory in the auth layer. We read it through a provider so
// the client has no hard dependency on React state and stays testable.
let tokenProvider: () => string | null = () => null;

export function setTokenProvider(provider: () => string | null): void {
  tokenProvider = provider;
}

// Called when any request returns 401 — lets the auth layer tear down session
// state and bounce to /login. Set by AuthProvider.
let onUnauthorized: (() => void) | null = null;

export function setUnauthorizedHandler(handler: (() => void) | null): void {
  onUnauthorized = handler;
}

interface RequestOptions {
  method?: string;
  body?: unknown;
  // Suppress the global 401 handler (used by the login call itself).
  skipAuthRedirect?: boolean;
  signal?: AbortSignal;
}

/** The server's two error envelopes, unioned. We extract a message from either. */
interface ErrorEnvelope {
  error?: string;
  detail?: string;
  error_code?: string;
  status_code?: number;
}

function buildUrl(path: string): string {
  if (path.startsWith("http://") || path.startsWith("https://")) return path;
  return `${BASE_URL}${path.startsWith("/") ? path : `/${path}`}`;
}

async function extractError(res: Response): Promise<ApiError> {
  let message = `Request failed (${res.status})`;
  let code: string | undefined;

  const raw = await res.text();
  if (raw) {
    try {
      const env = JSON.parse(raw) as ErrorEnvelope;
      // Handle BOTH envelopes: `{error}` and `{detail, error_code, status_code}`.
      const msg = env.detail ?? env.error;
      if (typeof msg === "string" && msg.length > 0) message = msg;
      if (typeof env.error_code === "string") code = env.error_code;
    } catch {
      // Non-JSON body (e.g. the machines routes return plain &'static str on
      // error). Use the trimmed text as the message if it looks sane.
      const trimmed = raw.trim();
      if (trimmed && trimmed.length < 300) message = trimmed;
    }
  }

  return new ApiError(message, res.status, code);
}

async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
  const headers: Record<string, string> = {};
  const token = tokenProvider();
  if (token) headers["Authorization"] = `Bearer ${token}`;

  let body: BodyInit | undefined;
  if (opts.body !== undefined) {
    headers["Content-Type"] = "application/json";
    body = JSON.stringify(opts.body);
  }

  let res: Response;
  try {
    res = await fetch(buildUrl(path), {
      method: opts.method ?? "GET",
      headers,
      body,
      signal: opts.signal,
    });
  } catch (err) {
    if (err instanceof DOMException && err.name === "AbortError") throw err;
    throw new ApiError("Network error — could not reach the server.", 0);
  }

  if (res.status === 401 && !opts.skipAuthRedirect) {
    onUnauthorized?.();
  }

  if (!res.ok) {
    throw await extractError(res);
  }

  // 204 No Content / empty body.
  if (res.status === 204) return undefined as T;
  const text = await res.text();
  if (!text) return undefined as T;
  // Most success responses are JSON, but some routes return a plain-text body
  // on 200 (e.g. cancel returns "Code cancelled"). Tolerate non-JSON so a
  // successful call isn't surfaced as a SyntaxError failure.
  try {
    return JSON.parse(text) as T;
  } catch {
    return undefined as T;
  }
}

export const http = {
  get: <T>(path: string, signal?: AbortSignal) =>
    request<T>(path, { method: "GET", signal }),
  post: <T>(path: string, body?: unknown, opts?: Partial<RequestOptions>) =>
    request<T>(path, { method: "POST", body, ...opts }),
  put: <T>(path: string, body?: unknown) =>
    request<T>(path, { method: "PUT", body }),
  del: <T>(path: string) => request<T>(path, { method: "DELETE" }),
};
