import axios, {
  type AxiosError,
  type InternalAxiosRequestConfig,
} from "axios";
import type { ApiResponse, AuthTokenResponse } from "./types";
import { getOrCreateTerminalId } from "@/lib/terminal";
import { useAuthStore } from "@/lib/stores/auth.store";
import {
  isNetworkError,
  isOnlineOnly,
  newIdempotencyKey,
  OfflineUnavailableError,
  queueWrite,
} from "@/lib/offline/offline-write";

const baseURL =
  process.env.NEXT_PUBLIC_API_URL ?? "https://localhost:7208";

export const api = axios.create({
  baseURL,
  headers: { "Content-Type": "application/json" },
});

api.interceptors.request.use((config) => {
  if (typeof window !== "undefined") {
    const token = localStorage.getItem("meezi_access_token");
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    config.headers["X-Meezi-Terminal-Id"] = getOrCreateTerminalId();
  }
  return config;
});

/**
 * Shared in-flight refresh promise so that a burst of concurrent 401s triggers
 * exactly one POST /api/auth/refresh instead of one per failed request.
 */
let refreshPromise: Promise<RefreshResult> | null = null;

/** Result of a refresh attempt. `sessionInvalid` is true ONLY when the session
 *  is definitively gone (no/expired/revoked refresh token) — never for a
 *  transient failure like the API being briefly down during a deploy. */
type RefreshResult = { token: string | null; sessionInvalid: boolean };

async function refreshAccessToken(): Promise<RefreshResult> {
  if (typeof window === "undefined") return { token: null, sessionInvalid: false };
  const refreshToken = localStorage.getItem("meezi_refresh_token");
  if (!refreshToken) return { token: null, sessionInvalid: true };
  try {
    // Bare axios call (not `api`) to avoid recursing through this interceptor.
    const { data } = await axios.post<ApiResponse<AuthTokenResponse>>(
      `${baseURL}/api/auth/refresh`,
      { refreshToken },
      { headers: { "Content-Type": "application/json" } }
    );
    if (data.success && data.data) {
      useAuthStore.getState().setAuth(data.data);
      return { token: data.data.accessToken, sessionInvalid: false };
    }
    // Server answered but rejected the token → session truly invalid.
    return { token: null, sessionInvalid: true };
  } catch (err) {
    // Only a definitive 4xx from /auth/refresh means the session is gone. A
    // network error, timeout, or 5xx (e.g. the API restarting mid-deploy) is
    // transient — do NOT log the user out for it.
    const status = (err as AxiosError)?.response?.status;
    const sessionInvalid = status === 400 || status === 401 || status === 403;
    return { token: null, sessionInvalid };
  }
}

function forceLogout(): void {
  if (typeof window === "undefined") return;
  const path = window.location.pathname;
  const isPublicGuest = path.startsWith("/q");
  const isAdmin = path.includes("/admin");
  if (isPublicGuest || isAdmin) return;
  localStorage.removeItem("meezi_access_token");
  localStorage.removeItem("meezi_refresh_token");
  localStorage.removeItem("meezi_auth");
  const locale = path.split("/")[1] ?? "fa";
  window.location.href = `/${locale}/login`;
}

api.interceptors.response.use(
  (response) => response,
  async (error: AxiosError<ApiResponse<unknown>>) => {
    const status = error.response?.status;
    const apiError = error.response?.data?.error;
    const original = error.config as
      | (InternalAxiosRequestConfig & { _retry?: boolean })
      | undefined;
    const onAuthPath = original?.url?.includes("/api/auth/") ?? false;

    // Expired access token → try a one-time refresh, then replay the request.
    if (
      status === 401 &&
      original &&
      !original._retry &&
      typeof window !== "undefined" &&
      !onAuthPath
    ) {
      original._retry = true;
      refreshPromise ??= refreshAccessToken().finally(() => {
        refreshPromise = null;
      });
      const { token, sessionInvalid } = await refreshPromise;
      if (token) {
        original.headers.Authorization = `Bearer ${token}`;
        return api(original);
      }
      // Refresh couldn't get a new token. Log out ONLY if the session is
      // definitively invalid — a transient failure (API redeploying, network
      // blip) just rejects so the request can be retried, keeping the user
      // logged in across deploys.
      if (sessionInvalid) forceLogout();
      return Promise.reject(
        apiError?.code
          ? new ApiClientError(apiError.code, apiError.message, undefined, status)
          : error
      );
    }

    if (apiError?.code) {
      return Promise.reject(new ApiClientError(apiError.code, apiError.message, undefined, status));
    }
    return Promise.reject(error);
  }
);

export interface PagedMeta {
  total: number;
  page: number;
  pageSize: number;
}

export interface PagedApiResponse<T> {
  success: boolean;
  data?: T[];
  meta?: PagedMeta;
  error?: { code: string; message: string; field?: string };
}

export async function apiGet<T>(url: string): Promise<T> {
  const { data } = await api.get<ApiResponse<T>>(url);
  if (!data.success || data.data === undefined) {
    throw new Error(data.error?.message ?? "Request failed");
  }
  return data.data;
}

export async function apiGetPaged<T>(url: string): Promise<{ items: T[]; meta: PagedMeta }> {
  const { data } = await api.get<PagedApiResponse<T>>(url);
  if (!data.success || data.data === undefined || !data.meta) {
    throw new Error(data.error?.message ?? "Request failed");
  }
  return { items: data.data, meta: data.meta };
}

export class ApiClientError extends Error {
  constructor(
    public readonly code: string,
    message: string,
    /** Payload returned alongside a non-success response (e.g. CHOOSE_CAFE choices). */
    public readonly payload?: unknown,
    /** HTTP status, when known — lets callers (e.g. the outbox) tell 5xx (retry) from 4xx (give up). */
    public readonly status?: number
  ) {
    super(message);
    this.name = "ApiClientError";
  }
}

/** Options for mutating requests. */
export interface WriteOptions {
  /** Reused as the `Idempotency-Key` header so the server de-duplicates retries. */
  idempotencyKey?: string;
  /**
   * Offline behavior:
   *  - undefined / "queue": auto-queue on offline/network failure and return an
   *    optimistic value (unless the URL is online-only → throws).
   *  - "reject": never queue — throw OfflineUnavailableError when offline.
   *  - "manual": caller handles offline itself; never auto-queue (POS order submit).
   */
  offline?: "queue" | "reject" | "manual";
}

async function rawWrite<T>(
  method: "POST" | "PUT" | "PATCH" | "DELETE",
  url: string,
  body: unknown,
  key: string
): Promise<T> {
  const config = { headers: { "Idempotency-Key": key } };
  let data: ApiResponse<T>;
  switch (method) {
    case "POST":
      ({ data } = await api.post<ApiResponse<T>>(url, body, config));
      break;
    case "PUT":
      ({ data } = await api.put<ApiResponse<T>>(url, body, config));
      break;
    case "PATCH":
      ({ data } = await api.patch<ApiResponse<T>>(url, body, config));
      break;
    case "DELETE":
      ({ data } = await api.delete<ApiResponse<T>>(url, config));
      break;
  }
  if (method === "DELETE") {
    if (!data.success) {
      throw new ApiClientError(data.error?.code ?? "REQUEST_FAILED", data.error?.message ?? "Request failed");
    }
    return undefined as T;
  }
  if (!data.success || data.data === undefined) {
    throw new ApiClientError(
      data.error?.code ?? "REQUEST_FAILED",
      data.error?.message ?? "Request failed",
      data.data
    );
  }
  return data.data;
}

async function doWrite<T>(
  method: "POST" | "PUT" | "PATCH" | "DELETE",
  url: string,
  body: unknown,
  opts?: WriteOptions
): Promise<T> {
  const manual = opts?.offline === "manual";
  const key = opts?.idempotencyKey ?? newIdempotencyKey();
  const onlineOnly = opts?.offline === "reject" || isOnlineOnly(url);
  const offline = typeof navigator !== "undefined" && !navigator.onLine;

  // Already offline: queue (or reject online-only) without attempting the network.
  if (offline && !manual) {
    if (onlineOnly) throw new OfflineUnavailableError();
    return (await queueWrite(method, url, body, key)) as T;
  }

  try {
    return await rawWrite<T>(method, url, body, key);
  } catch (err) {
    // A genuine network failure (no response) → queue and return optimistic.
    // Real server/validation errors and online-only endpoints still throw.
    if (!manual && !onlineOnly && isNetworkError(err)) {
      return (await queueWrite(method, url, body, key)) as T;
    }
    throw err;
  }
}

export async function apiPost<T, B = unknown>(url: string, body?: B, opts?: WriteOptions): Promise<T> {
  return doWrite<T>("POST", url, body, opts);
}

export async function apiPut<T, B = unknown>(url: string, body?: B, opts?: WriteOptions): Promise<T> {
  return doWrite<T>("PUT", url, body, opts);
}

export async function apiPatch<T, B = unknown>(url: string, body?: B, opts?: WriteOptions): Promise<T> {
  return doWrite<T>("PATCH", url, body, opts);
}

export async function apiDelete(url: string, opts?: WriteOptions): Promise<void> {
  await doWrite<void>("DELETE", url, undefined, opts);
}

/** GET binary response (QR PNG, Excel export, etc.) with auth headers. */
export async function apiGetBlob(path: string): Promise<Blob> {
  const response = await api.get(path, { responseType: "blob" });
  return response.data as Blob;
}

/** Public GET JSON (no auth required). */
export async function apiGetPublic<T>(path: string): Promise<T> {
  const { data } = await api.get<ApiResponse<T>>(path);
  if (!data.success || data.data === undefined) {
    throw new ApiClientError(
      data.error?.code ?? "REQUEST_FAILED",
      data.error?.message ?? "Request failed"
    );
  }
  return data.data;
}

/** Public POST JSON (no auth required). */
export async function apiPostPublic<T, B = unknown>(path: string, body?: B): Promise<T> {
  const { data } = await api.post<ApiResponse<T>>(path, body);
  if (!data.success || data.data === undefined) {
    throw new ApiClientError(
      data.error?.code ?? "REQUEST_FAILED",
      data.error?.message ?? "Request failed"
    );
  }
  return data.data;
}

export function openBlobInNewTab(blob: Blob): void {
  const url = URL.createObjectURL(blob);
  window.open(url, "_blank");
}

export async function apiDownload(path: string, filename: string): Promise<void> {
  const blob = await apiGetBlob(path);
  const objectUrl = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = objectUrl;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(objectUrl);
}

export async function apiUpload<T>(url: string, file: File): Promise<T> {
  const form = new FormData();
  form.append("file", file);
  const { data } = await api.post<ApiResponse<T>>(url, form, {
    headers: { "Content-Type": "multipart/form-data" },
  });
  if (!data.success || data.data === undefined) {
    throw new Error(data.error?.message ?? "Upload failed");
  }
  return data.data;
}

export function resolveMediaUrl(path?: string | null): string | undefined {
  if (!path) return undefined;
  if (path.startsWith("http://") || path.startsWith("https://")) return path;
  const base = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:5080";
  return `${base.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
}
