import { authUtils } from "./auth-utils";

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

// Allow undefined in params so callers can omit values
type ParamValue = string | number | boolean | undefined;
interface FetchOptions {
  headers?: Record<string, string>;
  params?: Record<string, ParamValue>;
  body?: any;
  /** Skip adding Authorization and refreshing the token */
  skipAuth?: boolean;
}

interface ApiError extends Error {
  status?: number;
  data?: any;
}

class HttpClient {
  private baseUrl = process.env.NEXT_PUBLIC_API_URL!;
  private isRefreshing = false;
  private refreshPromise: Promise<string | null> | null = null;

  /**
   * Remove undefined values and stringify others
   */
  private cleanParams(
    params?: Record<string, ParamValue>
  ): Record<string, string> {
    if (!params) return {};
    return Object.fromEntries(
      Object.entries(params)
        .filter(([, value]) => value !== undefined)
        .map(([key, value]) => [key, String(value)])
    );
  }

  private buildUrl(endpoint: string, params?: FetchOptions['params']) {
    const url = new URL(endpoint, this.baseUrl);
    if (params) {
      const clean = this.cleanParams(params);
      Object.entries(clean).forEach(([k, v]) => {
        url.searchParams.append(k, v);
      });
    }
    return url.toString();
  }

  private async rawRequest<T>(
    method: HttpMethod,
    endpoint: string,
    opts: FetchOptions = {}
  ) {
    const { headers = {}, params, body } = opts;
    const res = await fetch(this.buildUrl(endpoint, params), {
      method,
      headers: {
        'Content-Type': 'application/json',
        'Accept-Language': 'en',
        ...headers,
      },
      ...(method !== 'GET' && { body: JSON.stringify(body) }),
    });

    if (!res.ok) {
      const errData = await res.json().catch(() => ({}));
      const error: ApiError = new Error(
        errData.error || errData.message || 'Fetch failed'
      );
      error.status = res.status;
      error.data = errData;
      throw error;
    }
    return res.json() as Promise<T>;
  }

  private async refreshTokenIfNeeded(): Promise<string | null> {
    if (this.isRefreshing && this.refreshPromise) return this.refreshPromise;

    const refresh = authUtils.getRefreshToken();
    if (!refresh) return null;

    this.isRefreshing = true;
    this.refreshPromise = (async () => {
      try {
        const { access } = await this.rawRequest<{ access: string }>(
          'POST',
          '/api/accounts/refresh-token/',
          { body: { refresh } }
        );
        authUtils.setTokens(access, refresh);
        return access;
      } catch {
        authUtils.clearTokens();
        window.location.href = '/login';
        return null;
      } finally {
        this.isRefreshing = false;
        this.refreshPromise = null;
      }
    })();

    return this.refreshPromise;
  }

  public async request<T>(
    method: HttpMethod,
    endpoint: string,
    options: FetchOptions = {}
  ): Promise<T> {
    // Skip auth if requested
    if (options.skipAuth) {
      return this.rawRequest<T>(method, endpoint, options);
    }

    let token = authUtils.getAccessToken();
    if (token && authUtils.isTokenExpired(token)) {
      token = await this.refreshTokenIfNeeded();
    }

    const headers = {
      ...options.headers,
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    };

    try {
      return await this.rawRequest<T>(method, endpoint, {
        ...options,
        headers,
      });
    } catch (err: any) {
      // Retry once on 401 if not already refreshing
      if (err.status === 401 && !this.isRefreshing) {
        const newToken = await this.refreshTokenIfNeeded();
        if (newToken) {
          const retryHeaders = { ...options.headers, Authorization: `Bearer ${newToken}` };
          return this.rawRequest<T>(method, endpoint, { ...options, headers: retryHeaders });
        }
      }
      throw err;
    }
  }

  // Convenience methods
  get = <T>(ep: string, opts?: FetchOptions) => this.request<T>('GET', ep, opts);
  post = <T>(ep: string, opts?: FetchOptions) => this.request<T>('POST', ep, opts);
  put = <T>(ep: string, opts?: FetchOptions) => this.request<T>('PUT', ep, opts);
  patch = <T>(ep: string, opts?: FetchOptions) => this.request<T>('PATCH', ep, opts);
  delete = <T>(ep: string, opts?: FetchOptions) => this.request<T>('DELETE', ep, opts);
}

export const apiClient = new HttpClient();
