import axios, {
  type AxiosInstance,
  type AxiosResponse,
  type InternalAxiosRequestConfig,
} from 'axios'
import { useDialogStore } from '@/stores/dialog.store'

// axios 요청 설정 확장 — 호출 단위 옵션
declare module 'axios' {
  export interface AxiosRequestConfig {
    /** true 면 에러 발생 시 전역 알림(UI) 을 띄우지 않는다 (예: 부트스트랩용 /auth/me) */
    silent?: boolean
    /** 내부용 — 401 재시도(토큰 refresh) 1회 수행 여부 플래그 */
    _retry?: boolean
  }
}

// 표준 응답 포맷 타입 (Global Response Wrapper) — 백엔드 TransformInterceptor와 동일 구조
export interface StandardResponse<T = unknown> {
  success: boolean
  data?: T
  error?: {
    code: string
    message: string
  }
}

// 에러 코드 사전 — CLAUDE.md / 백엔드 HttpExceptionFilter 와 1:1 매핑
const ErrorCodeLexicon: Record<string, string> = {
  SYS_001: '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
  AUTH_001: '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.',
  AUTH_002: '안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요.',
  AUTH_003: '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.',
  VAL_001: '입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)',
  RES_001: '요청하신 정보나 페이지를 찾을 수 없습니다.',
  BIZ_001: '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.',
}

// 사용자 노출용 에러 알림 — 전역 공통 다이얼로그(danger)로 표시.
// (인터셉터 실행 시점엔 pinia 가 활성화돼 있어 store 접근 가능)
const showErrorUI = (message: string) => {
  void useDialogStore().alert(message, { variant: 'danger' })
}

// 에러 코드 → 메시지 변환 (미정의 코드는 SYS_001 로 폴백, 항상 문자열 보장)
const SYSTEM_ERROR_MESSAGE =
  ErrorCodeLexicon.SYS_001 ?? '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'
const resolveErrorMessage = (code: string): string => ErrorCodeLexicon[code] ?? SYSTEM_ERROR_MESSAGE

// 사용자 노출 메시지 결정 — 비즈니스 예외(BIZ_001)는 서버가 내려준 구체 문구를 우선 사용
// (CLAUDE.md: "비즈니스 예외는 message 로 사용자 노출 문구를 재정의할 수 있다")
const pickMessage = (code: string, serverMessage?: string): string =>
  code === 'BIZ_001' && serverMessage ? serverMessage : resolveErrorMessage(code)

// 중앙 Axios 인스턴스
const api: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  // HttpOnly/Secure 쿠키 인증 시 자격 증명을 함께 전송
  withCredentials: true,
})

// 백엔드 오리진 — <a href> 다운로드 링크용. axios 와 달리 앵커는 baseURL 을 쓰지 않으므로,
// 백엔드가 내려준 루트 상대 경로('/api/...')에 백엔드 오리진을 붙여 절대 URL 로 만든다.
// (dev 는 프론트 5273 / 백엔드 3100 로 오리진이 달라, 안 붙이면 프론트로 가서 404 페이지가 뜬다)
const API_ORIGIN = new URL(
  import.meta.env.VITE_API_BASE_URL || '/api',
  window.location.origin,
).origin

export function toAbsoluteApiUrl(path: string): string {
  if (!path || /^https?:\/\//i.test(path)) return path
  return `${API_ORIGIN}${path.startsWith('/') ? '' : '/'}${path}`
}

// 요청 인터셉터 — CSRF, 추가 헤더 등 필요 시 확장
api.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => config,
  (error: unknown) => Promise.reject(error),
)

// 인증 만료 응답으로 판단할지 — /auth/* 자체 호출은 재시도 대상에서 제외
const isAuthEndpoint = (url?: string): boolean => (url ?? '').includes('/auth/')

// refresh 단일 실행(single-flight) — 동시 401 이 각자 refresh 를 호출하면 회전형 토큰이
// 재사용으로 오탐되어 세션이 폐기될 수 있다. 진행 중 refresh 가 있으면 그 Promise 를 공유한다.
let refreshInFlight: Promise<void> | null = null
const refreshOnce = (): Promise<void> => {
  if (!refreshInFlight) {
    refreshInFlight = api
      .post('/auth/refresh', null, { silent: true })
      .then(() => undefined)
      .finally(() => {
        refreshInFlight = null
      })
  }
  return refreshInFlight
}

// 응답 인터셉터 — Global Response Wrapper 언래핑 + 401 자동 refresh + 에러코드 매핑
api.interceptors.response.use(
  (response: AxiosResponse<StandardResponse>) => {
    const payload = response.data

    if (payload && typeof payload.success === 'boolean') {
      if (payload.success) {
        // 성공 시 data 만 언래핑하여 반환
        return payload.data as never
      }
      // HTTP 200 이지만 비즈니스 로직상 실패인 경우
      const errCode = payload.error?.code || 'BIZ_001'
      if (!response.config.silent) showErrorUI(pickMessage(errCode, payload.error?.message))
      return Promise.reject(payload.error)
    }
    return payload as never
  },
  async (error) => {
    const config = error?.config as (InternalAxiosRequestConfig & { _retry?: boolean }) | undefined
    const status: number | undefined = error?.response?.status

    // access 토큰 만료(401) → refresh 1회 시도 후 원요청 재시도
    // (동시 401 은 single-flight 로 묶어 refresh 토큰 회전이 한 번만 일어나게 한다)
    if (status === 401 && config && !config._retry && !isAuthEndpoint(config.url)) {
      config._retry = true
      try {
        await refreshOnce()
        return api(config)
      } catch {
        // refresh 실패 → 세션 만료로 간주, 아래 일반 에러 처리로 진행
      }
    }

    let errCode = 'SYS_001'
    let serverMessage: string | undefined
    if (error?.response) {
      const payload = error.response.data as StandardResponse | undefined
      serverMessage = payload?.error?.message
      if (payload?.error?.code) {
        errCode = payload.error.code
      } else if (status === 401) errCode = 'AUTH_001'
      else if (status === 403) errCode = 'AUTH_003'
      else if (status === 400 || status === 422) errCode = 'VAL_001'
      else if (status === 404) errCode = 'RES_001'
    }

    if (!config?.silent) showErrorUI(pickMessage(errCode, serverMessage))
    return Promise.reject(error)
  },
)

/**
 * useApi — 컴포넌트에서 axios 인스턴스를 가져올 때 사용하는 composable
 * 직접 import 대신 useApi() 사용을 권장 (테스트 시 모킹 용이)
 */
export function useApi() {
  return api
}

export default api
