/**
 * API Client Service
 *
 * Fetch wrapper with JWT token handling, automatic refresh,
 * and standardized error handling for the Headroom API.
 */

import { unwrapResponse } from '$lib/api/client';

const API_BASE_URL = import.meta.env.VITE_API_URL ?? '/api';
export const GENERIC_SERVER_ERROR_MESSAGE = 'An unexpected server error occurred. Please try again.';

// Token storage keys
const ACCESS_TOKEN_KEY = 'headroom_access_token';
const REFRESH_TOKEN_KEY = 'headroom_refresh_token';

// Token management
export function getAccessToken(): string | null {
  if (typeof localStorage === 'undefined') return null;
  return localStorage.getItem(ACCESS_TOKEN_KEY);
}

export function getRefreshToken(): string | null {
  if (typeof localStorage === 'undefined') return null;
  return localStorage.getItem(REFRESH_TOKEN_KEY);
}

export function isValidJwtFormat(token: string | null): boolean {
  if (!token) return false;
  const parts = token.split('.');
  if (parts.length !== 3) return false;
  return parts.every(part => part.length > 0);
}

export function isJwtExpired(token: string | null): boolean {
  if (!isValidJwtFormat(token)) return true;

  try {
    const payload = token!.split('.')[1];
    const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
    const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
    const decoded = atob(padded);
    const data = JSON.parse(decoded) as { exp?: number };

    if (typeof data.exp !== 'number') {
      return false;
    }

    return data.exp <= Math.floor(Date.now() / 1000);
  } catch {
    return true;
  }
}

export function setTokens(accessToken: string, refreshToken: string): void {
  if (typeof localStorage === 'undefined') return;
  localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
  localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
}

export function clearTokens(): void {
  if (typeof localStorage === 'undefined') return;
  localStorage.removeItem(ACCESS_TOKEN_KEY);
  localStorage.removeItem(REFRESH_TOKEN_KEY);
}

// API Error type
export class ApiError extends Error {
  status: number;
  data: unknown;

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

export function sanitizeApiErrorMessage(message?: string): string {
  if (!message) {
    return GENERIC_SERVER_ERROR_MESSAGE;
  }

  const normalized = message.toLowerCase();
  const containsHtml = ['<!doctype', '<html', '<body', '<head'].some((fragment) =>
    normalized.includes(fragment)
  );
  const containsSql = ['sqlstate', 'illuminate\\', 'stack trace'].some((fragment) =>
    normalized.includes(fragment)
  );

  return containsHtml || containsSql ? GENERIC_SERVER_ERROR_MESSAGE : message;
}

// Queue for requests waiting for token refresh
let isRefreshing = false;
let refreshSubscribers: Array<(token: string) => void> = [];

function subscribeTokenRefresh(callback: (token: string) => void): void {
  refreshSubscribers.push(callback);
}

function onTokenRefreshed(newToken: string): void {
  refreshSubscribers.forEach((callback) => {
    callback(newToken);
  });
  refreshSubscribers = [];
}

// Refresh token function
async function refreshAccessToken(): Promise<string> {
  const refreshToken = getRefreshToken();
  
  if (!refreshToken) {
    throw new Error('No refresh token available');
  }
  
  const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ refresh_token: refreshToken }),
  });
  
  if (!response.ok) {
    clearTokens();
    throw new Error('Token refresh failed');
  }
  
  const data = await response.json() as { access_token: string; refresh_token: string };
  setTokens(data.access_token, data.refresh_token);
  return data.access_token;
}

// API request options type
interface ApiRequestOptions {
  method?: string;
  headers?: Record<string, string>;
  body?: unknown;
  unwrap?: boolean;
}

// Main API request function
export async function apiRequest<T>(endpoint: string, options: ApiRequestOptions = {}): Promise<T> {
  // Ensure we have an absolute URL for server-side rendering
  let url = endpoint;
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
    // Get the base URL - works in both browser and server contexts
    const baseUrl = typeof window !== 'undefined' 
      ? '' 
      : process.env['ORIGIN'] || '';
    url = `${baseUrl}${API_BASE_URL}${endpoint}`;
  }
  
  // Prepare headers
  const headers: Record<string, string> = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...options.headers,
  };
  
  // Add auth token if available
  const token = getAccessToken();
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  
  // Prepare request options
  const requestOptions: RequestInit = {
    method: options.method,
    headers,
  };
  
  // Handle request body
  if (options.body !== undefined) {
    const body = options.body;

    const isJsonObject =
      typeof body === 'object' &&
      body !== null &&
      !(body instanceof FormData) &&
      !(body instanceof URLSearchParams) &&
      !(body instanceof Blob) &&
      !(body instanceof ArrayBuffer) &&
      !ArrayBuffer.isView(body);

    requestOptions.body = isJsonObject ? JSON.stringify(body) : (body as BodyInit);
  }
  
  try {
    let response = await fetch(url, requestOptions);
    
    // Handle token expiration
    if (response.status === 401 && token) {
      if (!isRefreshing) {
        isRefreshing = true;
        
        try {
          const newToken = await refreshAccessToken();
          isRefreshing = false;
          onTokenRefreshed(newToken);
        } catch (error) {
          isRefreshing = false;
          refreshSubscribers = [];
          clearTokens();
          if (typeof window !== 'undefined') {
            window.dispatchEvent(new CustomEvent('auth:logout'));
          }
          throw new ApiError('Session expired. Please log in again.', 401, null);
        }
      }
      
      // Wait for token refresh and retry request
      return new Promise((resolve, reject) => {
        subscribeTokenRefresh((newToken: string) => {
          requestOptions.headers = {
            ...requestOptions.headers,
            'Authorization': `Bearer ${newToken}`,
          };
          fetch(url, requestOptions)
            .then((res) => handleResponse(res))
            .then((res) => {
              if (options.unwrap === false) {
                return res.json() as Promise<T>;
              }

              return unwrapResponse<T>(res);
            })
            .then(resolve)
            .catch(reject);
        });
      });
    }
    
    const validated = await handleResponse(response);
    if (options.unwrap === false) {
      return validated.json() as Promise<T>;
    }

    return unwrapResponse<T>(validated);
  } catch (error) {
    throw error;
  }
}

// Handle API response
async function handleResponse(response: Response): Promise<Response> {
  const contentType =
    response.headers?.get?.('content-type') || response.headers?.get?.('Content-Type');
  const isJson = contentType && contentType.includes('application/json');

  if (!response.ok) {
    const payloadResponse = response.clone();
    const data = isJson ? await payloadResponse.json() : await payloadResponse.text();
    const errorData = typeof data === 'object' ? data : { message: data };
    const rawMessage = (errorData as { message?: string }).message || 'API request failed';
    const message = sanitizeApiErrorMessage(rawMessage);
    console.error('API error', {
      url: response.url,
      status: response.status,
      message,
      data: errorData
    });
    throw new ApiError(message, response.status, errorData);
  }

  return response;
}

// Convenience methods
export const api = {
  get: <T>(endpoint: string, options: ApiRequestOptions = {}) => 
    apiRequest<T>(endpoint, { ...options, method: 'GET' }),
  post: <T>(endpoint: string, body?: unknown, options: ApiRequestOptions = {}) =>
    apiRequest<T>(endpoint, body === undefined ? { ...options, method: 'POST' } : { ...options, method: 'POST', body }),
  put: <T>(endpoint: string, body?: unknown, options: ApiRequestOptions = {}) =>
    apiRequest<T>(endpoint, body === undefined ? { ...options, method: 'PUT' } : { ...options, method: 'PUT', body }),
  patch: <T>(endpoint: string, body?: unknown, options: ApiRequestOptions = {}) =>
    apiRequest<T>(endpoint, body === undefined ? { ...options, method: 'PATCH' } : { ...options, method: 'PATCH', body }),
  delete: <T>(endpoint: string, options: ApiRequestOptions = {}) => 
    apiRequest<T>(endpoint, { ...options, method: 'DELETE' }),
};

// Login credentials type
interface LoginCredentials {
  email: string;
  password: string;
}

// Login response type
interface AuthPayload {
  id: string;
  name: string;
  email: string;
  role: 'superuser' | 'manager' | 'developer' | 'top_brass';
  active: boolean;
  created_at: string;
  updated_at: string;
}

interface LoginResponse {
  access_token: string;
  refresh_token: string;
  token_type: 'bearer';
  expires_in: number;
  data: AuthPayload;
}

// Auth-specific API methods
export const authApi = {
  login: (credentials: LoginCredentials) => 
    api.post<LoginResponse>('/auth/login', credentials, { unwrap: false }),
  logout: () => api.post<void>('/auth/logout'),
  refresh: () => api.post<LoginResponse>('/auth/refresh', { refresh_token: getRefreshToken() }, { unwrap: false }),
};

export default api;
