'use server';

import { revalidatePath } from 'next/cache';
import { verifySession } from '@/lib/auth/dal';
import {
  BackendRequestError,
  COMMUNICATION_ERROR_CODE,
} from '@/lib/http/backend-fetch';
import { fetchCommonCodes } from '@/lib/data/repositories/common-code-repository';
import {
  createDecorationItem,
  deleteDecorationItem,
  updateDecorationItem,
  uploadDecorationItemImage,
} from '@/lib/data/repositories/decoration-item-repository';
import { CODE_GROUP } from '@/lib/domain/common-code';
import type { DecorationItemType } from '@/lib/domain/decoration-item';
import {
  validateDecorationItemCreate,
  validateDecorationItemImage,
  validateDecorationItemUpdate,
  type DecorationItemEditableValues,
  type DecorationItemFormState,
} from '@/lib/domain/decoration-item-form';
import { DECORATION_ITEMS_PATH } from '@/lib/domain/decoration-item-query';

/**
 * 꾸미기 아이템 등록/수정/삭제 Server Action.
 *
 * **모든 Action이 `verifySession()`으로 시작한다** — Server Action은 UI를 거치지 않고 직접
 * POST될 수 있어 이 확인이 유일한 최종 방어선이다(설계서 §3 SRP 체크).
 *
 * 검증은 화면이 아니라 여기서 확정한다(`lib/domain/decoration-item-form.ts`의 규칙을 호출) —
 * 화면의 required 속성·기본값 채움은 편의일 뿐 신뢰 경계가 아니다. **허용 카테고리도 여기서
 * 다시 조회해 넘긴다** — 화면이 보낸 코드를 그대로 믿으면 코드테이블에 없는 값이 저장된다.
 *
 * **이미지 업로드도 이 계층을 지난다** — 브라우저는 백엔드를 직접 호출하지 않으므로(토큰이
 * httpOnly 세션 안에만 있다) 폼이 실어 보낸 File을 여기서 받아 Repository를 통해 백엔드에
 * 중계하고, 돌려받은 파일 ID를 그대로 아이템 저장에 쓴다.
 *
 * `DecorationItemFormState` 타입과 그 초깃값은 이 파일이 아니라 domain 계층에 있다 — Next.js가
 * `'use server'` 파일에서 함수가 아닌 값을 export하는 것을 런타임에 거부하기 때문이다
 * (`decoration-item-form.ts`의 해당 타입 주석 참조).
 */

const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const IMAGE_UPLOAD_FAILED_MESSAGE = '이미지를 업로드하지 못했습니다.';

// 백엔드가 사유를 준 경우(확장자·용량·저장소 설정 등)에는 그 말을 그대로 보여준다.
// 통신 실패는 사유가 일반화된 문구뿐이라 그때만 위 메시지로 떨어진다.
function describeUploadFailure(error: unknown): string {
  if (error instanceof BackendRequestError && error.code !== COMMUNICATION_ERROR_CODE) {
    return `${IMAGE_UPLOAD_FAILED_MESSAGE} (${error.message})`;
  }
  return IMAGE_UPLOAD_FAILED_MESSAGE;
}

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

/**
 * 폼 숫자 입력을 파싱한다. `Number('')`이 조용히 `0`이 되는 JS 함정을 피하려고 빈 문자열은
 * 명시적으로 `NaN`으로 취급한다 — 그래야 `Number.isInteger` 검증이 "값이 비었음"을 정상적으로
 * 잡아낸다(포인트 0은 유효한 값이라 "비어서 0"과 "실제로 0"을 구분해야 한다).
 */
function parseFormNumber(formData: FormData, key: string): number {
  const trimmed = readString(formData, key).trim();
  return trimmed === '' ? NaN : Number(trimmed);
}

function parseItemSn(formData: FormData): number | null {
  const value = parseFormNumber(formData, 'itemSn');
  return Number.isInteger(value) && value > 0 ? value : null;
}

/**
 * 새로 선택한 파일과 폼이 유지하고 있던 기존 파일 ID를 함께 읽는다. 등록 화면에는 기존 ID가
 * 없으므로 파일을 고르지 않으면 둘 다 비고, 그 상태는 도메인 검증(`imageFileId` 필수)이 잡는다.
 */
function readImageInput(formData: FormData): {
  newFile: File | null;
  currentImageFileId: string;
} {
  const file = formData.get('imageFile');
  return {
    newFile: file instanceof File && file.size > 0 ? file : null,
    currentImageFileId: readString(formData, 'imageFileId').trim(),
  };
}

/**
 * 업로드 전 검증 단계에서만 쓰는 자리표시자 — "새 파일이 선택됐으므로 이미지 필수 조건은
 * 충족될 예정"이라는 뜻이다. 실제 저장 값으로는 절대 쓰이지 않는다(업로드 성공 후 진짜 파일
 * ID로 교체된다).
 *
 * 검증을 업로드보다 먼저 하기 위해 필요하다 — 순서를 반대로 하면 다른 항목이 잘못됐을 때
 * 파일만 백엔드에 올라간 채 저장은 실패해 고아 파일이 쌓인다(파일 삭제 API를 부를 방법도 없다).
 */
const PENDING_UPLOAD_PLACEHOLDER = 'pending-upload';

/**
 * 등록·수정이 공유하는 입력 항목을 읽는다. 유형·카테고리는 select/radio 값을 그대로 읽고,
 * 허용 목록을 벗어난 값(위조된 요청 포함)은 뒤이은 검증이 걸러낸다 — 여기서 하는 캐스팅은 타입을
 * 맞추는 것일 뿐 신뢰를 부여하지 않는다.
 */
function readEditableValues(
  formData: FormData,
  imageFileId: string
): DecorationItemEditableValues {
  return {
    itemType: readString(formData, 'itemType') as DecorationItemType,
    name: readString(formData, 'name'),
    categoryCode: readString(formData, 'categoryCode'),
    points: parseFormNumber(formData, 'points'),
    description: readString(formData, 'description'),
    isActive: readString(formData, 'isActive') === 'true',
    sortOrder: parseFormNumber(formData, 'sortOrder'),
    imageFileId,
  };
}

/** 시안 ADM_ITM_102_p — 꾸미기 아이템 등록. 아이템ID(itemSn)는 백엔드가 자동 채번한다. */
export async function createDecorationItemAction(
  _prevState: DecorationItemFormState,
  formData: FormData
): Promise<DecorationItemFormState> {
  await verifySession();

  const { newFile, currentImageFileId } = readImageInput(formData);

  const validation = validateDecorationItemCreate(
    readEditableValues(
      formData,
      newFile ? PENDING_UPLOAD_PLACEHOLDER : currentImageFileId
    ),
    await fetchCommonCodes(CODE_GROUP.decorationItemCategory)
  );
  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  let imageFileId = currentImageFileId;
  if (newFile) {
    // 백엔드 저장소 설정에도 확장자·크기 제한이 있지만 걸리면 사유 없이 실패한다 — 시안이
    // 못박은 한도를 여기서 먼저 재서 어느 항목이 왜 잘못됐는지 알려 준다.
    const imageError = validateDecorationItemImage(newFile);
    if (imageError) {
      return { status: 'error', errors: { imageFileId: imageError } };
    }
    try {
      imageFileId = await uploadDecorationItemImage(newFile);
    } catch (error) {
      return { status: 'error', errors: { imageFileId: describeUploadFailure(error) } };
    }
  }

  try {
    await createDecorationItem({ ...validation.values, imageFileId });
  } catch {
    return { status: 'error', message: SAVE_FAILED_MESSAGE };
  }

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

/** 시안 ADM_ITM_103_p — 꾸미기 아이템 수정. 아이템ID는 읽기 전용이라 변경 대상이 아니다. */
export async function updateDecorationItemAction(
  _prevState: DecorationItemFormState,
  formData: FormData
): Promise<DecorationItemFormState> {
  await verifySession();

  const itemSn = parseItemSn(formData);
  if (itemSn === null) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const { newFile, currentImageFileId } = readImageInput(formData);

  const validation = validateDecorationItemUpdate(
    readEditableValues(
      formData,
      newFile ? PENDING_UPLOAD_PLACEHOLDER : currentImageFileId
    ),
    await fetchCommonCodes(CODE_GROUP.decorationItemCategory)
  );
  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  let imageFileId = currentImageFileId;
  if (newFile) {
    // 백엔드 저장소 설정에도 확장자·크기 제한이 있지만 걸리면 사유 없이 실패한다 — 시안이
    // 못박은 한도를 여기서 먼저 재서 어느 항목이 왜 잘못됐는지 알려 준다.
    const imageError = validateDecorationItemImage(newFile);
    if (imageError) {
      return { status: 'error', errors: { imageFileId: imageError } };
    }
    try {
      imageFileId = await uploadDecorationItemImage(newFile);
    } catch (error) {
      return { status: 'error', errors: { imageFileId: describeUploadFailure(error) } };
    }
  }

  try {
    await updateDecorationItem(itemSn, { ...validation.values, imageFileId });
  } catch {
    return { status: 'error', message: SAVE_FAILED_MESSAGE };
  }

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

/**
 * 시안 ADM_ITM_101 — 삭제. 확인 얼럿은 화면(`decoration-item-row-actions.tsx`)이 띄우고,
 * 여기서는 인증과 입력만 확인한다.
 *
 * 폼 제출이 아니라 얼럿의 [삭제] 클릭에 반응하는 단발 호출이라 `useActionState`의
 * (prevState, formData) 규약 대신 식별자를 직접 받는다.
 */
export async function deleteDecorationItemAction(
  itemSn: number
): Promise<DecorationItemFormState> {
  await verifySession();

  if (!Number.isInteger(itemSn) || itemSn <= 0) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  try {
    await deleteDecorationItem(itemSn);
  } catch {
    return { status: 'error', message: DELETE_FAILED_MESSAGE };
  }

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