import 'server-only';
import { cache } from 'react';
import { redirect } from 'next/navigation';
import { readSessionToken } from '@/lib/auth/session';
import { verifySessionToken, type SessionTokenPayload } from '@/lib/auth/session-token';
import { decodeJwtPayload } from '@/lib/auth/decode-jwt-payload';
import type { AdminUser } from '@/lib/domain/admin-user';

/**
 * 공식 가이드의 Data Access Layer(DAL) — 인증·인가 판단을 이 파일에 집중시킨다.
 * `cache()`로 렌더 패스당 세션 조회를 1회로 dedupe한다.
 *
 * 이후 모든 mutation(Server Action)은 Repository를 직접 호출하기 전에 반드시
 * `verifySession()`(또는 `getSessionAdmin()`)을 거쳐야 한다 — Server Action은 UI를 거치지 않고
 * 직접 호출될 수 있으므로 이 검증이 유일한 최종 방어선이다.
 *
 * 백엔드에 관리자 프로필 조회 API가 없어(2026-08 기준) 세션 payload에 이미 실려 있는 클레임
 * (adminId/loginId/admRoleCd)만으로 AdminUser를 구성한다 — Repository 왕복이 없다.
 */

/** 백엔드 accessToken(JWT) payload에서 신선도 검사에 필요한 클레임만 최소로 본다. */
type AccessTokenFreshnessClaims = {
  exp: unknown;
  adminId: unknown;
  loginId: unknown;
};

/**
 * 세션에 실린 백엔드 accessToken이 여전히 신선한지 검사한다 — **서명 검증이 아니다.** 위조
 * 방지는 이미 이 세션 자체의 HMAC 서명(verifySessionToken)이 담당하고, 이 함수는 그 서명을
 * 통과한 세션 안에 실린 accessToken이 그 뒤로 "만료·손상·불일치" 상태가 되지 않았는지만 본다
 * (decode-jwt-payload.ts 상단 주석의 신뢰 구분과 동일한 경계).
 *
 * 세 가지를 확인한다:
 *   1. 형식 — JWT 3-파트 구조이고 payload가 파싱 가능한가. `decodeJwtPayload`가 실패하면
 *      null을 반환하므로 그대로 무효 처리한다.
 *   2. 만료 — accessToken 자체의 `exp` 클레임이 아직 미래인가. 세션 HMAC 토큰의 `exp`
 *      (verifySessionToken이 이미 검사함)와는 **별개 값**이다 — 로그인 시점엔 두 값이 같지만
 *      (createSession이 백엔드 accessToken의 exp를 그대로 세션 exp로 쓴다) 그 사실이 이 검사의
 *      존재 이유를 없애지 않는다: 세션 HMAC의 exp 검사는 "우리 세션 자체가 안 끊겼는지"만
 *      보장하고, 이 검사는 "그 세션이 담고 있는 백엔드 토큰이 여전히 유효한지"를 재확인한다.
 *   3. 유저 일치 — 토큰 클레임의 adminId·loginId가 세션 payload에 저장된 값(로그인 시점에 같은
 *      토큰에서 읽어 저장해 둔 값, lib/data/repositories/auth-repository.ts 참고)과 같은가.
 *      다르면 토큰 바꿔치기·세션 오염 신호이므로 무효로 취급한다.
 *
 * clock skew 허용치는 두지 않는다 — verifySessionToken()의 기존 exp 검사도 허용치 없이
 * `exp <= now`로 엄격하게 판정하고(session-token.ts), 이 서버가 백엔드와 시계를 맞출 별도
 * 장치가 없는 단일 배포 환경이라 허용치를 두면 "이미 만료된 토큰을 짧게 유효로 오판"하는
 * 위험만 늘어난다.
 */
function isAccessTokenFresh(session: SessionTokenPayload): boolean {
  const claims = decodeJwtPayload<AccessTokenFreshnessClaims>(session.accessToken);
  if (!claims) {
    return false;
  }

  if (typeof claims.exp !== 'number') {
    return false;
  }
  const now = Math.floor(Date.now() / 1000);
  if (claims.exp <= now) {
    return false;
  }

  return claims.adminId === session.adminId && claims.loginId === session.loginId;
}

/**
 * 서명·형식·만료·유저 일치까지 모두 통과한 세션 payload만 반환한다 — `getSessionAdmin`·
 * `getSessionAccessToken` 공통 진입점(이 파일 밖으로 내보내지 않는 내부 헬퍼). 검사 로직을 이
 * 함수 하나에만 두어 두 공개 함수가 중복 구현 없이 같은 판단을 공유하게 한다. 렌더 패스당
 * 1회로 dedupe(cache()).
 */
const getValidSessionPayload = cache(
  async (): Promise<SessionTokenPayload | null> => {
    const token = await readSessionToken();
    if (!token) {
      return null;
    }

    const session = verifySessionToken(token);
    if (!session) {
      return null;
    }

    return isAccessTokenFresh(session) ? session : null;
  }
);

/** 세션 검증 결과를 null 허용으로 반환 — redirect 없이 상태만 알고 싶을 때 사용 (예: 로그인 페이지). */
export const getSessionAdmin = cache(async (): Promise<AdminUser | null> => {
  const session = await getValidSessionPayload();
  if (!session) {
    return null;
  }

  return {
    id: session.adminId,
    // 백엔드 accessToken에 관리자 이름 클레임이 없어 loginId로 대체한다(사용자 확정 사항).
    name: session.loginId,
    loginId: session.loginId,
    roleCode: session.admRoleCd,
  };
});

/**
 * 백엔드 호출에 부착할 accessToken을 세션에서 꺼낸다 — **서버 전용, 화면으로 내려보내지 않는다.**
 *
 * `getSessionAdmin()`이 반환하는 `AdminUser`에는 토큰이 없다(민감 값이라 화면 DTO에서 의도적으로
 * 제외). 그래서 백엔드 API를 부르는 Repository용 경로를 따로 둔다. 세션이 없거나 위조·만료·
 * 손상·유저 불일치(모두 `isAccessTokenFresh`가 판정)면 null이며, 그 경우 호출부는 Authorization
 * 없이 요청하게 되고 백엔드가 401로 거절한다(fail-closed).
 */
export const getSessionAccessToken = cache(async (): Promise<string | null> => {
  const session = await getValidSessionPayload();
  return session?.accessToken ?? null;
});

/** 보호 라우트·Server Action 진입점 — 세션이 없으면 `/login`으로 redirect한다. */
export async function verifySession(): Promise<AdminUser> {
  const admin = await getSessionAdmin();
  if (!admin) {
    redirect('/login');
  }

  return admin;
}
