/**
 * API Client
 * Centralized HTTP client with interceptors, error handling, and type safety.
 * (Ported from fe0/src/shared/api/client.ts — only the two intra-package import paths changed.)
 */
import axios, {
  AxiosInstance,
  AxiosError,
  InternalAxiosRequestConfig,
  AxiosRequestConfig,
} from 'axios';
import { env } from '../config/env';
import { getAccessToken, clearAccessToken, setAccessToken } from '../utils/token';

export interface ApiError {
  message: string;
  status: number;
  data?: any;
  code?: string;
}

/** Thrown by {@link ApiClient.postArrayBuffer} on HTTP error responses so callers can inspect status (e.g. 501). */
export class ApiArrayBufferRequestError extends Error {
  readonly status: number;

  constructor(message: string, status: number) {
    super(message);
    this.name = 'ApiArrayBufferRequestError';
    this.status = status;
  }
}

/** True when the client rejected a response with 404 (Axios or normalized {@link ApiError}). */
export function isApiNotFoundError(err: unknown): boolean {
  if (err instanceof AxiosError && err.response?.status === 404) return true;
  if (err && typeof err === 'object') {
    const s = (err as Partial<ApiError>).status;
    if (typeof s === 'number' && s === 404) return true;
  }
  return false;
}

/** Used by QueryClient retry: never backoff-retry auth failures (avoids poll storms on expired sessions). */
export function isAuthQueryError(err: unknown): boolean {
  if (err instanceof AxiosError) {
    const s = err.response?.status;
    return s === 401 || s === 403;
  }
  if (err && typeof err === 'object') {
    const s = (err as Partial<ApiError>).status;
    if (typeof s === 'number') return s === 401 || s === 403;
  }
  return false;
}

function summarizeResponseDataForDevLog(data: unknown): unknown {
  if (data == null) return data;
  if (typeof data !== 'object') return data;
  const rec = data as Record<string, unknown>;
  if (Array.isArray(rec.data)) {
    return { ...rec, data: `[${rec.data.length} items]` };
  }
  return data;
}

function stringifyFastApiDetail(detail: unknown): string | undefined {
  if (detail == null) return undefined;
  if (typeof detail === 'string') return detail;
  if (Array.isArray(detail)) {
    const parts = detail
      .map((x) =>
        typeof x === 'object' && x && 'msg' in x ? String((x as { msg: string }).msg) : String(x),
      )
      .filter(Boolean);
    return parts.length ? parts.join(' ') : undefined;
  }
  return undefined;
}

/** True for the synthetic "HTTP <status>" message the interceptor attaches to 4xx rejections. */
function isSyntheticHttpMessage(message: unknown): boolean {
  return typeof message === 'string' && /^HTTP \d+$/.test(message.trim());
}

/** Human-readable message from ApiClient errors, ApiError-shaped objects, or raw AxiosErrors. */
export function detailFromApiError(err: unknown, fallback = 'Lỗi không xác định.'): string {
  if (err && typeof err === 'object') {
    const e = err as ApiError & {
      response?: { data?: { detail?: unknown; message?: string } };
    };
    // Prefer the FastAPI `{detail}` — on a raw AxiosError it lives at err.response.data.detail;
    // on a normalized ApiError it lives at err.data.detail. Either beats the synthetic "HTTP <status>".
    const detail = stringifyFastApiDetail(e.response?.data?.detail ?? e.data?.detail);
    if (detail) return detail;
    const respMessage = e.response?.data?.message;
    if (typeof respMessage === 'string' && respMessage.trim()) return respMessage;
    if (typeof e.message === 'string' && e.message.trim() && !isSyntheticHttpMessage(e.message)) {
      return e.message;
    }
  }
  if (err instanceof Error && err.message.trim() && !isSyntheticHttpMessage(err.message)) {
    return err.message;
  }
  return fallback;
}

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    if (import.meta.env.DEV) {
      console.log('API Client initialized with baseURL:', env.API_URL);
    }

    this.client = axios.create({
      baseURL: env.API_URL,
      timeout: 60000, // 60s for Ollama responses
      headers: {
        'Content-Type': 'application/json',
      },
      withCredentials: true,
      validateStatus: (status) => status < 500, // Don't throw on 4xx errors
    });

    this.setupInterceptors();
  }

  private setupInterceptors(): void {
    this.client.interceptors.request.use(
      (config: InternalAxiosRequestConfig) => {
        const token = getAccessToken();
        if (token && config.headers) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        // Multipart uploads: let the browser set Content-Type (boundary). A global
        // application/json default breaks FastAPI's UploadFile / Form parsing.
        if (config.data instanceof FormData && config.headers) {
          delete (config.headers as Record<string, unknown>)['Content-Type'];
        }
        return config;
      },
      (error) => Promise.reject(error),
    );

    this.client.interceptors.response.use(
      (response) => {
        // Default validateStatus allows 4xx; reject here so `.then(res => res.data)` never treats
        // FastAPI `{ detail }` bodies as successful entity payloads.
        if (response.status >= 400) {
          const err = new AxiosError(
            `HTTP ${response.status}`,
            AxiosError.ERR_BAD_REQUEST,
            response.config,
            response.request,
            response,
          );
          return Promise.reject(err);
        }
        if (import.meta.env.DEV) {
          console.debug('✅ API OK', {
            url: response.config.url,
            status: response.status,
            data: summarizeResponseDataForDevLog(response.data),
          });
        }
        return response;
      },
      async (error: AxiosError) => {
        if (import.meta.env.DEV) {
          console.error('❌ API Error:', {
            message: error.message,
            code: error.code,
            response: error.response
              ? { status: error.response.status, data: error.response.data }
              : null,
            config: {
              url: error.config?.url,
              baseURL: error.config?.baseURL,
              method: error.config?.method,
            },
          });
        }

        const originalRequest = error.config as InternalAxiosRequestConfig & {
          _retry?: boolean;
        };

        // Handle 401 - Unauthorized → try one refresh.
        if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
          originalRequest._retry = true;
          try {
            const newToken = await this.refreshToken();
            if (newToken && originalRequest.headers) {
              originalRequest.headers.Authorization = `Bearer ${newToken}`;
              return this.client(originalRequest);
            }
          } catch (refreshError) {
            clearAccessToken();
            if (window.location.pathname !== '/login') {
              window.location.href = '/login';
            }
            return Promise.reject(refreshError);
          }
        }

        return Promise.reject(this.handleError(error));
      },
    );
  }

  private async refreshToken(): Promise<string | null> {
    try {
      const response = await axios.post(
        `${env.API_URL}/api/v1/auth/refresh`,
        {},
        { withCredentials: true, timeout: 15_000 },
      );
      const { accessToken } = response.data;
      setAccessToken(accessToken);
      return accessToken;
    } catch (error) {
      if (import.meta.env.DEV) {
        console.error('Token refresh failed:', error);
      }
      return null;
    }
  }

  private handleError(error: AxiosError): ApiError {
    if (error.response) {
      const data = error.response.data as any;
      const detailStr = stringifyFastApiDetail(data?.detail);
      return {
        message: data?.message || detailStr || 'An error occurred',
        status: error.response.status,
        data,
        code: data?.code,
      };
    } else if (error.request) {
      return {
        message: `Network error. Please check your connection. URL: ${error.config?.baseURL}${error.config?.url}`,
        status: 0,
        code: 'NETWORK_ERROR',
      };
    }
    return {
      message: error.message || 'An unexpected error occurred',
      status: 0,
      code: 'UNKNOWN_ERROR',
    };
  }

  public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get<T>(url, config).then((res) => res.data);
  }

  public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config).then((res) => res.data);
  }

  /** Binary POST (e.g. .docx). On 4xx/5xx, attempts to read JSON or text from the error body. */
  public postArrayBuffer(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ArrayBuffer> {
    const decodeErrorBody = (raw: ArrayBuffer, status: number) => {
      const text = new TextDecoder().decode(new Uint8Array(raw));
      let message = `HTTP ${status}`;
      try {
        const j = JSON.parse(text) as { detail?: unknown; message?: string };
        const d = j.detail;
        if (typeof d === 'string') message = d;
        else if (Array.isArray(d) && d[0] && typeof (d[0] as { msg?: string }).msg === 'string') {
          message = (d[0] as { msg: string }).msg;
        } else if (j.message) message = j.message;
      } catch {
        if (text.trim()) message = text;
      }
      return new ApiArrayBufferRequestError(message, status);
    };
    return this.client
      .post(url, data, { ...config, responseType: 'arraybuffer' })
      .then((res) => {
        if (res.status >= 400) {
          return Promise.reject(decodeErrorBody(res.data as ArrayBuffer, res.status));
        }
        return res.data as ArrayBuffer;
      })
      .catch((err: unknown) => {
        const ax = err as { response?: { status?: number; data?: ArrayBuffer } };
        if (ax?.response?.data instanceof ArrayBuffer && ax.response?.status) {
          return Promise.reject(decodeErrorBody(ax.response.data, ax.response.status));
        }
        if (err && typeof err === 'object' && 'status' in err) {
          const ae = err as ApiError;
          if (typeof ae.status === 'number' && ae.status >= 400) {
            const msg =
              typeof ae.message === 'string' && ae.message.trim()
                ? ae.message
                : stringifyFastApiDetail(ae.data?.detail) || `HTTP ${ae.status}`;
            return Promise.reject(new ApiArrayBufferRequestError(msg, ae.status));
          }
        }
        return Promise.reject(err);
      });
  }

  public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put<T>(url, data, config).then((res) => res.data);
  }

  public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete<T>(url, config).then((res) => res.data);
  }

  public patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.patch<T>(url, data, config).then((res) => res.data);
  }
}

export const apiClient = new ApiClient();
