/**
 * Core HTTP client for React Query integration
 * 
 * Architecture:
 * - Axios: HTTP client for making requests
 * - This Client: Handles auth tokens, tenant context, and error formatting
 * - Services: Business logic that uses this client
 * - React Query Hooks: Data fetching layer that uses services
 * 
 * React Query doesn't replace HTTP clients - it manages data fetching/caching/sync
 */
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { getApiUrl } from '../../config/runtime';

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

export interface SubscriptionError {
  error: string;
  message: string;
  code: string;
  details: {
    required_feature: string;
    required_level: string;
    current_plan: string;
    upgrade_url: string;
  };
}

// Subscription error event emitter
class SubscriptionErrorEmitter extends EventTarget {
  emitSubscriptionError(error: SubscriptionError) {
    this.dispatchEvent(new CustomEvent('subscriptionError', { detail: error }));
  }
}

export const subscriptionErrorEmitter = new SubscriptionErrorEmitter();

class ApiClient {
  private client: AxiosInstance;
  private baseURL: string;
  private authToken: string | null = null;
  private tenantId: string | null = null;
  private demoSessionId: string | null = null;
  private refreshToken: string | null = null;
  private isRefreshing: boolean = false;
  private refreshAttempts: number = 0;
  private maxRefreshAttempts: number = 3;
  private lastRefreshAttempt: number = 0;
  private failedQueue: Array<{
    resolve: (value?: any) => void;
    reject: (error?: any) => void;
    config: AxiosRequestConfig;
  }> = [];

  constructor(baseURL: string = getApiUrl() + '/v1') {
    this.baseURL = baseURL;
    
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor to add auth headers
    this.client.interceptors.request.use(
      (config) => {
        // Public endpoints that don't require authentication
        const publicEndpoints = [
          '/demo/accounts',
          '/demo/session/create',
          '/public/contact',
          '/public/feedback',
          '/public/prelaunch-subscribe',
          '/plans',  // Subscription plans metadata - public for registration flow
        ];

        // Endpoints that require authentication but not a tenant ID (user-level endpoints)
        const noTenantEndpoints = [
          '/auth/users/',           // User profile endpoints - user-level, no tenant context
          '/auth/register',         // Registration
          '/auth/login',            // Login
          '/geocoding',             // Geocoding/address search - utility service, no tenant context
          '/tenants/register',      // Tenant registration - creating new tenant, no existing tenant context
        ];

        // Additional public endpoints that don't require authentication at all (including registration)
        const publicAuthEndpoints = [
          '/auth/start-registration',    // Registration step 1 - SetupIntent creation
          '/auth/complete-registration', // Registration step 2 - Completion after 3DS
          '/auth/verify-email',          // Email verification
        ];

        const isPublicEndpoint = publicEndpoints.some(endpoint =>
          config.url?.includes(endpoint)
        );

        const isPublicAuthEndpoint = publicAuthEndpoints.some(endpoint =>
          config.url?.includes(endpoint)
        );

        const isNoTenantEndpoint = noTenantEndpoints.some(endpoint =>
          config.url?.includes(endpoint)
        );

        // Check demo session ID from memory OR localStorage
        const demoSessionId = this.demoSessionId || localStorage.getItem('demo_session_id');
        const isDemoMode = !!demoSessionId;

        // Only add auth token for non-public endpoints
        if (this.authToken && !isPublicEndpoint && !isPublicAuthEndpoint) {
          config.headers.Authorization = `Bearer ${this.authToken}`;
          console.log('🔑 [API Client] Adding Authorization header for:', config.url);
        } else if (!isPublicEndpoint && !isPublicAuthEndpoint && !isDemoMode) {
          // Only warn if NOT in demo mode - demo mode uses X-Demo-Session-Id header instead
          console.warn('⚠️ [API Client] No auth token available for:', config.url, 'authToken:', this.authToken ? 'exists' : 'missing');
        }

        // Add tenant ID only for endpoints that require it
        if (this.tenantId && !isPublicEndpoint && !isPublicAuthEndpoint && !isNoTenantEndpoint) {
          config.headers['X-Tenant-ID'] = this.tenantId;
          console.log('🔍 [API Client] Adding X-Tenant-ID header:', this.tenantId, 'for URL:', config.url);
        } else if (!isPublicEndpoint && !isPublicAuthEndpoint && !isNoTenantEndpoint) {
          console.warn('⚠️ [API Client] No tenant ID set for endpoint:', config.url);
        }

        // Add demo session ID header if in demo mode
        if (demoSessionId) {
          config.headers['X-Demo-Session-Id'] = demoSessionId;
          console.log('🔍 [API Client] Adding X-Demo-Session-Id header:', demoSessionId);
        }

        return config;
      },
      (error) => {
        return Promise.reject(this.handleError(error));
      }
    );

    // Response interceptor for error handling and automatic token refresh
    this.client.interceptors.response.use(
      (response) => {
        // Enhanced logging for token refresh header detection
        const refreshSuggested = response.headers['x-token-refresh-suggested'];
        if (refreshSuggested) {
          console.log('🔍 TOKEN REFRESH HEADER DETECTED:', {
            url: response.config?.url,
            method: response.config?.method,
            status: response.status,
            refreshSuggested,
            hasRefreshToken: !!this.refreshToken,
            currentTokenLength: this.authToken?.length || 0
          });
        }

        // Check if server suggests token refresh
        if (refreshSuggested === 'true' && this.refreshToken) {
          console.log('🔄 Server suggests token refresh - refreshing proactively');
          this.proactiveTokenRefresh();
        }
        return response;
      },
      async (error) => {
        const originalRequest = error.config;

        // Check if error is 401 and we have a refresh token
        if (error.response?.status === 401 && this.refreshToken && !originalRequest._retry) {
          // Check if we've exceeded max refresh attempts in a short time
          const now = Date.now();
          if (this.refreshAttempts >= this.maxRefreshAttempts && (now - this.lastRefreshAttempt) < 30000) {
            console.log('Max refresh attempts exceeded, logging out');
            await this.handleAuthFailure();
            return Promise.reject(this.handleError(error));
          }

          if (this.isRefreshing) {
            // If already refreshing, queue this request
            return new Promise((resolve, reject) => {
              this.failedQueue.push({ resolve, reject, config: originalRequest });
            });
          }

          originalRequest._retry = true;
          this.isRefreshing = true;
          this.refreshAttempts++;
          this.lastRefreshAttempt = now;

          try {
            console.log(`Attempting token refresh (attempt ${this.refreshAttempts})...`);

            // Attempt to refresh the token
            const response = await this.client.post('/auth/refresh', {
              refresh_token: this.refreshToken
            });

            const { access_token, refresh_token } = response.data;

            console.log('Token refresh successful');

            // Reset refresh attempts on success
            this.refreshAttempts = 0;

            // Update tokens
            this.setAuthToken(access_token);
            if (refresh_token) {
              this.setRefreshToken(refresh_token);
            }

            // Update auth store if available
            await this.updateAuthStore(access_token, refresh_token);

            // Process failed queue
            this.processQueue(null, access_token);

            // Retry original request with new token
            originalRequest.headers.Authorization = `Bearer ${access_token}`;
            return this.client(originalRequest);

          } catch (refreshError) {
            console.error(`Token refresh failed (attempt ${this.refreshAttempts}):`, refreshError);
            // Refresh failed, clear tokens and redirect to login
            this.processQueue(refreshError, null);
            await this.handleAuthFailure();
            return Promise.reject(this.handleError(refreshError as AxiosError));
          } finally {
            this.isRefreshing = false;
          }
        }

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

  private handleError(error: AxiosError): ApiError {
    if (error.response) {
      // Server responded with error status
      const { status, data } = error.response;

      // Check for subscription errors
      if (status === 403 && (data as any)?.code === 'SUBSCRIPTION_UPGRADE_REQUIRED') {
        const subscriptionError = data as SubscriptionError;
        subscriptionErrorEmitter.emitSubscriptionError(subscriptionError);
      }

      return {
        message: (data as any)?.detail || (data as any)?.message || `Request failed with status ${status}`,
        status,
        code: (data as any)?.code,
        details: data,
      };
    } else if (error.request) {
      // Network error
      return {
        message: 'Network error - please check your connection',
        status: 0,
      };
    } else {
      // Other error
      return {
        message: error.message || 'Unknown error occurred',
      };
    }
  }

  private processQueue(error: any, token: string | null = null) {
    this.failedQueue.forEach(({ resolve, reject, config }) => {
      if (error) {
        reject(error);
      } else {
        if (token) {
          config.headers = config.headers || {};
          config.headers.Authorization = `Bearer ${token}`;
        }
        resolve(this.client(config));
      }
    });

    this.failedQueue = [];
  }

  private async updateAuthStore(accessToken: string, refreshToken?: string) {
    try {
      // Dynamically import to avoid circular dependency
      const { useAuthStore } = await import('../../stores/auth.store');
      const { getSubscriptionFromJWT, getTenantAccessFromJWT, getPrimaryTenantIdFromJWT } = await import('../../utils/jwt');
      const setState = useAuthStore.setState;

      // CRITICAL: Extract fresh subscription data from new JWT
      const jwtSubscription = getSubscriptionFromJWT(accessToken);
      const jwtTenantAccess = getTenantAccessFromJWT(accessToken);
      const primaryTenantId = getPrimaryTenantIdFromJWT(accessToken);

      // Update the store with new tokens AND subscription data
      setState(state => ({
        ...state,
        token: accessToken,
        refreshToken: refreshToken || state.refreshToken,
        // IMPORTANT: Update subscription from fresh JWT
        jwtSubscription,
        jwtTenantAccess,
        primaryTenantId,
      }));

      console.log('✅ Auth store updated with new token and subscription:', jwtSubscription?.tier);

      // Broadcast change to all Zustand subscribers
      console.log('📢 Zustand state updated - all useJWTSubscription() hooks will re-render');
    } catch (error) {
      console.warn('Failed to update auth store:', error);
    }
  }

  private async proactiveTokenRefresh() {
    // Avoid multiple simultaneous proactive refreshes
    if (this.isRefreshing) {
      return;
    }

    try {
      this.isRefreshing = true;
      console.log('🔄 Proactively refreshing token...');

      const response = await this.client.post('/auth/refresh', {
        refresh_token: this.refreshToken
      });

      const { access_token, refresh_token } = response.data;

      // Update tokens
      this.setAuthToken(access_token);
      if (refresh_token) {
        this.setRefreshToken(refresh_token);
      }

      // Update auth store
      await this.updateAuthStore(access_token, refresh_token);

      console.log('✅ Proactive token refresh successful');
    } catch (error) {
      console.warn('⚠️ Proactive token refresh failed:', error);
      // Don't handle as auth failure here - let the next 401 handle it
    } finally {
      this.isRefreshing = false;
    }
  }

  private async handleAuthFailure() {
    try {
      // Clear tokens
      this.setAuthToken(null);
      this.setRefreshToken(null);

      // Dynamically import to avoid circular dependency
      const { useAuthStore } = await import('../../stores/auth.store');
      const store = useAuthStore.getState();

      // Logout user
      store.logout();

      // Redirect to login if not already there
      if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
        window.location.href = '/login';
      }
    } catch (error) {
      console.warn('Failed to handle auth failure:', error);
    }
  }

  // Configuration methods
  setAuthToken(token: string | null) {
    console.log('🔧 [API Client] setAuthToken called:', token ? `${token.substring(0, 20)}...` : 'null');
    this.authToken = token;
    console.log('✅ [API Client] authToken is now:', this.authToken ? 'set' : 'null');
  }

  setRefreshToken(token: string | null) {
    this.refreshToken = token;
  }

  setTenantId(tenantId: string | null) {
    console.log('🔧 [API Client] setTenantId called with:', tenantId);
    console.log('🔧 [API Client] Previous tenantId was:', this.tenantId);
    console.trace('📍 [API Client] setTenantId call stack:');
    this.tenantId = tenantId;
    console.log('✅ [API Client] tenantId is now:', this.tenantId);
  }

  setDemoSessionId(sessionId: string | null) {
    this.demoSessionId = sessionId;
    if (sessionId) {
      localStorage.setItem('demo_session_id', sessionId);
    } else {
      localStorage.removeItem('demo_session_id');
    }
  }

  getDemoSessionId(): string | null {
    return this.demoSessionId || localStorage.getItem('demo_session_id');
  }

  getAuthToken(): string | null {
    return this.authToken;
  }

  getRefreshToken(): string | null {
    return this.refreshToken;
  }

  getTenantId(): string | null {
    return this.tenantId;
  }

  // Token synchronization methods for WebSocket connections
  getCurrentValidToken(): string | null {
    return this.authToken;
  }

  async ensureValidToken(): Promise<string | null> {
    const originalToken = this.authToken;
    const originalTokenShort = originalToken ? `${originalToken.slice(0, 20)}...${originalToken.slice(-10)}` : 'null';

    console.log('🔍 ensureValidToken() called:', {
      hasToken: !!this.authToken,
      tokenPreview: originalTokenShort,
      isRefreshing: this.isRefreshing,
      hasRefreshToken: !!this.refreshToken
    });

    // If we have a valid token, return it
    if (this.authToken && !this.isTokenNearExpiry(this.authToken)) {
      const expiryInfo = this.getTokenExpiryInfo(this.authToken);
      console.log('✅ Token is valid, returning current token:', {
        tokenPreview: originalTokenShort,
        expiryInfo
      });
      return this.authToken;
    }

    // If token is near expiry or expired, try to refresh
    if (this.refreshToken && !this.isRefreshing) {
      console.log('🔄 Token needs refresh, attempting proactive refresh:', {
        reason: this.authToken ? 'near expiry' : 'no token',
        expiryInfo: this.authToken ? this.getTokenExpiryInfo(this.authToken) : 'N/A'
      });

      try {
        await this.proactiveTokenRefresh();
        const newTokenShort = this.authToken ? `${this.authToken.slice(0, 20)}...${this.authToken.slice(-10)}` : 'null';
        const tokenChanged = originalToken !== this.authToken;

        console.log('✅ Token refresh completed:', {
          tokenChanged,
          oldTokenPreview: originalTokenShort,
          newTokenPreview: newTokenShort,
          newExpiryInfo: this.authToken ? this.getTokenExpiryInfo(this.authToken) : 'N/A'
        });

        return this.authToken;
      } catch (error) {
        console.warn('❌ Failed to refresh token in ensureValidToken:', error);
        return null;
      }
    }

    console.log('⚠️ Returning current token without refresh:', {
      reason: this.isRefreshing ? 'already refreshing' : 'no refresh token',
      tokenPreview: originalTokenShort
    });
    return this.authToken;
  }

  private getTokenExpiryInfo(token: string): any {
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const exp = payload.exp;
      const iat = payload.iat;
      if (!exp) return { error: 'No expiry in token' };

      const now = Math.floor(Date.now() / 1000);
      const timeUntilExpiry = exp - now;
      const tokenLifetime = exp - iat;

      return {
        issuedAt: new Date(iat * 1000).toISOString(),
        expiresAt: new Date(exp * 1000).toISOString(),
        lifetimeMinutes: Math.floor(tokenLifetime / 60),
        secondsUntilExpiry: timeUntilExpiry,
        minutesUntilExpiry: Math.floor(timeUntilExpiry / 60),
        isNearExpiry: timeUntilExpiry < 300,
        isExpired: timeUntilExpiry <= 0
      };
    } catch (error) {
      return { error: 'Failed to parse token', details: error };
    }
  }

  private isTokenNearExpiry(token: string): boolean {
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const exp = payload.exp;
      if (!exp) return false;

      const now = Math.floor(Date.now() / 1000);
      const timeUntilExpiry = exp - now;

      // Consider token near expiry if less than 5 minutes remaining
      const isNear = timeUntilExpiry < 300;

      if (isNear) {
        console.log('⏰ Token is near expiry:', {
          secondsUntilExpiry: timeUntilExpiry,
          minutesUntilExpiry: Math.floor(timeUntilExpiry / 60),
          expiresAt: new Date(exp * 1000).toISOString()
        });
      }

      return isNear;
    } catch (error) {
      console.warn('Failed to parse token for expiry check:', error);
      return true; // Assume expired if we can't parse
    }
  }

  // HTTP Methods - Return direct data for React Query
  async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response: AxiosResponse<T> = await this.client.get(url, config);
    return response.data;
  }

  async post<T = any, D = any>(
    url: string,
    data?: D,
    config?: AxiosRequestConfig
  ): Promise<T> {
    const response: AxiosResponse<T> = await this.client.post(url, data, config);
    return response.data;
  }

  async put<T = any, D = any>(
    url: string,
    data?: D,
    config?: AxiosRequestConfig
  ): Promise<T> {
    const response: AxiosResponse<T> = await this.client.put(url, data, config);
    return response.data;
  }

  async patch<T = any, D = any>(
    url: string,
    data?: D,
    config?: AxiosRequestConfig
  ): Promise<T> {
    const response: AxiosResponse<T> = await this.client.patch(url, data, config);
    return response.data;
  }

  async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response: AxiosResponse<T> = await this.client.delete(url, config);
    return response.data;
  }

  // File upload helper
  async uploadFile<T = any>(
    url: string,
    file: File | FormData,
    config?: AxiosRequestConfig
  ): Promise<T> {
    const formData = file instanceof FormData ? file : new FormData();
    if (file instanceof File) {
      formData.append('file', file);
    }

    return this.post<T>(url, formData, {
      ...config,
      headers: {
        ...config?.headers,
        'Content-Type': 'multipart/form-data',
      },
    });
  }

  // Raw axios instance for advanced usage
  getAxiosInstance(): AxiosInstance {
    return this.client;
  }
}

// Create and export singleton instance
export const apiClient = new ApiClient();
export default apiClient;