'use server';

import { revalidatePath } from 'next/cache';
import { verifySession } from '@/lib/auth/dal';
import { BackendRequestError } from '@/lib/http/backend-fetch';
import {
  createAdminMember,
  deleteAdminMember,
  isAdminLoginIdTaken,
  updateAdminMember,
} from '@/lib/data/repositories/admin-member-repository';
import { ADMIN_MENU_OPTIONS } from '@/lib/domain/admin-member';
import {
  formatPhoneNumber,
  validateAdminLoginId,
  validateAdminMemberCreate,
  validateAdminMemberUpdate,
  type AdminMemberEditableValues,
  type AdminMemberFormState,
} from '@/lib/domain/admin-member-form';
import { ADMIN_MEMBERS_PATH } from '@/lib/domain/admin-member-query';

/**
 * 관리자 회원 등록/수정/삭제 Server Action.
 *
 * **모든 Action이 `verifySession()`으로 시작한다** — Server Action은 UI를 거치지 않고 직접
 * POST될 수 있어 이 확인이 유일한 최종 방어선이다(설계서 §8).
 *
 * 검증은 화면이 아니라 여기서 확정한다(`lib/domain/admin-member-form.ts`의 규칙을 호출) —
 * 화면의 required 속성은 편의일 뿐 신뢰 경계가 아니다.
 *
 * 실제 저장은 Repository에 맡긴다 — 어느 항목이 백엔드로 가고 어느 것이 mock인지는 이 파일이
 * 알지 못한다(현재 삭제만 mock이다).
 *
 * `AdminMemberFormState` 타입과 그 초깃값(`INITIAL_ADMIN_MEMBER_FORM_STATE`)은 이 파일이 아니라
 * `lib/domain/admin-member-form.ts`에 있다 — Next.js가 `'use server'` 파일에서 함수가 아닌 값을
 * export하는 것을 런타임에 거부하기 때문이다(해당 타입 주석 참조). 타입만 이 파일에서 다시 쓰는
 * 것은 문제 없다 — 타입은 컴파일 시 지워져 런타임 export로 남지 않는다.
 */

const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
const SELF_DELETE_MESSAGE = '현재 로그인한 본인 계정은 삭제할 수 없습니다.';
const DUPLICATE_LOGIN_ID_MESSAGE = '이미 사용 중인 ID입니다.';
const LOGIN_ID_CHECK_FAILED_MESSAGE =
  '중복 확인에 실패했습니다. 잠시 후 다시 시도해 주세요.';

function readString(formData: FormData, key: string): string {
  const value = formData.get(key);
  return typeof value === 'string' ? value : '';
}

/** 체크박스처럼 같은 이름으로 여러 번 오는 값 — 허용 목록에 있는 것만 남긴다. */
function readMenuCodes(formData: FormData): string[] {
  return formData
    .getAll('menuCodes')
    .filter((value): value is string => typeof value === 'string')
    .filter((value) =>
      ADMIN_MENU_OPTIONS.some((option) => option.value === value)
    );
}

/**
 * 등록·수정이 공유하는 입력 항목을 읽는다. 휴대전화번호는 숫자만 담겨 오므로 여기서 저장 형식으로
 * 바꾼다 — 자릿수가 어긋나면 빈 문자열이 되어 검증에서 걸린다.
 */
function readEditableValues(formData: FormData): AdminMemberEditableValues {
  const phoneNumber = formatPhoneNumber(readString(formData, 'phoneNumber'));

  return {
    password: readString(formData, 'password'),
    phoneNumber: phoneNumber ?? '',
    email: readString(formData, 'email'),
    roleCode: readString(formData, 'roleCode'),
    menuCodes: readMenuCodes(formData),
  };
}

/**
 * 쓰기 호출의 백엔드 실패를 폼 상태로 바꾼다. 실패하면 그 상태를, 성공하면 null을 돌려준다.
 *
 * 백엔드가 주는 문구는 그대로 보여준다 — 등록 시 아이디 선점처럼 사용자가 조치할 수 있는 사유가
 * 이 경로로 온다("이미 등록된 아이디 입니다"). 통신 오류·타임아웃은 `backend-fetch`가 이미
 * 일반화된 문구로 바꿔 두므로 내부 사정이 새어 나가지 않는다.
 */
async function runWrite(
  write: () => Promise<void>
): Promise<AdminMemberFormState | null> {
  try {
    await write();
    return null;
  } catch (error) {
    if (error instanceof BackendRequestError) {
      return { status: 'error', message: error.message };
    }
    throw error;
  }
}

/** 시안 ADM_ADM_102_p — 관리자 등록. */
export async function createAdminMemberAction(
  _prevState: AdminMemberFormState,
  formData: FormData
): Promise<AdminMemberFormState> {
  await verifySession();

  const validation = validateAdminMemberCreate({
    ...readEditableValues(formData),
    name: readString(formData, 'name'),
    loginId: readString(formData, 'loginId'),
  });

  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  const { name, loginId, password, phoneNumber, email, roleCode } =
    validation.values;

  // 화면의 [중복 확인]은 편의 기능일 뿐이라 저장 직전에 다시 확인한다 — 확인을 누르지 않고
  // 직접 제출하는 경로가 열려 있고, 확인 후 저장까지의 사이에 선점될 수도 있다.
  if (await isAdminLoginIdTaken(loginId)) {
    return { status: 'error', errors: { loginId: DUPLICATE_LOGIN_ID_MESSAGE } };
  }

  // 메뉴 선택은 넘기지 않는다 — 백엔드에 저장할 곳이 없다(Repository 주석 참조).
  const failure = await runWrite(() =>
    createAdminMember({ name, loginId, password, phoneNumber, email, roleCode })
  );
  if (failure) {
    return failure;
  }

  revalidatePath(ADMIN_MEMBERS_PATH);
  return { status: 'success' };
}

/** 시안 ADM_ADM_103_p — 관리자 수정. 이름·ID는 읽기 전용이라 변경 대상이 아니다. */
export async function updateAdminMemberAction(
  _prevState: AdminMemberFormState,
  formData: FormData
): Promise<AdminMemberFormState> {
  await verifySession();

  const id = readString(formData, 'id');
  if (!id) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const validation = validateAdminMemberUpdate(readEditableValues(formData));
  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  const { password, phoneNumber, email, roleCode } = validation.values;

  const failure = await runWrite(() =>
    updateAdminMember(id, {
      password,
      phoneNumber,
      email,
      roleCode,
      // 시안 ③의 "잠김여부"는 활성/비활성으로 표기되고 활성이 곧 잠기지 않은 상태다.
      isLocked: readString(formData, 'isLocked') === 'true',
    })
  );
  if (failure) {
    return failure;
  }

  revalidatePath(ADMIN_MEMBERS_PATH);
  return { status: 'success' };
}

/**
 * 시안 ADM_ADM_101 ⑤ — 삭제. 확인 얼럿은 화면이 띄우고, 여기서는 권한과 자기 계정만 확인한다.
 *
 * 폼 제출이 아니라 얼럿의 [삭제] 클릭에 반응하는 단발 호출이라 `useActionState`의
 * (prevState, formData) 규약 대신 id를 직접 받는다.
 */
export async function deleteAdminMemberAction(
  id: string
): Promise<AdminMemberFormState> {
  const admin = await verifySession();

  if (!id) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  // 시안의 "최고관리자 본인 계정 삭제 방지". 세션의 adminId는 백엔드 accessToken의 `adminId`
  // 클레임이고 그 값이 곧 목록의 `admUserId`라(EgovJwtTokenUtil.generateAccessToken) 그대로
  // 비교할 수 있다. 역할과 무관하게 "로그인한 본인"을 막는다 — 자기 계정을 지워 스스로
  // 로그인 불가 상태가 되는 것은 어떤 역할이든 사고이기 때문이다.
  if (id === admin.id) {
    return { status: 'error', message: SELF_DELETE_MESSAGE };
  }

  await deleteAdminMember(id);

  revalidatePath(ADMIN_MEMBERS_PATH);
  return { status: 'success' };
}

export type LoginIdCheckResult =
  | { status: 'idle' }
  | { status: 'available'; loginId: string }
  /** 쓸 수 없는 ID — 형식 오류이거나 이미 선점됐다. */
  | { status: 'unavailable'; message: string }
  /** 확인 자체를 못 했다(통신·권한 등). 값의 판정이 아니라 **확인 실패**다. */
  | { status: 'failed'; message: string };

/**
 * 시안 ADM_ADM_102_p ① — ID 중복 확인.
 *
 * 폼 제출이 아니라 버튼 클릭에 반응하는 단발 호출이라 `useActionState`의 (prevState, formData)
 * 규약 대신 값을 직접 받는다 — 등록 폼 안에 또 다른 폼을 중첩할 수 없기 때문이다(HTML 제약).
 * 클라이언트에서 일반 함수처럼 `await` 한다.
 *
 * 형식이 맞지 않는 ID는 중복 여부를 물을 필요도 없이 되돌린다. 확인에 성공하면 **검사한 ID를 함께**
 * 돌려주는데, 화면이 "확인한 ID"와 "지금 입력창의 ID"를 비교해 확인 후 값을 고친 경우를 잡아내기
 * 위해서다.
 */
export async function checkAdminLoginId(
  loginId: string
): Promise<LoginIdCheckResult> {
  await verifySession();

  const normalized = loginId.trim();

  const formatError = validateAdminLoginId(normalized);
  if (formatError) {
    return { status: 'unavailable', message: formatError };
  }

  // 확인 호출이 실패하면 그 사유를 화면에 그대로 돌려준다 — 예외로 두면 클라이언트의
  // transition에서 처리되지 않은 rejection이 되어 버튼만 원복되고 아무 말도 남지 않는다.
  try {
    if (await isAdminLoginIdTaken(normalized)) {
      return { status: 'unavailable', message: DUPLICATE_LOGIN_ID_MESSAGE };
    }
  } catch (error) {
    if (error instanceof BackendRequestError) {
      return { status: 'failed', message: error.message };
    }
    return { status: 'failed', message: LOGIN_ID_CHECK_FAILED_MESSAGE };
  }

  return { status: 'available', loginId: normalized };
}
