import { Injectable, inject } from '@angular/core';
import {
  HttpClient,
  HttpParams,
  HttpHeaders,
  HttpEvent,
  HttpEventType,
  HttpRequest,
  HttpErrorResponse,
} from '@angular/common/http';
import {
  Observable,
  map,
  filter,
  timeout,
  retry,
  catchError,
  throwError,
  shareReplay,
  of,
} from 'rxjs';
import { RuntimeConfigService } from './runtime-config.service';

// Configuration constants
const DEFAULT_TIMEOUT_MS = 30000; // 30 seconds
const UPLOAD_TIMEOUT_MS = 300000; // 5 minutes for uploads
const MAX_RETRIES = 2;
const RETRY_DELAY_MS = 1000;

export interface UploadProgress<T> {
  progress: number;
  loaded: number;
  total: number;
  complete: boolean;
  response?: T;
}

export interface ApiResponse<T> {
  success: boolean;
  data: T;
  timestamp: string;
  message?: string;
}

export interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
  hasNextPage: boolean;
}

export interface RequestOptions {
  timeoutMs?: number;
  retries?: number;
  skipRetry?: boolean;
}

/**
 * Validates and sanitizes an ID parameter
 * @throws Error if ID is invalid
 */
function validateId(id: string | undefined | null, fieldName = 'ID'): string {
  if (id === undefined || id === null) {
    throw new Error(`${fieldName} is required`);
  }

  if (typeof id !== 'string') {
    throw new Error(`${fieldName} must be a string`);
  }

  const trimmedId = id.trim();

  if (trimmedId.length === 0) {
    throw new Error(`${fieldName} cannot be empty`);
  }

  // Check for dangerous characters that could lead to path traversal or injection
  if (/[\/\\<>|"'`;&$]/.test(trimmedId)) {
    throw new Error(`${fieldName} contains invalid characters`);
  }

  return trimmedId;
}

/**
 * Validates pagination parameters
 */
function validatePagination(page?: number, limit?: number): { page: number; limit: number } {
  const validPage = typeof page === 'number' && !isNaN(page) ? Math.max(1, Math.floor(page)) : 1;
  const validLimit =
    typeof limit === 'number' && !isNaN(limit)
      ? Math.min(100, Math.max(1, Math.floor(limit)))
      : 10;

  return { page: validPage, limit: validLimit };
}

/**
 * Safely extracts data from API response with null checks
 */
function extractData<T>(response: ApiResponse<T> | null | undefined): T {
  if (!response) {
    throw new Error('Empty response received from server');
  }

  // Handle case where response itself is the data (no wrapper)
  if (!('data' in response) && !('success' in response)) {
    return response as unknown as T;
  }

  // Handle paginated responses: have 'data' and pagination fields but no 'success'
  // These should be returned as-is, not unwrapped
  if ('data' in response && !('success' in response) && ('total' in response || 'page' in response)) {
    return response as unknown as T;
  }

  if (response.data === undefined) {
    // Return null as T if data is explicitly undefined but response exists
    return null as T;
  }

  return response.data;
}

/**
 * Determines if an error is retryable
 */
function isRetryableError(error: HttpErrorResponse): boolean {
  // Network errors (status 0) or server errors (5xx) are retryable
  // 429 (rate limiting) is also retryable
  return error.status === 0 || error.status === 429 || (error.status >= 500 && error.status < 600);
}

@Injectable({
  providedIn: 'root',
})
export class ApiService {
  private readonly http = inject(HttpClient);
  private readonly configService = inject(RuntimeConfigService);

  /**
   * Get API base URL from runtime config (supports deployment-time configuration)
   */
  private get baseUrl(): string {
    return this.configService.apiBaseUrl;
  }

  /**
   * Cache for GET requests that should be shared
   */
  private readonly cache = new Map<string, Observable<unknown>>();

  get<T>(
    path: string,
    params?: Record<string, string | number | boolean | undefined | null>,
    options?: RequestOptions
  ): Observable<T> {
    let httpParams = new HttpParams();

    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== '') {
          httpParams = httpParams.set(key, String(value));
        }
      });
    }

    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const retries = options?.skipRetry ? 0 : (options?.retries ?? MAX_RETRIES);

    return this.http.get<ApiResponse<T>>(`${this.baseUrl}${path}`, { params: httpParams }).pipe(
      timeout(timeoutMs),
      retry({
        count: retries,
        delay: (error, retryCount) => {
          if (isRetryableError(error)) {
            return of(null).pipe(
              // Exponential backoff
              timeout(RETRY_DELAY_MS * Math.pow(2, retryCount - 1))
            );
          }
          return throwError(() => error);
        },
      }),
      map((response) => extractData<T>(response)),
      catchError((error) => this.handleError(error, path))
    );
  }

  /**
   * GET with caching support using shareReplay
   * Useful for frequently accessed, rarely changing data
   */
  getCached<T>(
    path: string,
    params?: Record<string, string | number | boolean>,
    cacheTimeMs = 60000
  ): Observable<T> {
    const cacheKey = `${path}?${JSON.stringify(params || {})}`;

    if (!this.cache.has(cacheKey)) {
      const request$ = this.get<T>(path, params).pipe(
        shareReplay({ bufferSize: 1, refCount: true })
      );

      this.cache.set(cacheKey, request$);

      // Clear cache after specified time
      setTimeout(() => this.cache.delete(cacheKey), cacheTimeMs);
    }

    return this.cache.get(cacheKey) as Observable<T>;
  }

  /**
   * Clear the cache for a specific path or all cache
   */
  clearCache(path?: string): void {
    if (path) {
      // Clear all cache entries that start with this path
      for (const key of this.cache.keys()) {
        if (key.startsWith(path)) {
          this.cache.delete(key);
        }
      }
    } else {
      this.cache.clear();
    }
  }

  getRaw<T>(
    path: string,
    params?: Record<string, string | number | boolean>,
    options?: RequestOptions
  ): Observable<T> {
    let httpParams = new HttpParams();

    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null) {
          httpParams = httpParams.set(key, String(value));
        }
      });
    }

    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

    return this.http.get<T>(`${this.baseUrl}${path}`, { params: httpParams }).pipe(
      timeout(timeoutMs),
      catchError((error) => this.handleError(error, path))
    );
  }

  post<T>(path: string, body: unknown, options?: RequestOptions): Observable<T> {
    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

    return this.http.post<ApiResponse<T>>(`${this.baseUrl}${path}`, body ?? {}).pipe(
      timeout(timeoutMs),
      map((response) => extractData<T>(response)),
      catchError((error) => this.handleError(error, path))
    );
  }

  postRaw<T>(path: string, body: unknown, options?: RequestOptions): Observable<T> {
    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

    return this.http.post<T>(`${this.baseUrl}${path}`, body ?? {}).pipe(
      timeout(timeoutMs),
      catchError((error) => this.handleError(error, path))
    );
  }

  put<T>(path: string, body: unknown, options?: RequestOptions): Observable<T> {
    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

    return this.http.put<ApiResponse<T>>(`${this.baseUrl}${path}`, body ?? {}).pipe(
      timeout(timeoutMs),
      map((response) => extractData<T>(response)),
      catchError((error) => this.handleError(error, path))
    );
  }

  patch<T>(path: string, body: unknown, options?: RequestOptions): Observable<T> {
    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

    return this.http.patch<ApiResponse<T>>(`${this.baseUrl}${path}`, body ?? {}).pipe(
      timeout(timeoutMs),
      map((response) => extractData<T>(response)),
      catchError((error) => this.handleError(error, path))
    );
  }

  delete<T>(path: string, options?: RequestOptions): Observable<T> {
    const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

    return this.http.delete<ApiResponse<T>>(`${this.baseUrl}${path}`).pipe(
      timeout(timeoutMs),
      map((response) => extractData<T>(response)),
      catchError((error) => this.handleError(error, path))
    );
  }

  upload<T>(path: string, formData: FormData, options?: RequestOptions): Observable<T> {
    const timeoutMs = options?.timeoutMs ?? UPLOAD_TIMEOUT_MS;

    return this.http.post<ApiResponse<T>>(`${this.baseUrl}${path}`, formData).pipe(
      timeout(timeoutMs),
      map((response) => extractData<T>(response)),
      catchError((error) => this.handleError(error, path))
    );
  }

  /**
   * Upload with progress tracking
   * Returns an observable that emits upload progress and final response
   */
  uploadWithProgress<T>(
    path: string,
    formData: FormData,
    options?: RequestOptions
  ): Observable<UploadProgress<T>> {
    const timeoutMs = options?.timeoutMs ?? UPLOAD_TIMEOUT_MS;

    const req = new HttpRequest('POST', `${this.baseUrl}${path}`, formData, {
      reportProgress: true,
    });

    return this.http.request<ApiResponse<T>>(req).pipe(
      timeout(timeoutMs),
      map((event: HttpEvent<ApiResponse<T>>) => {
        switch (event.type) {
          case HttpEventType.UploadProgress: {
            const total = event.total ?? 0;
            const loaded = event.loaded ?? 0;
            const progress = total > 0 ? Math.round((loaded / total) * 100) : 0;
            return {
              progress,
              loaded,
              total,
              complete: false,
            } as UploadProgress<T>;
          }

          case HttpEventType.Response: {
            // Handle both wrapped ({data: ...}) and unwrapped responses
            const body = event.body;
            let responseData: T | undefined;
            if (body && typeof body === 'object' && 'data' in body) {
              responseData = (body as ApiResponse<T>).data;
            } else {
              responseData = body as T | undefined;
            }
            return {
              progress: 100,
              loaded: 1,
              total: 1,
              complete: true,
              response: responseData,
            } as UploadProgress<T>;
          }

          default:
            return {
              progress: 0,
              loaded: 0,
              total: 0,
              complete: false,
            } as UploadProgress<T>;
        }
      }),
      catchError((error) => this.handleError(error, path))
    );
  }

  download(path: string, options?: RequestOptions): Observable<Blob> {
    const timeoutMs = options?.timeoutMs ?? UPLOAD_TIMEOUT_MS; // Downloads can be large

    return this.http
      .get(`${this.baseUrl}${path}`, {
        responseType: 'blob',
      })
      .pipe(
        timeout(timeoutMs),
        catchError((error) => this.handleError(error, path))
      );
  }

  getBlob(url: string, options?: RequestOptions): Observable<Blob> {
    const timeoutMs = options?.timeoutMs ?? UPLOAD_TIMEOUT_MS;

    return this.http.get(url, { responseType: 'blob' }).pipe(
      timeout(timeoutMs),
      catchError((error) => this.handleError(error, url))
    );
  }

  /**
   * Centralized error handling
   */
  private handleError(error: unknown, context: string): Observable<never> {
    let message = 'An unexpected error occurred';

    if (error instanceof HttpErrorResponse) {
      if (error.status === 0) {
        message = 'Network error. Please check your connection.';
      } else if (error.error?.message) {
        message = error.error.message;
      } else {
        message = `Request failed: ${error.statusText || error.status}`;
      }
    } else if (error instanceof Error) {
      if (error.name === 'TimeoutError') {
        message = 'Request timed out. Please try again.';
      } else {
        message = error.message;
      }
    }

    console.error(`API Error [${context}]:`, error);
    return throwError(() => new Error(message));
  }
}

// Export utility functions for use in other services
export { validateId, validatePagination };
