import { md5 } from 'js-md5';
import {
  getAPIBaseUrl,
  getNodeAPIBaseUrl,
  isTauriAppPlatform,
  isWebAppPlatform,
} from '@/services/environment';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { READEST_OPDS_USER_AGENT } from '@/services/constants';
import {
  OPDSCustomHeaders,
  normalizeOPDSCustomHeaders,
  serializeOPDSCustomHeaders,
} from './customHeaders';

const OPDS_PROXY_URL = `${getAPIBaseUrl()}/opds/proxy`;
const NODE_OPDS_PROXY_URL = `${getNodeAPIBaseUrl()}/opds/proxy`;
/**
 * Extract username and password from URL credentials
 */
const extractCredentialsFromURL = (
  url: string,
): { url: string; username?: string; password?: string } => {
  try {
    const urlObj = new URL(url);
    const username = decodeURIComponent(urlObj.username) || undefined;
    const password = decodeURIComponent(urlObj.password) || undefined;

    if (username || password) {
      urlObj.username = '';
      urlObj.password = '';
      return {
        url: urlObj.toString(),
        username,
        password,
      };
    }
  } catch (e) {
    console.warn('Failed to parse URL:', e);
  }

  return { url };
};

export const needsProxy = (url: string): boolean => {
  return isWebAppPlatform() && url.startsWith('http');
};

const PROXY_OVERRIDES: Record<string, string> = {
  standardebooks: NODE_OPDS_PROXY_URL,
};

const getProxyBaseUrl = (url: string): string => {
  for (const [domain, proxyUrl] of Object.entries(PROXY_OVERRIDES)) {
    if (url.includes(domain)) {
      return proxyUrl;
    }
  }
  return OPDS_PROXY_URL;
};

/**
 * Generate proxied URL for OPDS requests
 */
export const getProxiedURL = (
  url: string,
  auth: string = '',
  stream = false,
  customHeaders: OPDSCustomHeaders = {},
): string => {
  if (url.startsWith('http')) {
    const { url: cleanUrl } = extractCredentialsFromURL(url);
    const params = new URLSearchParams();
    params.append('url', cleanUrl);
    params.append('stream', `${stream}`);
    if (auth) {
      params.append('auth', auth);
    }
    const serializedHeaders = serializeOPDSCustomHeaders(customHeaders);
    if (serializedHeaders) {
      params.append('headers', serializedHeaders);
    }
    const baseUrl = getProxyBaseUrl(url);
    const proxyUrl = `${baseUrl}?${params.toString()}`;
    return proxyUrl;
  }
  return url;
};

/**
 * Parse Digest authentication challenge from WWW-Authenticate header
 */
const parseDigestChallenge = (challenge: string): Record<string, string> => {
  const params: Record<string, string> = {};
  const regex = /(\w+)=["']?([^"',]+)["']?/g;
  let match;

  while ((match = regex.exec(challenge)) !== null) {
    params[match[1]!] = match[2]!;
  }

  return params;
};

/**
 * Generate Digest authentication response hash
 */
const generateDigestResponse = (
  username: string,
  password: string,
  params: Record<string, string>,
  method: string,
  uri: string,
  nc: string,
  cnonce: string,
) => {
  const realm = params['realm'];
  const nonce = params['nonce'];
  const qop = params['qop'];
  const algorithm = params['algorithm'];

  let ha1 = md5(`${username}:${realm}:${password}`);

  if (algorithm && algorithm.toLowerCase() === 'md5-sess') {
    ha1 = md5(`${ha1}:${nonce}:${cnonce}`);
  }

  const ha2 = md5(`${method}:${uri}`);

  let response: string;

  if (qop) {
    response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
  } else {
    response = md5(`${ha1}:${nonce}:${ha2}`);
  }

  return response;
};

/**
 * Create Digest Authorization header
 */
export const createDigestAuth = async (
  username: string,
  password: string,
  wwwAuthenticate: string,
  method: string,
  uri: string,
): Promise<string> => {
  const params = parseDigestChallenge(wwwAuthenticate);
  const cnonce = Math.random().toString(36).slice(2);
  const nc = '00000001';
  const response = await generateDigestResponse(
    username,
    password,
    params,
    method,
    uri,
    nc,
    cnonce,
  );

  const parts = [
    `username="${username}"`,
    `realm="${params['realm']}"`,
    `nonce="${params['nonce']}"`,
    `uri="${uri}"`,
    `response="${response}"`,
  ];

  if (params['algorithm']) {
    parts.push(`algorithm="${params['algorithm']}"`);
  }

  if (params['opaque']) {
    parts.push(`opaque="${params['opaque']}"`);
  }

  if (params['qop']) {
    parts.push(`qop="auth"`);
    parts.push(`nc=${nc}`);
    parts.push(`cnonce="${cnonce}"`);
  }

  return `Digest ${parts.join(', ')}`;
};

/**
 * Create Basic Authorization header
 */
export const createBasicAuth = (username: string, password: string): string => {
  const credentials = `${username}:${password}`;
  const utf8Bytes = new TextEncoder().encode(credentials);
  const encoded = btoa(String.fromCharCode(...utf8Bytes));
  return `Basic ${encoded}`;
};

/**
 * Probe URL for authentication requirements using HEAD request
 * Returns auth header if authentication is needed, null otherwise
 */
export const probeAuth = async (
  url: string,
  username?: string,
  password?: string,
  useProxy = false,
  customHeaders: OPDSCustomHeaders = {},
): Promise<string | null> => {
  const {
    url: cleanUrl,
    username: urlUsername,
    password: urlPassword,
  } = extractCredentialsFromURL(url);

  const finalUsername = username || urlUsername;
  const finalPassword = password || urlPassword;
  const normalizedCustomHeaders = normalizeOPDSCustomHeaders(customHeaders);

  // No credentials provided, can't generate auth header
  if (!finalUsername || !finalPassword) {
    return null;
  }

  const fetchURL = useProxy
    ? getProxiedURL(cleanUrl, '', false, normalizedCustomHeaders)
    : cleanUrl;
  const headers: Record<string, string> = {
    'User-Agent': READEST_OPDS_USER_AGENT,
    Accept: 'application/atom+xml, application/xml, text/xml, */*',
    ...(!useProxy ? normalizedCustomHeaders : {}),
  };

  // Probe with HEAD request
  const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
  const res = await fetch(fetchURL, {
    method: 'HEAD',
    headers,
    danger: { acceptInvalidCerts: true, acceptInvalidHostnames: true },
  });

  // Check if authentication is required
  if (res.status === 401 || res.status === 403) {
    const wwwAuthenticate = res.headers.get('WWW-Authenticate');
    if (wwwAuthenticate) {
      if (wwwAuthenticate.toLowerCase().startsWith('digest')) {
        const urlObj = new URL(cleanUrl);
        return await createDigestAuth(
          finalUsername,
          finalPassword,
          wwwAuthenticate,
          'GET',
          urlObj.pathname + urlObj.search,
        );
      } else if (wwwAuthenticate.toLowerCase().startsWith('basic')) {
        return createBasicAuth(finalUsername, finalPassword);
      }
    } else {
      // Fallback to Basic auth if no WWW-Authenticate header
      // some older Calibre-Web versions behave this way, see issue #2656
      return createBasicAuth(finalUsername, finalPassword);
    }
  }

  // Komga returns 200 even if requires auth, so we return Basic auth header in this case
  return createBasicAuth(finalUsername, finalPassword);
};

export const probeFilename = async (headers: Record<string, string>) => {
  const contentDisposition = headers['content-disposition'];
  if (contentDisposition) {
    // 1. Try RFC 5987 format (filename*=utf-8''encoded_name)
    const extendedMatch = contentDisposition.match(
      /filename\*\s*=\s*(?:utf-8|UTF-8)'[^']*'([^;\s]+)/i,
    );
    if (extendedMatch?.[1]) {
      try {
        return decodeURIComponent(extendedMatch[1]);
      } catch (e) {
        // If decoding fails, ignore and proceed to the next format
        console.warn('Failed to decode filename*', e);
      }
    }

    // 2. Try standard quoted format (supports spaces, apostrophes, and escaped quotes)
    const quotedMatch = contentDisposition.match(/filename\s*=\s*(["'])((?:(?!\1)[^\\]|\\.)*)\1/i);
    if (quotedMatch?.[2]) {
      // Unescape characters (e.g., \" becomes ")
      const unescaped = quotedMatch[2].replace(/\\(.)/g, '$1');
      try {
        // Attempt to decode in case the server incorrectly applied URL encoding
        return decodeURIComponent(unescaped);
      } catch {
        // If decoding fails (e.g., literal '%' symbols), return the unescaped string as-is
        return unescaped;
      }
    }

    // 3. Fallback: standard format without quotes
    const plainMatch = contentDisposition.match(/filename\s*=\s*([^;\s]+)/i);
    if (plainMatch?.[1]) {
      try {
        return decodeURIComponent(plainMatch[1]);
      } catch {
        return plainMatch[1];
      }
    }
  }

  return '';
};

/**
 * Perform authenticated HTTP request with retry logic for Digest/Basic auth
 */
export const fetchWithAuth = async (
  url: string,
  username?: string,
  password?: string,
  useProxy = false,
  options: RequestInit = {},
  customHeaders: OPDSCustomHeaders = {},
): Promise<Response> => {
  const {
    url: cleanUrl,
    username: urlUsername,
    password: urlPassword,
  } = extractCredentialsFromURL(url);

  const finalUsername = username || urlUsername;
  const finalPassword = password || urlPassword;
  const normalizedCustomHeaders = normalizeOPDSCustomHeaders(customHeaders);

  // Send Basic auth preemptively when credentials are available. Servers that
  // allow anonymous access (e.g. Calibre-Web) return 200 without a challenge,
  // so the user keeps seeing guest content unless the auth header is included
  // on the first request. Digest auth can't be sent preemptively (it needs the
  // server's nonce), so Digest servers still fall through to the retry below.
  const preemptiveAuth =
    finalUsername && finalPassword ? createBasicAuth(finalUsername, finalPassword) : null;

  const fetchURL = useProxy
    ? getProxiedURL(cleanUrl, preemptiveAuth || '', false, normalizedCustomHeaders)
    : cleanUrl;
  const baseHeaders: Record<string, string> = {
    'User-Agent': READEST_OPDS_USER_AGENT,
    Accept: 'application/atom+xml, application/xml, text/xml, */*',
    ...(!useProxy ? normalizedCustomHeaders : {}),
    ...(options.headers as Record<string, string>),
  };
  const headers: Record<string, string> = {
    ...baseHeaders,
    ...(preemptiveAuth && !useProxy ? { Authorization: preemptiveAuth } : {}),
  };

  const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
  let res = await fetch(fetchURL, {
    ...options,
    method: options.method || 'GET',
    headers,
    danger: { acceptInvalidCerts: true, acceptInvalidHostnames: true },
  });

  // Calibre in 'digest' (or 'auto' over http) mode rejects a Basic
  // Authorization header outright with 400 "Unsupported authentication
  // method" instead of returning a 401 challenge, so the preemptive Basic
  // header would dead-end the request. Re-issue it without credentials to
  // surface the WWW-Authenticate challenge and let the retry below negotiate
  // the scheme the server actually wants.
  if (res.status === 400 && preemptiveAuth) {
    res = await fetch(
      useProxy ? getProxiedURL(cleanUrl, '', false, normalizedCustomHeaders) : fetchURL,
      {
        ...options,
        method: options.method || 'GET',
        headers: baseHeaders,
        danger: { acceptInvalidCerts: true, acceptInvalidHostnames: true },
      },
    );
  }

  // Handle authentication if needed
  if (!res.ok && (res.status === 401 || res.status === 403) && finalUsername && finalPassword) {
    const wwwAuthenticate = res.headers.get('WWW-Authenticate');
    if (wwwAuthenticate) {
      let authHeader: string | null = null;

      if (wwwAuthenticate.toLowerCase().startsWith('digest')) {
        const urlObj = new URL(cleanUrl);
        authHeader = await createDigestAuth(
          finalUsername,
          finalPassword,
          wwwAuthenticate,
          options.method || 'GET',
          urlObj.pathname + urlObj.search,
        );
      } else if (wwwAuthenticate.toLowerCase().startsWith('basic')) {
        authHeader = createBasicAuth(finalUsername, finalPassword);
      }

      // Skip the retry when it would just repeat the preemptive Basic header
      // we already sent (the credentials are simply wrong in that case).
      if (authHeader && authHeader !== preemptiveAuth) {
        const finalUrl = useProxy
          ? getProxiedURL(cleanUrl, authHeader, false, normalizedCustomHeaders)
          : fetchURL;
        res = await fetch(finalUrl, {
          ...options,
          method: options.method || 'GET',
          headers: useProxy ? headers : { ...headers, Authorization: authHeader },
          danger: { acceptInvalidCerts: true, acceptInvalidHostnames: true },
        });
      }
    }
  }

  return res;
};
