/* eslint-disable @typescript-eslint/no-explicit-any -- Axios-compatible API requires matching axios's `any` signatures */
import { fetch } from "@tauri-apps/plugin-http";
import {
  shouldUseFastLocalTransport,
  fetchViaLocalProxy,
  markFastTransportUnavailable,
} from "@app/services/tauriLocalProxy";

/**
 * Tauri HTTP Client - wrapper around Tauri's native HTTP client
 * Provides axios-compatible API while bypassing CORS restrictions
 */

export interface TauriHttpResponse<T = any> {
  data: T;
  status: number;
  statusText: string;
  headers: Record<string, string>;
  config: TauriHttpRequestConfig;
}

export interface TauriHttpRequestConfig {
  url?: string;
  method?: string;
  baseURL?: string;
  headers?: Record<string, string>;
  params?: Record<string, string | number | boolean> | any;
  data?: any;
  timeout?: number;
  responseType?: "json" | "text" | "blob" | "arraybuffer";
  withCredentials?: boolean;
  // Custom properties for desktop
  operationName?: string;
  skipBackendReadyCheck?: boolean;
  skipAuthRedirect?: boolean;
  // Axios compatibility properties (ignored by Tauri HTTP)
  suppressErrorToast?: boolean;
  cancelToken?: any;
  signal?: AbortSignal;
}

export interface TauriHttpError extends Error {
  config?: TauriHttpRequestConfig;
  code?: string;
  request?: unknown;
  response?: TauriHttpResponse;
  isAxiosError: boolean;
  toJSON: () => object;
}

type RequestInterceptor = (
  config: TauriHttpRequestConfig,
) => Promise<TauriHttpRequestConfig> | TauriHttpRequestConfig;
type ResponseInterceptor<T = any> = (
  response: TauriHttpResponse<T>,
) => Promise<TauriHttpResponse<T>> | TauriHttpResponse<T>;
type ErrorInterceptor = (error: any) => Promise<any>;

interface Interceptors {
  request: {
    handlers: RequestInterceptor[];
    use: (
      onFulfilled: RequestInterceptor,
      onRejected?: ErrorInterceptor,
    ) => number;
  };
  response: {
    handlers: { fulfilled: ResponseInterceptor; rejected?: ErrorInterceptor }[];
    use: (
      onFulfilled: ResponseInterceptor,
      onRejected?: ErrorInterceptor,
    ) => number;
  };
}

class TauriHttpClient {
  public defaults: TauriHttpRequestConfig = {
    baseURL: "",
    headers: {
      "User-Agent": "StirlingPDF-Desktop/1.0 Tauri",
    },
    timeout: 120000,
    responseType: "json",
    withCredentials: false, // Desktop doesn't need credentials (backend has allowCredentials=false)
  };

  public interceptors: Interceptors = {
    request: {
      handlers: [],
      use: (
        onFulfilled: RequestInterceptor,
        _onRejected?: ErrorInterceptor,
      ) => {
        this.interceptors.request.handlers.push(onFulfilled);
        return this.interceptors.request.handlers.length - 1;
      },
    },
    response: {
      handlers: [],
      use: (
        onFulfilled: ResponseInterceptor,
        onRejected?: ErrorInterceptor,
      ) => {
        this.interceptors.response.handlers.push({
          fulfilled: onFulfilled,
          rejected: onRejected,
        });
        return this.interceptors.response.handlers.length - 1;
      },
    },
  };

  constructor(config?: TauriHttpRequestConfig) {
    if (config) {
      this.defaults = { ...this.defaults, ...config };
    }
  }

  private createError(
    message: string,
    config?: TauriHttpRequestConfig,
    code?: string,
    response?: TauriHttpResponse,
    originalError?: unknown,
  ): TauriHttpError {
    const error = new Error(message) as TauriHttpError;
    error.config = config;
    error.code = code;
    error.response = response;
    error.isAxiosError = true;
    error.toJSON = () => ({
      message: error.message,
      name: error.name,
      config: error.config,
      code: error.code,
    });

    // Log detailed error information for debugging
    console.error("[TauriHttpClient] Error details:", {
      message,
      code,
      url: config?.url,
      method: config?.method,
      status: response?.status,
      originalError:
        originalError instanceof Error
          ? {
              name: originalError.name,
              message: originalError.message,
              stack: originalError.stack,
            }
          : originalError,
    });

    return error;
  }

  private buildUrl(config: TauriHttpRequestConfig): string {
    let url = config.url || "";

    // If URL is already absolute, use it as-is
    if (url.startsWith("http://") || url.startsWith("https://")) {
      return url;
    }

    // Prepend baseURL if present
    const baseURL = config.baseURL || this.defaults.baseURL || "";
    if (baseURL) {
      url = baseURL + url;
    }

    // Add query parameters
    if (config.params && typeof config.params === "object") {
      const searchParams = new URLSearchParams();
      Object.entries(config.params as Record<string, unknown>).forEach(
        ([key, value]) => {
          if (value !== null && value !== undefined) {
            searchParams.append(key, String(value));
          }
        },
      );
      const queryString = searchParams.toString();
      if (queryString) {
        url += (url.includes("?") ? "&" : "?") + queryString;
      }
    }

    return url;
  }

  private async executeRequest<T = any>(
    config: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    // Merge with defaults
    const mergedConfig: TauriHttpRequestConfig = {
      ...this.defaults,
      ...config,
      headers: {
        ...this.defaults.headers,
        ...config.headers,
      },
    };

    // Run request interceptors
    let finalConfig = mergedConfig;
    for (const interceptor of this.interceptors.request.handlers) {
      finalConfig = await Promise.resolve(interceptor(finalConfig));
    }

    const url = this.buildUrl(finalConfig);
    const method = (finalConfig.method || "GET").toUpperCase();

    // Prepare request body and headers
    let body: BodyInit | undefined;
    const headers: Record<string, string> = { ...(finalConfig.headers || {}) };

    if (finalConfig.data) {
      if (finalConfig.data instanceof FormData) {
        // FormData can be passed directly
        body = finalConfig.data;
        // Drop any caller-supplied Content-Type so the native fetch generates
        // multipart/form-data WITH its boundary (matches axios's FormData
        // handling). A boundary-less "multipart/form-data" header makes the
        // server reject the body ("No multipart boundary parameter").
        for (const key of Object.keys(headers)) {
          if (key.toLowerCase() === "content-type") {
            delete headers[key];
          }
        }
      } else if (typeof finalConfig.data === "object") {
        // Serialize as JSON
        body = JSON.stringify(finalConfig.data);
        if (!headers["Content-Type"]) {
          headers["Content-Type"] = "application/json";
        }
      } else {
        body = String(finalConfig.data);
      }
    }

    try {
      // Convert withCredentials to fetch API's credentials option
      const credentials: RequestCredentials = finalConfig.withCredentials
        ? "include"
        : "omit";

      // Make the request using Tauri's native HTTP client (standard Fetch API)
      // Enable certificate bypass for HTTPS to handle missing intermediate certs and self-signed certs
      const fetchOptions: RequestInit & {
        danger?: {
          acceptInvalidCerts: boolean;
          acceptInvalidHostnames: boolean;
        };
      } = {
        method,
        headers,
        body,
        credentials,
        ...(finalConfig.signal ? { signal: finalConfig.signal } : {}),
      };

      // Always enable dangerous settings for HTTPS to allow connections to servers with:
      // - Missing intermediate certificates
      // - Self-signed certificates
      // - Certificate hostname mismatches
      if (url.startsWith("https://")) {
        fetchOptions.danger = {
          acceptInvalidCerts: true,
          acceptInvalidHostnames: true,
        };
      }

      // Fast path: for localhost PDF uploads/downloads, move the body as raw
      // bytes via the Rust proxy instead of plugin-http's number-array IPC.
      // Only binary localhost traffic qualifies (see shouldUseFastLocalTransport);
      // everything else — remote requests, JSON/GET calls, anything without a
      // PDF body — uses the unchanged plugin-http path. Falls back to plugin-http
      // automatically if the fast path throws, so behaviour is never worse.
      let response: Response;
      if (
        shouldUseFastLocalTransport(
          url,
          finalConfig.responseType,
          finalConfig.data,
        )
      ) {
        try {
          response = await fetchViaLocalProxy(
            url,
            method,
            headers,
            body,
            finalConfig.signal,
          );
        } catch (proxyError) {
          // A deliberate abort must propagate, not silently re-issue the request.
          if (
            proxyError instanceof DOMException &&
            proxyError.name === "AbortError"
          ) {
            throw proxyError;
          }
          // Fast path failed — trip the circuit breaker and fall back to plugin-http.
          markFastTransportUnavailable();
          console.warn(
            "[TauriHttpClient] local fast-path failed; reverting to plugin-http for this session",
            proxyError,
          );
          response = await fetch(url, fetchOptions);
        }
      } else {
        response = await fetch(url, fetchOptions);
      }

      // Convert Headers to plain object
      const responseHeaders: Record<string, string> = {};
      response.headers.forEach((value, key) => {
        responseHeaders[key] = value;
      });

      // Check for HTTP errors BEFORE reading the body so that a plain-text error
      // response from the server doesn't cause a JSON parse failure that gets
      // misreported as ERR_NETWORK.
      if (!response.ok) {
        // Read the body as text to surface the real server error message
        let errorBody: string;
        try {
          errorBody = await response.text();
        } catch {
          errorBody = "";
        }

        // Create more descriptive error messages based on status code
        let errorMessage =
          errorBody || `Request failed with status code ${response.status}`;
        let errorCode = "ERR_BAD_REQUEST";

        if (response.status === 401) {
          errorMessage = "Authentication failed - Invalid credentials";
          errorCode = "ERR_UNAUTHORIZED";
        } else if (response.status === 403) {
          errorMessage = "Access denied - Insufficient permissions";
          errorCode = "ERR_FORBIDDEN";
        } else if (response.status === 404) {
          errorMessage =
            "Endpoint not found - Server may not support this operation";
          errorCode = "ERR_NOT_FOUND";
        } else if (response.status === 500) {
          errorMessage = "Internal server error - Please check server logs";
          errorCode = "ERR_SERVER_ERROR";
        } else if (
          response.status === 502 ||
          response.status === 503 ||
          response.status === 504
        ) {
          errorMessage = "Server unavailable or timeout - Please try again";
          errorCode = "ERR_SERVICE_UNAVAILABLE";
        }

        console.error(`[TauriHttpClient] HTTP Error ${response.status}:`, {
          url,
          method,
          status: response.status,
          statusText: response.statusText,
          body: errorBody,
        });

        const errorResponse: TauriHttpResponse<T> = {
          data: errorBody as T,
          status: response.status,
          statusText: response.statusText || "",
          headers: responseHeaders,
          config: finalConfig,
        };

        const error = this.createError(
          errorMessage,
          finalConfig,
          errorCode,
          errorResponse,
        );

        // Run error interceptors
        let finalError: unknown = error;
        for (const handler of this.interceptors.response.handlers) {
          if (handler.rejected) {
            try {
              finalError = await Promise.resolve(handler.rejected(finalError));
            } catch (e) {
              finalError = e;
            }
          }
        }
        throw finalError;
      }

      // Parse response body for successful responses
      let data: T;
      const responseType = finalConfig.responseType || "json";

      if (responseType === "json") {
        const text = await response.text();
        data = (text ? JSON.parse(text) : null) as T;
      } else if (responseType === "text") {
        data = (await response.text()) as T;
      } else if (responseType === "blob") {
        // Standard fetch doesn't set blob.type from Content-Type header (unlike axios)
        // Set it manually to match axios behavior
        const blob = await response.blob();
        if (!blob.type) {
          const contentType =
            response.headers.get("content-type") || "application/octet-stream";
          data = new Blob([blob], { type: contentType }) as T;
        } else {
          data = blob as T;
        }
      } else if (responseType === "arraybuffer") {
        data = (await response.arrayBuffer()) as T;
      } else {
        data = (await response.json()) as T;
      }

      const httpResponse: TauriHttpResponse<T> = {
        data,
        status: response.status,
        statusText: response.statusText || "",
        headers: responseHeaders,
        config: finalConfig,
      };

      // Run response interceptors
      let finalResponse = httpResponse;
      for (const handler of this.interceptors.response.handlers) {
        finalResponse = (await Promise.resolve(
          handler.fulfilled(finalResponse),
        )) as TauriHttpResponse<T>;
      }

      return finalResponse;
    } catch (error: unknown) {
      // If it's already a TauriHttpError with interceptors run, re-throw
      if (error && typeof error === "object" && "isAxiosError" in error) {
        throw error;
      }

      // Create detailed error messages for network/other failures
      let errorMessage = "Network Error";
      let errorCode = "ERR_NETWORK";

      if (error instanceof Error) {
        const errMsg = error.message.toLowerCase();

        // Connection refused - server not running or wrong port
        if (
          errMsg.includes("connection refused") ||
          errMsg.includes("econnrefused")
        ) {
          errorMessage = `Unable to connect to server at ${url}. Server may not be running or port is incorrect.`;
          errorCode = "ERR_CONNECTION_REFUSED";
        }
        // Timeout - server too slow or unreachable
        else if (errMsg.includes("timeout") || errMsg.includes("timed out")) {
          errorMessage = `Connection timed out to ${url}. Server is not responding or is too slow.`;
          errorCode = "ERR_TIMEOUT";
        }
        // DNS failure - invalid domain or network issue
        else if (
          errMsg.includes("getaddrinfo") ||
          errMsg.includes("dns") ||
          errMsg.includes("not found") ||
          errMsg.includes("enotfound")
        ) {
          errorMessage = `Cannot resolve server address: ${url}. Please check the URL is correct.`;
          errorCode = "ERR_DNS_FAILURE";
        }
        // SSL/TLS errors - certificate issues
        else if (
          errMsg.includes("ssl") ||
          errMsg.includes("tls") ||
          errMsg.includes("certificate") ||
          errMsg.includes("cert")
        ) {
          errorMessage = `SSL/TLS certificate error for ${url}. Server may have invalid or self-signed certificate.`;
          errorCode = "ERR_SSL_ERROR";
        }
        // Protocol errors - wrong protocol (http vs https)
        else if (
          errMsg.includes("protocol") ||
          errMsg.includes("https") ||
          errMsg.includes("http")
        ) {
          errorMessage = `Protocol error connecting to ${url}. Try using https:// instead of http:// or vice versa.`;
          errorCode = "ERR_PROTOCOL";
        }
        // CORS errors
        else if (errMsg.includes("cors")) {
          errorMessage = `CORS error connecting to ${url}. Server may not allow requests from this application.`;
          errorCode = "ERR_CORS";
        }
        // Generic error with original message
        else {
          errorMessage = `Network error: ${error.message}`;
          errorCode = "ERR_NETWORK";
        }

        console.error("[TauriHttpClient] Network error:", {
          url,
          method,
          errorType: errorCode,
          originalMessage: error.message,
          stack: error.stack,
        });
      } else {
        console.error("[TauriHttpClient] Unknown error type:", error);
      }

      const httpError = this.createError(
        errorMessage,
        finalConfig,
        errorCode,
        undefined,
        error,
      );

      // Run error interceptors
      let finalError: unknown = httpError;
      for (const handler of this.interceptors.response.handlers) {
        if (handler.rejected) {
          try {
            finalError = await Promise.resolve(handler.rejected(finalError));
          } catch (e) {
            finalError = e;
          }
        }
      }
      throw finalError;
    }
  }

  async request<T = any>(
    config: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>(config);
  }

  async get<T = any>(
    url: string,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "GET", url });
  }

  async delete<T = any>(
    url: string,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "DELETE", url });
  }

  async head<T = any>(
    url: string,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "HEAD", url });
  }

  async options<T = any>(
    url: string,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "OPTIONS", url });
  }

  async post<T = any>(
    url: string,
    data?: any,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "POST", url, data });
  }

  async put<T = any>(
    url: string,
    data?: any,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "PUT", url, data });
  }

  async patch<T = any>(
    url: string,
    data?: any,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    return this.executeRequest<T>({ ...config, method: "PATCH", url, data });
  }

  // Axios compatibility methods
  create(config?: TauriHttpRequestConfig): TauriHttpClient {
    return new TauriHttpClient({ ...this.defaults, ...config });
  }

  getUri(config?: TauriHttpRequestConfig): string {
    return this.buildUrl({ ...this.defaults, ...config });
  }

  async postForm<T = any>(
    url: string,
    data?: any,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    const formData = data instanceof FormData ? data : new FormData();
    if (!(data instanceof FormData) && data && typeof data === "object") {
      Object.entries(data).forEach(([key, value]) => {
        formData.append(key, String(value));
      });
    }
    return this.post<T>(url, formData, config);
  }

  async putForm<T = any>(
    url: string,
    data?: any,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    const formData = data instanceof FormData ? data : new FormData();
    if (!(data instanceof FormData) && data && typeof data === "object") {
      Object.entries(data).forEach(([key, value]) => {
        formData.append(key, String(value));
      });
    }
    return this.put<T>(url, formData, config);
  }

  async patchForm<T = any>(
    url: string,
    data?: any,
    config?: TauriHttpRequestConfig,
  ): Promise<TauriHttpResponse<T>> {
    const formData = data instanceof FormData ? data : new FormData();
    if (!(data instanceof FormData) && data && typeof data === "object") {
      Object.entries(data).forEach(([key, value]) => {
        formData.append(key, String(value));
      });
    }
    return this.patch<T>(url, formData, config);
  }
}

// Factory function matching axios.create()
export function create(config?: TauriHttpRequestConfig): TauriHttpClient {
  return new TauriHttpClient(config);
}

// Default instance
export default new TauriHttpClient();
