import { useUserStore } from '@/store';
import { getStringEnv } from '@/c-utils/envUtils';
import { getDynamicApiBaseUrl } from '@/config/environment';
import { debugError, debugInfo, debugWarn } from '@/c-utils/debugConsole';
import { toast } from '@/hooks/useToast';
import i18n from 'i18next';
import { v4 as uuidv4 } from 'uuid';

const AUTH_ERROR_CODES = new Set([1001, 1004, 1005]);
let isHandlingAuthError = false;

// ===== Type Definitions =====
export type RequestConfig = RequestInit & { params?: any; data?: any };

export type StreamRequestConfig = RequestInit & {
  params?: any;
  data?: any;
  parseChunk?: (chunkValue: string) => string;
};
export type StreamCallback = (
  done: boolean,
  text: string,
  abort: () => void,
) => void;

export type BusinessResponse = {
  code: number;
  message?: string;
  data?: unknown;
};

type RequestDebugMeta = {
  url?: string;
  method?: string;
  requestToken?: string;
  httpStatus?: number;
};

// ===== Error Handling =====
export class ErrorWithCode extends Error {
  code: number;
  status?: number;
  constructor(message: string, code: number) {
    super(message);
    this.code = code;
  }
}

const REQUEST_DEBUG_PATTERNS = [
  '/api/learn/shifu/',
  '/api/user/info',
  '/api/user/require_tmp',
] as const;

const maskTokenForDebug = (token?: string) => {
  const normalizedToken = String(token || '').trim();
  if (!normalizedToken) {
    return 'empty';
  }

  if (normalizedToken.length <= 8) {
    return `len:${normalizedToken.length}`;
  }

  return `${normalizedToken.slice(0, 4)}...${normalizedToken.slice(-4)}(len:${normalizedToken.length})`;
};

const shouldLogRequestDebug = (url?: string) => {
  const normalizedUrl = String(url || '');
  return REQUEST_DEBUG_PATTERNS.some(pattern =>
    normalizedUrl.includes(pattern),
  );
};

const buildRequestDebugPayload = (
  error: unknown,
  meta: RequestDebugMeta = {},
) => {
  const typedError = error as Partial<ErrorWithCode> & {
    status?: number;
    cause?: unknown;
  };
  const currentToken = useUserStore.getState().getToken?.() || '';

  return {
    url: meta.url || '',
    method: meta.method || '',
    errorName: typedError?.name || 'Error',
    errorMessage: typedError?.message || '',
    businessCode: typedError?.code ?? '',
    httpStatus: typedError?.status ?? meta.httpStatus ?? '',
    requestToken: maskTokenForDebug(meta.requestToken),
    currentToken: maskTokenForDebug(currentToken),
    tokenChanged:
      Boolean(meta.requestToken) &&
      Boolean(currentToken) &&
      meta.requestToken !== currentToken,
    online:
      typeof navigator !== 'undefined' ? String(navigator.onLine) : 'unknown',
    userAgent:
      typeof navigator !== 'undefined' ? navigator.userAgent : 'server',
    path:
      typeof window !== 'undefined'
        ? `${window.location.pathname}${window.location.search}`
        : '',
    cause: typedError?.cause || '',
  };
};

// Unified error handling function
const handleApiError = (error: ErrorWithCode, showToast = true) => {
  if (showToast) {
    toast({
      title: error.message || i18n.t('common.core.networkError'),
      variant: 'destructive',
    });
  }

  // Dispatch error event (only on client side)
  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
    const apiError = new CustomEvent('apiError', {
      detail: error,
      bubbles: true,
    });
    document.dispatchEvent(apiError);
  }
};

const handleAuthRecovery = async () => {
  if (
    isHandlingAuthError ||
    typeof window === 'undefined' ||
    (window as any).__IS_LOGGING_OUT__
  ) {
    debugInfo('[auth-chain] recovery skipped', {
      isHandlingAuthError,
      isLoggingOut: Boolean(
        typeof window !== 'undefined' && (window as any).__IS_LOGGING_OUT__,
      ),
    });
    return;
  }

  const { logout } = useUserStore.getState();
  if (!logout) {
    debugWarn(
      '[auth-chain] recovery skipped because logout handler is missing',
    );
    return;
  }

  isHandlingAuthError = true;
  try {
    debugInfo('[auth-chain] recovery start', {
      path:
        typeof window !== 'undefined'
          ? `${window.location.pathname}${window.location.search}`
          : '',
    });
    await logout(false);
    debugInfo('[auth-chain] recovery finished with guest session');
  } catch (authError) {
    debugError('[auth-chain] recovery failed', authError);
    console.warn('Failed to recover from auth error:', authError);
  } finally {
    isHandlingAuthError = false;
  }
};

// Check response status code and handle business logic
export const isBusinessResponse = (
  value: unknown,
): value is BusinessResponse => {
  if (!value || typeof value !== 'object') {
    return false;
  }

  return typeof (value as BusinessResponse).code === 'number';
};

export const parseBusinessResponsePayload = (
  payload: unknown,
): BusinessResponse | null => {
  if (isBusinessResponse(payload)) {
    return payload;
  }

  if (typeof payload !== 'string') {
    return null;
  }

  const normalizedPayload = payload.trim();
  if (!normalizedPayload) {
    return null;
  }

  try {
    const parsed = JSON.parse(normalizedPayload);
    return isBusinessResponse(parsed) ? parsed : null;
  } catch {
    return null;
  }
};

export const handleBusinessCode = async (
  response: BusinessResponse,
  requestToken?: string,
  meta: RequestDebugMeta = {},
) => {
  const error = new ErrorWithCode(
    response.message || i18n.t('common.core.unknownError'),
    response.code || -1,
  ) as ErrorWithCode & { status?: number };

  if (typeof meta.httpStatus === 'number') {
    error.status = meta.httpStatus;
  }

  const isAuthError = AUTH_ERROR_CODES.has(response.code);
  const currentToken = useUserStore.getState().getToken?.();
  const tokenChangedDuringRequest =
    isAuthError && currentToken && requestToken !== currentToken;

  if (response.code !== 0) {
    if (isAuthError) {
      debugWarn('[auth-chain] auth business error received', {
        httpStatus: meta.httpStatus ?? '',
        responseCode: response.code,
        responseMessage: response.message || '',
        requestToken: maskTokenForDebug(requestToken),
        currentToken: maskTokenForDebug(currentToken),
        tokenChangedDuringRequest,
        url: meta.url || '',
        method: meta.method || '',
      });
    }

    if (shouldLogRequestDebug(meta.url)) {
      debugWarn('[request-debug] business error', {
        url: meta.url || '',
        method: meta.method || '',
        httpStatus: meta.httpStatus ?? '',
        responseCode: response.code,
        responseMessage: response.message || '',
        isAuthError,
        requestToken: maskTokenForDebug(requestToken),
        currentToken: maskTokenForDebug(currentToken),
        tokenChangedDuringRequest,
        path:
          typeof window !== 'undefined'
            ? `${window.location.pathname}${window.location.search}`
            : '',
      });
    }

    // Special status codes do not show toast
    if (!isAuthError) {
      handleApiError(error);
    }

    // If the token has changed since this request was sent, treat the auth error
    // as stale and avoid logging the user out with a newer session active.
    if (tokenChangedDuringRequest) {
      debugInfo('[auth-chain] auth recovery aborted because token changed', {
        requestToken: maskTokenForDebug(requestToken),
        currentToken: maskTokenForDebug(currentToken),
        url: meta.url || '',
      });
      return Promise.reject(error);
    }

    if (isAuthError) {
      await handleAuthRecovery();
    }

    // Authentication related errors, redirect to login (only on client side)
    // BUGFIX: Prevent double redirects during logout
    // Issue: After logout refreshes the page, some API calls still return auth errors and trigger another redirect
    // Fix: Check the global __IS_LOGGING_OUT__ flag and skip automatic redirects while logout is in progress
    // Related file: src/store/useUserStore.ts
    if (
      typeof window !== 'undefined' &&
      !location.pathname.includes('/login') &&
      isAuthError &&
      !(window as any).__IS_LOGGING_OUT__ // Added: skip redirects while logout is in progress
    ) {
      const currentPath = encodeURIComponent(
        location.pathname + location.search,
      );
      const redirectUrl = `/login?redirect=${currentPath}`;
      debugWarn('[auth-chain] login redirect start', {
        httpStatus: meta.httpStatus ?? '',
        redirectUrl,
        responseCode: response.code,
        responseMessage: response.message || '',
      });
      window.location.href = redirectUrl;
    }

    // Permission error (only on client side)
    if (
      typeof window !== 'undefined' &&
      location.pathname.startsWith('/shifu/') &&
      response.code === 9002
    ) {
      toast({
        title: i18n.t('common.errors.noPermission'),
        variant: 'destructive',
      });
    }

    return Promise.reject(error);
  }
  return response.data ?? response;
};

type SseFallbackXhr = Pick<XMLHttpRequest, 'addEventListener' | 'responseText'>;

type AttachSseBusinessResponseFallbackOptions = {
  requestToken?: string;
  onHandled?: (error: ErrorWithCode) => void;
};

export const attachSseBusinessResponseFallback = (
  source: { xhr?: SseFallbackXhr | null },
  options: AttachSseBusinessResponseFallbackOptions = {},
) => {
  const xhr = source.xhr;
  if (!xhr) {
    return;
  }

  let handled = false;

  xhr.addEventListener('load', () => {
    if (handled) {
      return;
    }

    const response = parseBusinessResponsePayload(xhr.responseText);
    if (!response || response.code === 0) {
      return;
    }

    handled = true;

    void handleBusinessCode(response, options.requestToken).catch(error => {
      options.onHandled?.(error as ErrorWithCode);
    });
  });
};

// ===== Utility Functions =====
const parseJson = (text: string) => {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
};

// ===== Fetch Wrapper Class =====
export class Request {
  private defaultConfig: RequestInit = {};

  constructor(defaultConfig: RequestInit = {}) {
    this.defaultConfig = defaultConfig;
  }

  private async prepareConfig(
    url: string,
    config: RequestInit,
  ): Promise<{ url: string; config: RequestInit; tokenUsed: string }> {
    const mergedConfig = {
      ...this.defaultConfig,
      ...config,
      headers: {
        ...this.defaultConfig.headers,
        ...config.headers,
      },
    };

    const isFormDataBody =
      typeof FormData !== 'undefined' && config.body instanceof FormData;
    if (isFormDataBody && mergedConfig.headers) {
      delete (mergedConfig.headers as Record<string, string>)['Content-Type'];
    }

    // Handle URL
    let fullUrl = url;
    if (!url.startsWith('http')) {
      if (typeof window !== 'undefined') {
        // Client: use cached API base URL to avoid repeated requests
        const siteHost = await getDynamicApiBaseUrl();
        fullUrl = (siteHost || window.location.origin || '') + url;
      } else {
        // Fallback for server-side rendering
        fullUrl = (getStringEnv('baseURL') || '') + url;
      }
    }

    // Add authentication headers
    const token = useUserStore.getState().getToken() || '';
    if (token) {
      mergedConfig.headers = {
        Authorization: `Bearer ${token}`,
        Token: token,
        'X-Request-ID': uuidv4().replace(/-/g, ''),
        ...mergedConfig.headers,
      } as HeadersInit;
    }

    return { url: fullUrl, config: mergedConfig, tokenUsed: token };
  }

  private async interceptFetch(url: string, config: RequestConfig) {
    let fullUrl = url;
    let requestMethod = String(config.method || 'GET');
    let tokenUsed = '';

    try {
      const {
        url: resolvedFullUrl,
        config: mergedConfig,
        tokenUsed: resolvedTokenUsed,
      } = await this.prepareConfig(url, config);
      fullUrl = resolvedFullUrl;
      tokenUsed = resolvedTokenUsed;
      requestMethod = String(mergedConfig.method || config.method || 'GET');

      if (shouldLogRequestDebug(fullUrl)) {
        debugInfo('[request-debug] start', {
          url: fullUrl,
          method: requestMethod,
          requestToken: maskTokenForDebug(tokenUsed),
          path:
            typeof window !== 'undefined'
              ? `${window.location.pathname}${window.location.search}`
              : '',
        });
      }

      const response = await fetch(fullUrl, mergedConfig);

      if (!response.ok) {
        const isDevelopment = process.env.NODE_ENV === 'development';
        const errorMessage = isDevelopment
          ? `Request failed with status ${response.status}`
          : 'Network request failed';
        const httpError = new ErrorWithCode(
          errorMessage,
          response.status,
        ) as ErrorWithCode & { status?: number };
        httpError.status = response.status;
        throw httpError;
      }

      const res = await response.json();

      // Check business status code
      if (Object.prototype.hasOwnProperty.call(res, 'code')) {
        if (shouldLogRequestDebug(fullUrl)) {
          debugInfo('[request-debug] response envelope', {
            url: fullUrl,
            method: requestMethod,
            httpStatus: response.status,
            responseCode: res.code,
            responseMessage: res.message || '',
          });
        }
        const isAuthError = AUTH_ERROR_CODES.has(res.code);
        // If it's login page, we only skip non-auth errors to allow UI to handle business errors
        // But Auth errors (1001, 1004, 1005) MUST be handled by global handler to clear token
        if (location.pathname.includes('/login') && !isAuthError) {
          return res;
        }
        return handleBusinessCode(res, tokenUsed, {
          url: fullUrl,
          method: requestMethod,
          requestToken: tokenUsed,
          httpStatus: response.status,
        });
      }

      if (shouldLogRequestDebug(fullUrl)) {
        debugInfo('[request-debug] response ok', {
          url: fullUrl,
          method: requestMethod,
          httpStatus: response.status,
          responseCode: '',
        });
      }

      return res;
    } catch (error: any) {
      if (shouldLogRequestDebug(fullUrl)) {
        debugError(
          '[request-debug] fetch failure',
          buildRequestDebugPayload(error, {
            url: fullUrl,
            method: requestMethod,
            requestToken: tokenUsed,
            httpStatus:
              typeof error?.status === 'number' ? error.status : undefined,
          }),
        );
      }
      handleApiError(error);
      throw error;
    }
  }

  // HTTP method wrappers
  get(url: string, config: RequestConfig = {}) {
    return this.interceptFetch(url, { method: 'GET', ...config });
  }

  post(url: string, body: any = {}, config: RequestConfig = {}) {
    const isFormData =
      typeof FormData !== 'undefined' && body instanceof FormData;
    const headers = { ...(config.headers as HeadersInit) };

    const requestConfig: RequestConfig = {
      method: 'POST',
      ...config,
    };

    if (isFormData) {
      if (headers && 'Content-Type' in headers) {
        delete (headers as Record<string, string>)['Content-Type'];
      }
      requestConfig.headers = headers;
      requestConfig.body = body;
    } else {
      try {
        requestConfig.body = JSON.stringify(body ?? {});
      } catch (e) {
        // Payload serialization failed (often due to passing event objects)
        handleApiError(new ErrorWithCode('Invalid request payload', -1));
        throw e;
      }
      requestConfig.headers = {
        'Content-Type': 'application/json',
        ...headers,
      } as HeadersInit;
    }

    return this.interceptFetch(url, requestConfig);
  }

  put(url: string, body: any = {}, config: RequestConfig = {}) {
    const isFormData =
      typeof FormData !== 'undefined' && body instanceof FormData;
    const headers = { ...(config.headers as HeadersInit) };

    const requestConfig: RequestConfig = {
      method: 'PUT',
      ...config,
    };

    if (isFormData) {
      if (headers && 'Content-Type' in headers) {
        delete (headers as Record<string, string>)['Content-Type'];
      }
      requestConfig.headers = headers;
      requestConfig.body = body;
    } else {
      requestConfig.body = JSON.stringify(body ?? {});
      requestConfig.headers = {
        'Content-Type': 'application/json',
        ...headers,
      } as HeadersInit;
    }

    return this.interceptFetch(url, requestConfig);
  }

  delete(url: string, config: RequestConfig = {}) {
    return this.interceptFetch(url, { method: 'DELETE', ...config });
  }

  patch(url: string, body: any = {}, config: RequestConfig = {}) {
    return this.interceptFetch(url, {
      method: 'PATCH',
      body: JSON.stringify(body),
      ...config,
    });
  }
  // Stream request
  async stream(
    url: string,
    body: any = {},
    config: StreamRequestConfig = {},
    callback?: StreamCallback,
  ) {
    const {
      url: fullUrl,
      config: preparedConfig,
      tokenUsed,
    } = await this.prepareConfig(url, config);

    try {
      const { parseChunk, ...rest } = config as any;
      const controller = new AbortController();
      const response = await fetch(fullUrl, {
        ...preparedConfig,
        ...rest,
        method: 'POST',
        body: JSON.stringify(body),
        signal: controller.signal,
      });

      if (!response.ok) {
        const isDevelopment = process.env.NODE_ENV === 'development';
        const errorMessage = isDevelopment
          ? `Request failed with status ${response.status}`
          : 'Network request failed';
        throw new ErrorWithCode(errorMessage, response.status);
      }

      const reader = response.body?.getReader();
      if (!reader) throw new Error('Response body is not readable');

      const decoder = new TextDecoder();
      let done = false;
      let text = '';

      const stop = () => {
        done = true;
        controller.abort();
      };

      while (!done) {
        const { value, done: doneReading } = await reader.read();
        done = doneReading;
        let chunkValue = decoder.decode(value);

        if (parseChunk) {
          chunkValue = parseChunk(chunkValue);
        }

        text += chunkValue;

        if (callback) {
          callback(done, text, stop);
        }
      }

      const result = parseJson(text);
      if (typeof result === 'object' && result.code !== undefined) {
        return handleBusinessCode(result, tokenUsed);
      }

      return result;
    } catch (error: any) {
      console.error('Stream request failed:', error);
      throw error;
    }
  }

  // Stream line by line request
  async streamLine(
    url: string,
    body: any = {},
    config: StreamRequestConfig = {},
    callback?: StreamCallback,
  ) {
    const {
      url: fullUrl,
      config: preparedConfig,
      tokenUsed,
    } = await this.prepareConfig(url, config);

    try {
      const { parseChunk, ...rest } = config as any;
      const controller = new AbortController();
      const response = await fetch(fullUrl, {
        ...preparedConfig,
        ...rest,
        method: 'POST',
        body: JSON.stringify(body),
        signal: controller.signal,
      });

      if (!response.ok) {
        const isDevelopment = process.env.NODE_ENV === 'development';
        const errorMessage = isDevelopment
          ? `Request failed with status ${response.status}`
          : 'Network request failed';
        throw new ErrorWithCode(errorMessage, response.status);
      }

      const reader = response.body?.getReader();
      if (!reader) throw new Error('Response body is not readable');

      const utf8Decoder = new TextDecoder('utf-8');
      let done = false;
      const stop = () => {
        done = true;
        controller.abort();
      };

      const lines: string[] = [];
      let { value: chunk, done: readerDone } = await reader.read();
      let decodedChunk = chunk
        ? utf8Decoder.decode(chunk, { stream: true })
        : '';
      const re = /\r\n|\n|\r/gm;
      let startIndex = 0;

      // Stream read line processing
      for (;;) {
        const result = re.exec(decodedChunk);
        if (!result) {
          if (readerDone) break;
          const remainder = decodedChunk.substring(startIndex);
          ({ value: chunk, done: readerDone } = await reader.read());
          decodedChunk =
            remainder +
            (chunk ? utf8Decoder.decode(chunk, { stream: true }) : '');
          startIndex = re.lastIndex = 0;
          continue;
        }
        let line = decodedChunk.substring(startIndex, result.index);
        if (parseChunk) {
          line = parseChunk(line);
        }
        lines.push(line);
        if (callback) {
          callback(done, line, stop);
        }
        startIndex = re.lastIndex;
      }

      if (startIndex < decodedChunk.length) {
        let line = decodedChunk.substring(startIndex);
        if (parseChunk) {
          line = parseChunk(line);
        }
        lines.push(line);
        if (callback) {
          callback(done, line, stop);
        }
      }

      if (callback) {
        callback(true, '', stop);
      }

      // Check the last non-empty line for auth error codes to maintain
      // consistent stale-token detection across all request types.
      let lastLine: string | undefined;
      for (let i = lines.length - 1; i >= 0; i--) {
        if (lines[i].trim() !== '') {
          lastLine = lines[i];
          break;
        }
      }
      if (lastLine) {
        const parsed = parseJson(lastLine);
        if (typeof parsed === 'object' && parsed.code !== undefined) {
          await handleBusinessCode(parsed, tokenUsed);
        }
      }

      return lines;
    } catch (error: any) {
      console.error('StreamLine request failed:', error);
      throw error;
    }
  }
}

// ===== Default Instance Export =====
const defaultConfig = {
  headers: {
    'Content-Type': 'application/json',
  },
};

const request = new Request(defaultConfig);

export default request;
