import type { InternalAxiosRequestConfig } from 'axios';

declare module 'axios' {
  interface AxiosRequestConfig {
    dedupe?: boolean;
    /**
     * 跳过 Authorization 注入。
     *
     * 用于公开接口（refresh-token / login / register 等 PermitAll 路径），
     * 避免给它们带上过期 access 头被网关拦截。
     */
    skipAuth?: boolean;
    /** 请求失败时不走通用错误 toast，由调用方自行收敛提示。 */
    suppressErrorMessage?: boolean;
    /** 请求失败命中过期 access code 时，不再触发 refresh-token 流程。 */
    skipTokenRefresh?: boolean;
  }
}

const WRITE_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH']);

type DedupableConfig = Pick<InternalAxiosRequestConfig, 'method' | 'url' | 'data' | 'params'> & {
  dedupe?: boolean;
};

function isFormDataLike(value: unknown): boolean {
  if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
  if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
  return false;
}

function stableJson(value: unknown): string {
  if (value === null || value === undefined) return '';
  if (typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
  const obj = value as Record<string, unknown>;
  const keys = Object.keys(obj).sort();
  return `{${keys.map(k => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`;
}

export function computeDedupeKey(config: DedupableConfig): string | null {
  const method = (config.method ?? 'GET').toUpperCase();
  if (!WRITE_METHODS.has(method)) return null;
  if (config.dedupe === false) return null;
  if (isFormDataLike(config.data)) return null;

  const url = config.url ?? '';
  const paramsPart = stableJson(config.params);
  const bodyPart = stableJson(config.data);
  return `${method}|${url}?${paramsPart}|${bodyPart}`;
}

const DEFAULT_TTL_MS = 30_000;

export interface WithDedupeOptions {
  ttlMs?: number;
  now?: () => number;
}

type AnyRequestFn = (...args: any[]) => Promise<unknown>;

export function withDedupe<TFn extends AnyRequestFn>(request: TFn, options: WithDedupeOptions = {}): TFn {
  const ttl = options.ttlMs ?? DEFAULT_TTL_MS;
  const now = options.now ?? Date.now;
  const pending = new Map<string, { promise: Promise<unknown>; expiresAt: number }>();

  return new Proxy(request, {
    apply(target, thisArg, args: Parameters<TFn>) {
      const [config] = args;
      const key = computeDedupeKey(config as DedupableConfig);
      if (key === null) return Reflect.apply(target, thisArg, args);

      const cached = pending.get(key);
      if (cached && cached.expiresAt > now()) return cached.promise;
      if (cached) pending.delete(key);

      const promise = Promise.resolve()
        .then(() => Reflect.apply(target, thisArg, args))
        .finally(() => {
          const current = pending.get(key);
          if (current && current.promise === promise) {
            pending.delete(key);
          }
        });

      pending.set(key, { promise, expiresAt: now() + ttl });
      return promise;
    }
  }) as TFn;
}
