import 'server-only';
import { getApiBaseUrl, getDevApiAccessToken } from '@/lib/env';

/**
 * 백엔드(edupay-backend) 통신 규약의 단일 지점 (설계서 §4 `server/api-client`).
 *
 * 이 파일이 아는 것은 "백엔드와 말하는 법"뿐이다 — URL 조립, 인증 헤더 부착, 공통 응답 봉투
 * 해석, 실패 정규화. 어떤 화면이 무엇을 조회하는지(엔드포인트·파라미터·필드 매핑)는 각
 * Repository의 책임이라 여기로 들어오지 않는다. 반환 타입이 `unknown`인 것도 그 때문이다 —
 * 응답 shape 검증은 그 shape을 아는 Repository가 한다.
 *
 * 브라우저는 백엔드를 직접 호출하지 않는다(설계서 §7 BFF 전제). `server-only`로 클라이언트
 * 번들 유입 시 빌드가 실패하게 막아 이 전제를 도구로 강제한다.
 */

/** 백엔드 공통 응답 봉투. 성공·실패 모두 이 형태로 온다. */
type ApiEnvelope = {
  success?: unknown;
  code?: unknown;
  message?: unknown;
  data?: unknown;
};

export type ApiErrorKind =
  /** 401 — 토큰이 없거나 만료·무효다. */
  | 'unauthorized'
  /** 백엔드가 입력값을 거절했다(예: code 900 입력값 무결성 오류). */
  | 'validation'
  /** 백엔드가 그 외 실패를 반환했다. */
  | 'server'
  /** 백엔드에 닿지 못했다(DNS·타임아웃·TLS 등). */
  | 'network'
  /** 닿았지만 약속된 형태가 아니다(JSON 파싱 실패, 필드 누락 등). */
  | 'contract';

/**
 * 백엔드 호출 실패의 정규화된 표현 (설계서 §8.1 6층 "실패 안전").
 * 화면에는 이 메시지를 그대로 내보내지 않는다 — 호출부가 일반화된 문구로 바꿔 보여준다.
 */
export class ApiError extends Error {
  readonly kind: ApiErrorKind;
  /** 백엔드가 준 code. HTTP 레벨에서 끊긴 경우엔 HTTP status, 그마저 없으면 null. */
  readonly code: number | null;

  constructor(
    kind: ApiErrorKind,
    message: string,
    code: number | null = null,
    options?: { cause?: unknown }
  ) {
    super(message, options);
    this.name = 'ApiError';
    this.kind = kind;
    this.code = code;
  }
}

/**
 * 요청에 붙일 백엔드 accessToken을 얻는다 — **인증 연동의 유일한 이식 지점**이다.
 *
 * 현재 세션 토큰(`lib/auth/session-token.ts`)은 mock 관리자의 `adminId`만 담고 있어 부착할
 * 백엔드 토큰이 없다. 관리자 로그인 연동(별도 작업)이 들어오면 이 함수 하나만
 * "세션에서 accessToken을 읽어 반환"하도록 바꾸면 되고, 개별 Repository는 손대지 않는다.
 * 그때 개발용 env 폴백(`getDevApiAccessToken`)은 함께 제거한다.
 */
async function resolveAccessToken(): Promise<string | null> {
  return getDevApiAccessToken();
}

/** base URL과 경로를 합친다. base 끝의 `/`·경로 앞의 `/` 유무와 무관하게 같은 URL을 만든다. */
function buildRequestUrl(
  path: string,
  params: Record<string, string | number>
): string {
  const base = getApiBaseUrl().replace(/\/+$/, '');
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
  const url = new URL(`${base}${normalizedPath}`);

  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, String(value));
  }

  return url.toString();
}

/** 실패 응답에서 code/message를 최대한 건져낸다 — 형태가 깨져 있어도 throw하지 않는다. */
function readEnvelopeCode(envelope: ApiEnvelope): number | null {
  return typeof envelope.code === 'number' ? envelope.code : null;
}

function readEnvelopeMessage(envelope: ApiEnvelope, fallback: string): string {
  return typeof envelope.message === 'string' && envelope.message
    ? envelope.message
    : fallback;
}

function classifyFailure(code: number | null): ApiErrorKind {
  if (code === 401 || code === 403) {
    return 'unauthorized';
  }
  // 900 = 입력값 무결성 오류(백엔드 통지 계약).
  if (code === 900) {
    return 'validation';
  }
  return 'server';
}

async function readEnvelope(response: Response): Promise<ApiEnvelope> {
  try {
    const parsed: unknown = await response.json();
    return parsed !== null && typeof parsed === 'object'
      ? (parsed as ApiEnvelope)
      : {};
  } catch {
    return {};
  }
}

/**
 * 백엔드 GET 조회. 성공 시 봉투의 `data`만 돌려주고, 실패는 전부 `ApiError`로 정규화한다.
 *
 * 캐시: `no-store` 고정. 어드민은 데이터 신선도가 우선이고(설계서 §7.1), 조회 대상이
 * 개인정보이며 검색 조건이 매 요청 달라져 재사용 캐시를 둘 이유가 없다.
 *
 * 성공 판정에 `success` 플래그를 단독으로 믿지 않는다 — 실측 결과 401 응답이
 * `{"success":true,"auth":false,"code":401,...}`로 와서 `success`만 보면 실패를 성공으로
 * 읽는다. HTTP 상태와 `code`를 함께 확인한다.
 */
export async function apiGet(
  path: string,
  params: Record<string, string | number> = {}
): Promise<unknown> {
  const url = buildRequestUrl(path, params);
  const accessToken = await resolveAccessToken();

  let response: Response;
  try {
    response = await fetch(url, {
      method: 'GET',
      headers: {
        Accept: 'application/json',
        ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
      },
      cache: 'no-store',
    });
  } catch (cause) {
    throw new ApiError(
      'network',
      `백엔드에 연결하지 못했습니다: ${path}`,
      null,
      { cause }
    );
  }

  const envelope = await readEnvelope(response);
  const code = readEnvelopeCode(envelope) ?? response.status;

  if (!response.ok) {
    throw new ApiError(
      classifyFailure(code),
      readEnvelopeMessage(envelope, `백엔드 응답 실패(HTTP ${response.status})`),
      code
    );
  }

  if (envelope.success !== true || code !== 200) {
    throw new ApiError(
      classifyFailure(code),
      readEnvelopeMessage(envelope, '백엔드 응답 실패'),
      code
    );
  }

  return envelope.data;
}
