/**
 * API fetch 래퍼
 *
 * - access token: 메모리 변수
 * - refresh token: HttpOnly cookie (서버가 관리)
 * - refresh 중복 방지: isRefreshing 플래그 + 대기 큐
 * - 401 retry: 1회만, 실패 시 강제 logout
 */

const API_BASE = '/api';

let accessToken: string | null = null;

// refresh 큐
let isRefreshing = false;
let refreshQueue: Array<{
  resolve: (token: string) => void;
  reject: (err: Error) => void;
}> = [];

export function setAccessToken(token: string | null) {
  accessToken = token;
}

export function getAccessToken(): string | null {
  return accessToken;
}

async function refreshAccessToken(): Promise<string> {
  const res = await fetch(`${API_BASE}/auth/refresh`, {
    method: 'POST',
    credentials: 'include', // cookie 전송
  });
  if (!res.ok) {
    throw new Error('refresh failed');
  }
  const data = await res.json();
  accessToken = data.access_token;
  return data.access_token;
}

function processRefreshQueue(error: Error | null, token: string | null) {
  refreshQueue.forEach(({ resolve, reject }) => {
    if (error) reject(error);
    else resolve(token!);
  });
  refreshQueue = [];
}

async function handleTokenRefresh(): Promise<string> {
  if (isRefreshing) {
    return new Promise((resolve, reject) => {
      refreshQueue.push({ resolve, reject });
    });
  }

  isRefreshing = true;
  try {
    const token = await refreshAccessToken();
    processRefreshQueue(null, token);
    return token;
  } catch (err) {
    const error = err instanceof Error ? err : new Error('refresh failed');
    processRefreshQueue(error, null);
    // 강제 logout
    accessToken = null;
    if (typeof window !== 'undefined') {
      window.location.href = '/login';
    }
    throw error;
  } finally {
    isRefreshing = false;
  }
}

/**
 * 일반 에러는 `detail` 만 string. 업로드 엔드포인트는 `{error_code, message}`
 * 객체를 보내므로 (§4), 응답 파서가 그 객체를 풀어 `errorCode` / `errorMessage`
 * 필드로 분리하고 `detail` 은 사람이 읽을 메시지(string) 로 정규화한다.
 * 덕분에 기존 `err.detail` 소비자(toast 등)는 그대로 동작.
 */
export type ApiError = {
  status: number;
  detail: string;
  errorCode?: string;
  errorMessage?: string;
  /**
   * PR-DocSrv-Web-Ask-Selector-1: backend_unavailable 503 path 가
   * body level 에 `error_reason` (예: `macbook_unavailable`) 을 둠. 본 필드는
   * 그 raw 값. 다른 endpoint 응답에 `error_reason` 이 없으면 undefined.
   */
  errorReason?: string;
};

function parseDetail(body: unknown, fallback: string): {
  detail: string;
  errorCode?: string;
  errorMessage?: string;
  errorReason?: string;
} {
  if (body && typeof body === 'object') {
    const top = body as { detail?: unknown; error_reason?: unknown };
    const errorReason = typeof top.error_reason === 'string' ? top.error_reason : undefined;
    if ('detail' in body) {
      const d = top.detail;
      if (typeof d === 'string' && d) return { detail: d, errorReason };
      if (d && typeof d === 'object') {
        const obj = d as { error_code?: string; message?: string };
        const message = obj.message || fallback;
        return { detail: message, errorCode: obj.error_code, errorMessage: obj.message, errorReason };
      }
    }
  }
  return { detail: fallback };
}

export async function api<T = unknown>(
  path: string,
  options: RequestInit = {},
): Promise<T> {
  const headers: Record<string, string> = {
    ...(options.headers as Record<string, string> || {}),
  };

  if (accessToken) {
    headers['Authorization'] = `Bearer ${accessToken}`;
  }

  // FormData일 때는 Content-Type 자동 설정
  if (options.body && !(options.body instanceof FormData)) {
    headers['Content-Type'] = 'application/json';
  }

  const res = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers,
    credentials: 'include',
  });

  // 401 → refresh 1회 시도 (로그인/리프레시 엔드포인트는 제외)
  const isAuthEndpoint = path.startsWith('/auth/login') || path.startsWith('/auth/refresh');
  if (res.status === 401 && accessToken && !isAuthEndpoint) {
    try {
      await handleTokenRefresh();
      headers['Authorization'] = `Bearer ${accessToken}`;
      const retryRes = await fetch(`${API_BASE}${path}`, {
        ...options,
        headers,
        credentials: 'include',
      });
      if (!retryRes.ok) {
        const body = await retryRes.json().catch(() => null);
        const parsed = parseDetail(body, retryRes.statusText);
        throw { status: retryRes.status, ...parsed } as ApiError;
      }
      return retryRes.json();
    } catch (e) {
      if ((e as ApiError).detail) throw e;
      throw { status: 401, detail: '인증이 만료되었습니다' } as ApiError;
    }
  }

  if (!res.ok) {
    const body = await res.json().catch(() => null);
    const parsed = parseDetail(body, res.statusText);
    throw { status: res.status, ...parsed } as ApiError;
  }

  // 204 No Content
  if (res.status === 204) return {} as T;

  return res.json();
}

/**
 * Raw fetch 헬퍼 — SSE/스트리밍 등 JSON 일괄 파싱이 부적합한 endpoint 전용.
 *
 * api<T>() 와 동일한 정책을 공유한다:
 * - access token 자동 첨부
 * - 401 → refresh 1회 재시도 (실패 시 handleTokenRefresh 가 강제 logout)
 * - JSON body 면 Content-Type 자동 설정
 *
 * 차이: Response 를 그대로 반환한다 (status 판단 / body 소비는 호출자 책임).
 * PR-Eid-Chat: `/api/eid/chat` SSE 스트림이 첫 소비자. additive export only —
 * 기존 api()/uploadFile() 동작은 변경하지 않는다.
 */
export async function apiFetchRaw(
  path: string,
  options: RequestInit = {},
): Promise<Response> {
  const headers: Record<string, string> = {
    ...(options.headers as Record<string, string> || {}),
  };

  if (accessToken) {
    headers['Authorization'] = `Bearer ${accessToken}`;
  }
  if (options.body && !(options.body instanceof FormData)) {
    headers['Content-Type'] = 'application/json';
  }

  const res = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers,
    credentials: 'include',
  });

  // 401 → refresh 1회 시도 (api() 와 같은 정책, auth endpoint 제외)
  const isAuthEndpoint = path.startsWith('/auth/login') || path.startsWith('/auth/refresh');
  if (res.status === 401 && accessToken && !isAuthEndpoint) {
    try {
      await handleTokenRefresh();
    } catch {
      // refresh 실패 — handleTokenRefresh 가 강제 logout(리다이렉트) 처리.
      // api() 와 일관되게 원본 401 Response 를 그대로 반환해 호출자가
      // 네트워크 에러로 오인하지 않게 한다 (body 미소비 상태라 재사용 가능).
      return res;
    }
    headers['Authorization'] = `Bearer ${accessToken}`;
    return fetch(`${API_BASE}${path}`, {
      ...options,
      headers,
      credentials: 'include',
    });
  }

  return res;
}

/**
 * 업로드 전용 헬퍼 — XMLHttpRequest 기반.
 *
 * 이유: fetch() 는 upload progress 이벤트를 표준에서 지원하지 않음.
 * 진행률 표시 + 사용자 abort 가 필수인 업로드만 XHR 로 분리.
 *
 * - access token 자동 첨부
 * - 401 → refresh 1회 재시도 (api() 와 같은 정책)
 * - signal: AbortSignal — 사용자 취소시 xhr.abort() 호출
 * - onProgress(loaded, total): 전송 중 콜백
 * - 에러는 ApiError 형식으로 reject
 */
export interface UploadOptions {
  signal?: AbortSignal;
  onProgress?: (loaded: number, total: number) => void;
}

export async function uploadFile<T = unknown>(
  path: string,
  formData: FormData,
  opts: UploadOptions = {},
): Promise<T> {
  const tryOnce = (token: string | null) =>
    new Promise<T>((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open('POST', `${API_BASE}${path}`);
      xhr.withCredentials = true;
      if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);

      const abortHandler = () => xhr.abort();
      if (opts.signal) {
        if (opts.signal.aborted) {
          xhr.abort();
        } else {
          opts.signal.addEventListener('abort', abortHandler);
        }
      }
      const cleanup = () => {
        if (opts.signal) opts.signal.removeEventListener('abort', abortHandler);
      };

      if (opts.onProgress) {
        xhr.upload.addEventListener('progress', (e) => {
          if (e.lengthComputable) opts.onProgress!(e.loaded, e.total);
        });
      }

      xhr.addEventListener('load', () => {
        cleanup();
        if (xhr.status >= 200 && xhr.status < 300) {
          if (!xhr.responseText) return resolve({} as T);
          try {
            resolve(JSON.parse(xhr.responseText));
          } catch {
            resolve({} as T);
          }
        } else {
          let body: unknown = null;
          try {
            body = JSON.parse(xhr.responseText);
          } catch {
            // 응답 본문 파싱 실패 — fallback 사용
          }
          const parsed = parseDetail(body, xhr.statusText || '업로드 실패');
          reject({ status: xhr.status, ...parsed } as ApiError);
        }
      });

      xhr.addEventListener('error', () => {
        cleanup();
        reject({
          status: 0,
          detail: '네트워크 오류',
          errorCode: 'network_abort',
          errorMessage: '네트워크 오류',
        } as ApiError);
      });

      xhr.addEventListener('abort', () => {
        cleanup();
        reject({
          status: 0,
          detail: '업로드가 취소되었습니다',
          errorCode: 'network_abort',
          errorMessage: '업로드가 취소되었습니다',
        } as ApiError);
      });

      xhr.send(formData);
    });

  try {
    return await tryOnce(accessToken);
  } catch (e) {
    const err = e as ApiError;
    // 401 → refresh 1회. abort/네트워크 에러는 retry 안 함.
    if (err.status === 401 && accessToken) {
      try {
        await handleTokenRefresh();
        return await tryOnce(accessToken);
      } catch (e2) {
        if ((e2 as ApiError).detail) throw e2;
        throw { status: 401, detail: '인증이 만료되었습니다' } as ApiError;
      }
    }
    throw err;
  }
}
