"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.HttpService = void 0;
var _cache = require("../utils/cache");
var _requestUtils = require("../utils/requestUtils");
var _asyncUtils = require("../utils/asyncUtils");
var _errorUtils = require("../utils/errorUtils");
var _jwtDecode = require("jwt-decode");
/**
 * Unified HTTP Service
 * 
 * Consolidates HttpClient + RequestManager into a single efficient class.
 * Uses native fetch instead of axios for smaller bundle size.
 * 
 * Handles:
 * - Authentication (token management, auto-refresh)
 * - Caching (TTL-based)
 * - Deduplication (concurrent requests)
 * - Retry logic
 * - Error handling
 * - Request queuing
 */

/**
 * Token store for authentication (singleton)
 */
class TokenStore {
  accessToken = null;
  refreshToken = null;
  constructor() {}
  static getInstance() {
    if (!TokenStore.instance) {
      TokenStore.instance = new TokenStore();
    }
    return TokenStore.instance;
  }
  setTokens(accessToken, refreshToken = '') {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
  }
  getAccessToken() {
    return this.accessToken;
  }
  getRefreshToken() {
    return this.refreshToken;
  }
  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
  }
  hasAccessToken() {
    return !!this.accessToken;
  }
}

/**
 * Unified HTTP Service
 * 
 * Consolidates HttpClient + RequestManager into a single efficient class.
 * Uses native fetch instead of axios for smaller bundle size.
 */
class HttpService {
  // Performance monitoring
  requestMetrics = {
    totalRequests: 0,
    successfulRequests: 0,
    failedRequests: 0,
    cacheHits: 0,
    cacheMisses: 0,
    averageResponseTime: 0
  };
  constructor(config) {
    this.config = config;
    this.baseURL = config.baseURL;
    this.tokenStore = TokenStore.getInstance();
    this.logger = new _requestUtils.SimpleLogger(config.enableLogging || false, config.logLevel || 'error', 'HttpService');

    // Initialize performance infrastructure
    this.cache = new _cache.TTLCache(config.cacheTTL || 5 * 60 * 1000);
    (0, _cache.registerCacheForCleanup)(this.cache);
    this.deduplicator = new _requestUtils.RequestDeduplicator();
    this.requestQueue = new _requestUtils.RequestQueue(config.maxConcurrentRequests || 10, config.requestQueueSize || 100);
  }

  /**
   * Main request method - handles everything in one place
   */
  async request(config) {
    const {
      method,
      url,
      data,
      params,
      timeout = this.config.requestTimeout || 5000,
      signal,
      cache = method === 'GET',
      cacheTTL,
      deduplicate = true,
      retry = this.config.enableRetry !== false,
      maxRetries = this.config.maxRetries || 3
    } = config;

    // Generate cache key (optimized for large objects)
    const cacheKey = cache ? this.generateCacheKey(method, url, data || params) : null;

    // Check cache first
    if (cache && cacheKey) {
      const cached = this.cache.get(cacheKey);
      if (cached !== null) {
        this.requestMetrics.cacheHits++;
        this.logger.debug('Cache hit:', url);
        return cached;
      }
      this.requestMetrics.cacheMisses++;
    }

    // Request function
    const requestFn = async () => {
      const startTime = Date.now();
      try {
        // Build URL with params
        const fullUrl = this.buildURL(url, params);

        // Get auth token (with auto-refresh)
        const authHeader = await this.getAuthHeader();

        // Determine if data is FormData
        const isFormData = data instanceof FormData;

        // Make fetch request
        const controller = new AbortController();
        const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
        if (signal) {
          signal.addEventListener('abort', () => controller.abort());
        }

        // Build headers
        const headers = {
          'Accept': 'application/json'
        };

        // Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
        if (!isFormData) {
          headers['Content-Type'] = 'application/json';
        }
        if (authHeader) {
          headers['Authorization'] = authHeader;
        }
        const response = await fetch(fullUrl, {
          method,
          headers,
          body: method !== 'GET' && data ? isFormData ? data : JSON.stringify(data) : undefined,
          signal: controller.signal
        });
        if (timeoutId) clearTimeout(timeoutId);

        // Handle response
        if (!response.ok) {
          if (response.status === 401) {
            this.tokenStore.clearTokens();
          }

          // Try to parse error response (handle empty/malformed JSON)
          let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
          const contentType = response.headers.get('content-type');
          if (contentType && contentType.includes('application/json')) {
            try {
              const errorData = await response.json();
              if (errorData?.message) {
                errorMessage = errorData.message;
              }
            } catch (parseError) {
              // Malformed JSON or empty response - use status text
              this.logger.warn('Failed to parse error response JSON:', parseError);
            }
          }
          const error = new Error(errorMessage);
          error.status = response.status;
          error.response = {
            status: response.status,
            statusText: response.statusText
          };
          throw error;
        }

        // Handle different response types (optimized - read response once)
        const contentType = response.headers.get('content-type');
        let responseData;
        if (contentType && contentType.includes('application/json')) {
          // Use response.json() directly for better performance
          try {
            responseData = await response.json();
            // Handle null/undefined responses
            if (responseData === null || responseData === undefined) {
              responseData = null;
            } else {
              // Unwrap standardized API response format for JSON
              responseData = this.unwrapResponse(responseData);
            }
          } catch (parseError) {
            // Handle malformed JSON or empty responses gracefully
            // Note: Once response.json() is called, the body is consumed and cannot be read again
            // So we check the error type to determine if it's empty or malformed
            if (parseError instanceof SyntaxError) {
              this.logger.warn('Failed to parse JSON response (malformed or empty):', parseError);
              // SyntaxError typically means empty or malformed JSON
              // For empty responses, return null; for malformed JSON, throw descriptive error
              responseData = null; // Treat as empty response for safety
            } else {
              this.logger.warn('Failed to read response:', parseError);
              throw new Error('Failed to read response from server');
            }
          }
        } else if (contentType && (contentType.includes('application/octet-stream') || contentType.includes('image/') || contentType.includes('video/') || contentType.includes('audio/'))) {
          // For binary responses (blobs), return the blob directly without unwrapping
          responseData = await response.blob();
        } else {
          // For other responses, return as text
          const text = await response.text();
          responseData = text || null;
        }
        const duration = Date.now() - startTime;
        this.updateMetrics(true, duration);
        this.config.onRequestEnd?.(url, method, duration, true);
        return responseData;
      } catch (error) {
        const duration = Date.now() - startTime;
        this.updateMetrics(false, duration);
        this.config.onRequestEnd?.(url, method, duration, false);
        this.config.onRequestError?.(url, method, error instanceof Error ? error : new Error(String(error)));

        // Handle AbortError specifically for better error messages
        if (error instanceof Error && error.name === 'AbortError') {
          throw (0, _errorUtils.handleHttpError)(error);
        }
        throw (0, _errorUtils.handleHttpError)(error);
      }
    };

    // Wrap with retry if enabled
    const requestWithRetry = retry ? () => (0, _asyncUtils.retryAsync)(requestFn, maxRetries, this.config.retryDelay || 1000) : requestFn;

    // Wrap with deduplication if enabled (use optimized key generation)
    const dedupeKey = deduplicate ? this.generateCacheKey(method, url, data || params) : null;
    const finalRequest = dedupeKey ? () => this.deduplicator.deduplicate(dedupeKey, requestWithRetry) : requestWithRetry;

    // Execute request (with queue if needed)
    const result = await this.requestQueue.enqueue(finalRequest);

    // Cache the result if caching is enabled
    if (cache && cacheKey && result) {
      this.cache.set(cacheKey, result, cacheTTL);
    }
    return result;
  }

  /**
   * Generate cache key efficiently
   * Uses simple hash for large objects to avoid expensive JSON.stringify
   */
  generateCacheKey(method, url, data) {
    if (!data || typeof data === 'object' && Object.keys(data).length === 0) {
      return `${method}:${url}`;
    }

    // For small objects, use JSON.stringify
    const dataStr = JSON.stringify(data);
    if (dataStr.length < 1000) {
      return `${method}:${url}:${dataStr}`;
    }

    // For large objects, use a simple hash based on keys and values length
    // This avoids expensive serialization while still being unique enough
    const hash = typeof data === 'object' && data !== null ? Object.keys(data).sort().join(',') + ':' + dataStr.length : String(data).substring(0, 100);
    return `${method}:${url}:${hash}`;
  }

  /**
   * Build full URL with query params
   */
  buildURL(url, params) {
    const base = url.startsWith('http') ? url : `${this.baseURL}${url}`;
    if (!params || Object.keys(params).length === 0) {
      return base;
    }
    const searchParams = new URLSearchParams();
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null) {
        searchParams.append(key, String(value));
      }
    });
    const queryString = searchParams.toString();
    return queryString ? `${base}${base.includes('?') ? '&' : '?'}${queryString}` : base;
  }

  /**
   * Get auth header with automatic token refresh
   */
  async getAuthHeader() {
    const accessToken = this.tokenStore.getAccessToken();
    if (!accessToken) {
      return null;
    }
    try {
      const decoded = (0, _jwtDecode.jwtDecode)(accessToken);
      const currentTime = Math.floor(Date.now() / 1000);

      // If token expires in less than 60 seconds, refresh it
      if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
        try {
          const refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;

          // Use AbortSignal.timeout for consistent timeout handling
          const response = await fetch(refreshUrl, {
            method: 'GET',
            headers: {
              'Accept': 'application/json'
            },
            signal: AbortSignal.timeout(5000)
          });
          if (response.ok) {
            const {
              accessToken: newToken
            } = await response.json();
            this.tokenStore.setTokens(newToken);
            this.logger.debug('Token refreshed');
            return `Bearer ${newToken}`;
          }
        } catch (refreshError) {
          this.logger.warn('Token refresh failed, using current token');
        }
      }
      return `Bearer ${accessToken}`;
    } catch (error) {
      this.logger.error('Error processing token:', error);
      return `Bearer ${accessToken}`;
    }
  }

  /**
   * Unwrap standardized API response format
   */
  unwrapResponse(responseData) {
    // Handle paginated responses: { data: [...], pagination: {...} }
    if (responseData && typeof responseData === 'object' && 'data' in responseData && 'pagination' in responseData) {
      return responseData;
    }

    // Handle regular success responses: { data: ... }
    if (responseData && typeof responseData === 'object' && 'data' in responseData && !Array.isArray(responseData)) {
      return responseData.data;
    }

    // Return as-is for responses that don't use sendSuccess wrapper
    return responseData;
  }

  /**
   * Update request metrics
   */
  updateMetrics(success, duration) {
    this.requestMetrics.totalRequests++;
    if (success) {
      this.requestMetrics.successfulRequests++;
    } else {
      this.requestMetrics.failedRequests++;
    }
    const alpha = 0.1;
    this.requestMetrics.averageResponseTime = this.requestMetrics.averageResponseTime * (1 - alpha) + duration * alpha;
  }

  // Convenience methods (for backward compatibility)
  async get(url, config) {
    const result = await this.request({
      method: 'GET',
      url,
      ...config
    });
    return {
      data: result
    };
  }
  async post(url, data, config) {
    const result = await this.request({
      method: 'POST',
      url,
      data,
      ...config
    });
    return {
      data: result
    };
  }
  async put(url, data, config) {
    const result = await this.request({
      method: 'PUT',
      url,
      data,
      ...config
    });
    return {
      data: result
    };
  }
  async patch(url, data, config) {
    const result = await this.request({
      method: 'PATCH',
      url,
      data,
      ...config
    });
    return {
      data: result
    };
  }
  async delete(url, config) {
    const result = await this.request({
      method: 'DELETE',
      url,
      ...config
    });
    return {
      data: result
    };
  }

  // Token management
  setTokens(accessToken, refreshToken = '') {
    this.tokenStore.setTokens(accessToken, refreshToken);
  }
  clearTokens() {
    this.tokenStore.clearTokens();
  }
  getAccessToken() {
    return this.tokenStore.getAccessToken();
  }
  hasAccessToken() {
    return this.tokenStore.hasAccessToken();
  }
  getBaseURL() {
    return this.baseURL;
  }

  // Cache management
  clearCache() {
    this.cache.clear();
  }
  clearCacheEntry(key) {
    this.cache.delete(key);
  }
  getCacheStats() {
    const cacheStats = this.cache.getStats();
    const total = this.requestMetrics.cacheHits + this.requestMetrics.cacheMisses;
    return {
      size: cacheStats.size,
      hits: this.requestMetrics.cacheHits,
      misses: this.requestMetrics.cacheMisses,
      hitRate: total > 0 ? this.requestMetrics.cacheHits / total : 0
    };
  }
  getMetrics() {
    return {
      ...this.requestMetrics
    };
  }

  // Test-only utility
  static __resetTokensForTests() {
    try {
      TokenStore.getInstance().clearTokens();
    } catch (error) {
      // Silently fail in test cleanup - this is expected behavior
      // TokenStore might not be initialized in some test scenarios
    }
  }
}
exports.HttpService = HttpService;
//# sourceMappingURL=HttpService.js.map