import type { ApiRequest, ApiResult } from "./types";
import { analytics } from "../analytics/analytics";
import { MobileAnalyticsEvents } from "../analytics/events";
import { nextCorrelationId } from "../telemetry/correlation";

type CreateApiClientOptions = {
  baseUrl: string;
  fetchImpl?: typeof fetch;
  defaultTimeoutMs?: number;
  getAccessToken?: () => string | undefined;
  getTenantId?: () => string | undefined;
  getUserAgentTag?: () => string | undefined;
  onAuthError?: () => Promise<string | null>;
  retry?: {
    maxRetries: number;
    baseDelayMs: number;
    maxDelayMs: number;
  };
};

function buildUrl(
  baseUrl: string,
  path: string,
  query?: ApiRequest["query"],
): string {
  const url = new URL(path.replace(/^\//, ""), baseUrl.replace(/\/+$/, "") + "/");

  if (query) {
    for (const [key, value] of Object.entries(query)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  return url.toString();
}

function normalizePathForAnalytics(path: string): string {
  const uuid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
  return path.replace(uuid, ":id");
}

type ServerErrorShape =
  | {
      error?: {
        code?: string;
        message?: string;
        details?: unknown;
      };
    }
  | undefined;

function getServerErrorInfo(body: unknown): {
  code?: string;
  message?: string;
  details?: unknown;
} {
  if (!body || typeof body !== "object") return {};
  const maybe = body as ServerErrorShape;
  const code = maybe?.error?.code;
  const message = maybe?.error?.message;
  const details = maybe?.error?.details;
  return { code, message, details };
}

async function readBody(response: Response): Promise<unknown> {
  const contentType = response.headers.get("content-type") ?? "";
  if (contentType.includes("application/json")) {
    try {
      return await response.json();
    } catch {
      return undefined;
    }
  }

  try {
    return await response.text();
  } catch {
    return undefined;
  }
}

function mergeHeaders(
  headers?: Record<string, string | undefined>,
): Record<string, string> {
  const merged: Record<string, string> = {};
  if (!headers) return merged;
  for (const [key, value] of Object.entries(headers)) {
    if (!value) continue;
    merged[key] = value;
  }
  return merged;
}

function stableSerializeHeaders(headers: Record<string, string>): string {
  const entries = Object.entries(headers).sort(([a], [b]) => a.localeCompare(b));
  return JSON.stringify(entries);
}

export type ApiClient = {
  request<T>(req: ApiRequest): Promise<ApiResult<T>>;
};

export function createApiClient(options: CreateApiClientOptions): ApiClient {
  const fetchImpl = options.fetchImpl ?? fetch;
  const defaultTimeoutMs = options.defaultTimeoutMs ?? 30_000;
  const baseUrl = options.baseUrl;
  const getAccessToken = options.getAccessToken;
  const getTenantId = options.getTenantId;
  const getUserAgentTag = options.getUserAgentTag;
  const onAuthError = options.onAuthError;
  const retry = options.retry ?? { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 5_000 };
  const inFlightGetRequests = new Map<string, Promise<ApiResult<unknown>>>();

  return {
    async request<T>(req: ApiRequest): Promise<ApiResult<T>> {
      const startedAt = Date.now();
      const url = buildUrl(baseUrl, req.path, req.query);
      const timeoutMs = req.timeoutMs ?? defaultTimeoutMs;
      const correlationId = nextCorrelationId();

      const isRetryableMethod = req.method === "GET" || req.method === "HEAD";
      const maxAttempts = isRetryableMethod ? retry.maxRetries + 1 : 1;

      const trackSucceeded = (status: number, attempts: number) => {
        analytics.trackEvent(MobileAnalyticsEvents.apiRequestSucceeded, {
          method: req.method,
          path: normalizePathForAnalytics(req.path),
          status,
          durationMs: Date.now() - startedAt,
          attempts,
        });
      };

      const sleep = (ms: number) =>
        new Promise<void>((resolve, reject) => {
          const onAbort = () => {
            cleanup();
            reject(new Error("aborted"));
          };

          const onTimeout = () => {
            cleanup();
            resolve();
          };

          const cleanup = () => {
            clearTimeout(handle);
            if (req.signal) req.signal.removeEventListener("abort", onAbort);
          };

          const handle = setTimeout(onTimeout, ms);

          if (req.signal) {
            if (req.signal.aborted) return onAbort();
            req.signal.addEventListener("abort", onAbort);
          }
        });

      const attemptOnce = async (overrideAuthToken?: string): Promise<ApiResult<T>> => {
        const abortController = new AbortController();
        let timedOut = false;
        const timeoutHandle = setTimeout(() => {
          timedOut = true;
          abortController.abort();
        }, timeoutMs);

        const abortListener = () => abortController.abort();
        if (req.signal) {
          if (req.signal.aborted) {
            abortController.abort();
          } else {
            req.signal.addEventListener("abort", abortListener, { once: true });
          }
        }

        try {
          const authToken = overrideAuthToken ?? getAccessToken?.();
          const tenantId = getTenantId?.();
          const userAgentTag = getUserAgentTag?.();
          const isFormDataBody = typeof FormData !== "undefined" && req.body instanceof FormData;

          const headers = mergeHeaders({
            accept: "application/json",
            ...(req.body === undefined || isFormDataBody ? {} : { "content-type": "application/json" }),
            ...(tenantId ? { "x-tenant-id": tenantId } : {}),
            ...(userAgentTag ? { "x-alga-client": userAgentTag } : {}),
            ...req.headers,
          });

          if (authToken) headers.authorization = `Bearer ${authToken}`;
          if (overrideAuthToken) headers["x-api-key"] = overrideAuthToken;
          if (!headers["x-correlation-id"]) headers["x-correlation-id"] = correlationId;

          const response = await fetchImpl(url, {
            method: req.method,
            headers,
            body:
              req.body === undefined
                ? undefined
                : isFormDataBody
                  ? (req.body as FormData)
                  : JSON.stringify(req.body),
            signal: abortController.signal,
          });

          const body = await readBody(response);
          const serverError = getServerErrorInfo(body);

          if (!response.ok) {
            const message = serverError.message ?? `HTTP ${response.status}`;
            const code = serverError.code;
            const details = serverError.details;

            if (response.status === 401) {
              return {
                ok: false,
                status: response.status,
                error: { kind: "auth", status: response.status, message, code, body },
              };
            }

            if (response.status === 403) {
              return {
                ok: false,
                status: response.status,
                error: { kind: "permission", status: response.status, message, code, body },
              };
            }

            if (response.status === 400 || response.status === 422) {
              return {
                ok: false,
                status: response.status,
                error: { kind: "validation", status: response.status, message, code, details, body },
              };
            }

            if (response.status >= 500) {
              return {
                ok: false,
                status: response.status,
                error: { kind: "server", status: response.status, message, code, body },
              };
            }

            return {
              ok: false,
              status: response.status,
              error: {
                kind: "http",
                status: response.status,
                message,
                code,
                body,
              },
            };
          }

          return { ok: true, status: response.status, data: body as T };
        } catch (error) {
          if (abortController.signal.aborted) {
            if (timedOut) {
              return {
                ok: false,
                error: { kind: "timeout", message: "Request timed out", timeoutMs },
              };
            }
            return { ok: false, error: { kind: "canceled", message: "Request canceled" } };
          }

          return {
            ok: false,
            error: { kind: "network", message: "Network request failed", cause: error },
          };
        } finally {
          clearTimeout(timeoutHandle);
          if (req.signal) req.signal.removeEventListener("abort", abortListener);
        }
      };

      const execute = async (): Promise<ApiResult<T>> => {
        let didAuthRetry = false;
        for (let attempt = 0; attempt < maxAttempts; attempt++) {
          const result = await attemptOnce();
          if (result.ok) {
            trackSucceeded(result.status, attempt + 1);
            return result;
          }
          if (result.error.kind === "canceled") return result;

          if (result.error.kind === "auth" && onAuthError && !didAuthRetry) {
            didAuthRetry = true;
            try {
              const nextToken = await onAuthError();
              if (nextToken) {
                const afterRefresh = await attemptOnce(nextToken);
                const attemptsUsed = attempt + 2;
                if (afterRefresh.ok) {
                  trackSucceeded(afterRefresh.status, attemptsUsed);
                  return afterRefresh;
                }
                analytics.trackEvent(MobileAnalyticsEvents.apiRequestFailed, {
                  method: req.method,
                  path: normalizePathForAnalytics(req.path),
                  status: afterRefresh.status ?? null,
                  errorKind: afterRefresh.error.kind,
                  durationMs: Date.now() - startedAt,
                  attempts: attemptsUsed,
                });
                return afterRefresh;
              }
            } catch {
              analytics.trackEvent(MobileAnalyticsEvents.apiRequestFailed, {
                method: req.method,
                path: normalizePathForAnalytics(req.path),
                status: result.status ?? null,
                errorKind: "exception",
                durationMs: Date.now() - startedAt,
                attempts: attempt + 1,
              });
              return result;
            }

            analytics.trackEvent(MobileAnalyticsEvents.apiRequestFailed, {
              method: req.method,
              path: normalizePathForAnalytics(req.path),
              status: result.status ?? null,
              errorKind: result.error.kind,
              durationMs: Date.now() - startedAt,
              attempts: attempt + 1,
            });
            return result;
          }

          const retryableStatus =
            result.status !== undefined && result.status >= 500 && result.status < 600;

          const retryableError =
            result.error.kind === "network" || result.error.kind === "timeout" || retryableStatus;

          const isLastAttempt = attempt === maxAttempts - 1;
          if (!retryableError || isLastAttempt) {
            analytics.trackEvent(MobileAnalyticsEvents.apiRequestFailed, {
              method: req.method,
              path: normalizePathForAnalytics(req.path),
              status: result.status ?? null,
              errorKind: result.error.kind,
              durationMs: Date.now() - startedAt,
              attempts: attempt + 1,
            });
            return result;
          }

          const delay = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** attempt);
          const jittered = Math.round(delay * (0.8 + Math.random() * 0.4));
          try {
            await sleep(jittered);
          } catch {
            analytics.trackEvent(MobileAnalyticsEvents.apiRequestFailed, {
              method: req.method,
              path: normalizePathForAnalytics(req.path),
              status: result.status ?? null,
              errorKind: result.error.kind,
              durationMs: Date.now() - startedAt,
              attempts: attempt + 1,
            });
            return result;
          }
        }

        analytics.trackEvent(MobileAnalyticsEvents.apiRequestFailed, {
          method: req.method,
          path: normalizePathForAnalytics(req.path),
          status: null,
          errorKind: "network",
          durationMs: Date.now() - startedAt,
          attempts: maxAttempts,
        });
        return { ok: false, error: { kind: "network", message: "Network request failed" } };
      };

      if (req.method === "GET" && !req.signal) {
        const accessToken = getAccessToken?.();
        const tenantId = getTenantId?.();
        const userAgentTag = getUserAgentTag?.();

        const dedupeHeaders = mergeHeaders({
          ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
          ...(tenantId ? { "x-tenant-id": tenantId } : {}),
          ...(userAgentTag ? { "x-alga-client": userAgentTag } : {}),
          ...req.headers,
        });

        const key = `${req.method} ${url} ${stableSerializeHeaders(dedupeHeaders)}`;
        const existing = inFlightGetRequests.get(key);
        if (existing) return (await existing) as ApiResult<T>;

        const promise = execute() as Promise<ApiResult<unknown>>;
        inFlightGetRequests.set(key, promise);
        promise.finally(() => {
          if (inFlightGetRequests.get(key) === promise) inFlightGetRequests.delete(key);
        });
        return (await promise) as ApiResult<T>;
      }

      return execute();
    },
  };
}
