'use server';

import { revalidatePath } from 'next/cache';
import { verifySession } from '@/lib/auth/dal';
import {
  createBoardPost,
  deleteBoardPost,
  saveInquiryAnswer,
  updateBoardPost,
} from '@/lib/data/repositories/board-post-repository';
import {
  MAX_ATTACHMENT_BYTES,
  uploadAttachment,
} from '@/lib/data/repositories/file-repository';
import {
  ANSWER_STATUS_DONE,
  type BoardType,
} from '@/lib/domain/board-post';
import {
  validateBoardPostForm,
  validateInquiryAnswerForm,
  type BoardPostFormState,
  type InquiryAnswerFormState,
} from '@/lib/domain/board-post-form';
import { BOARD_PATHS } from '@/lib/domain/board-post-query';
import { FILE_MODULE_ID } from '@/lib/domain/file-module';

/**
 * 게시판(공지사항·1:1문의·FAQ) 등록/수정/삭제/답변 Server Action.
 *
 * 세 화면이 같은 API를 쓰므로 Action도 한 파일이 담당하고, 어느 게시판인지는 폼에 실린
 * `boardType`으로 가른다. **그 값도 신뢰하지 않는다** — 허용 목록에 없으면 즉시 거절한다.
 *
 * **모든 Action이 `verifySession()`으로 시작한다** — Server Action은 UI를 거치지 않고 직접
 * POST될 수 있어 이 확인이 유일한 최종 방어선이다(설계서 §8).
 *
 * 검증은 화면이 아니라 여기서 확정한다(`lib/domain/board-post-form.ts`의 규칙을 호출).
 *
 * `*FormState` 타입과 그 초깃값(`INITIAL_*`)은 이 파일이 아니라 `lib/domain/board-post-form.ts`에
 * 있다 — `'use server'` 파일은 async 함수 외의 값을 export할 수 없다(그 쪽 주석에 근거를 적어 두었다).
 *
 * 첨부파일은 **같은 폼에 실려 온 File을 이 Action이 직접 업로드**한다. 브라우저에서 먼저 업로드해
 * id를 받아 두는 방식보다 단순하고(왕복 1회), 업로드 API가 요구하는 관리자 토큰이 서버에만
 * 있으므로 자연스럽다 — 토큰은 httpOnly 세션 안에 있어 브라우저가 꺼낼 수 없다.
 */

const BOARD_TYPES: readonly BoardType[] = ['notice', 'inquiry', 'faq'];

const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
const UPLOAD_FAILED_MESSAGE =
  '첨부파일을 업로드하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const OVERSIZE_MESSAGE = `첨부파일은 ${Math.floor(
  MAX_ATTACHMENT_BYTES / (1024 * 1024)
)}MB 이하만 올릴 수 있습니다.`;
const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';

async function runWrite(
  write: () => Promise<void>,
  fallback: string
): Promise<BoardPostFormState | null> {
  try {
    await write();
    return null;
  } catch (error) {
    return {
      status: 'error',
      message: error instanceof Error ? error.message : fallback,
    };
  }
}

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

/** 체크박스·라디오는 값이 있을 때만 전송되므로 "Y로 온 경우"만 참으로 본다. */
function readBoolean(formData: FormData, key: string): boolean {
  return readString(formData, key) === 'Y';
}

function readBoardType(formData: FormData): BoardType | null {
  const raw = readString(formData, 'boardType');
  return BOARD_TYPES.find((type) => type === raw) ?? null;
}

/**
 * 새로 첨부된 파일이 있으면 업로드하고 새 id를, 없으면 기존 id를 그대로 돌려준다.
 *
 * 빈 파일(파일을 고르지 않으면 브라우저가 size 0으로 보낸다)은 "선택 안 함"이므로 무시한다 —
 * 이때 기존 첨부를 지우지 않는 것이 의도다(수정 화면에서 첨부를 건드리지 않은 경우).
 */
async function resolveAttachmentId(formData: FormData): Promise<string> {
  const existingId = readString(formData, 'attachmentId');
  const file = formData.get('attachment');

  if (!(file instanceof File) || file.size === 0) {
    return existingId;
  }

  if (file.size > MAX_ATTACHMENT_BYTES) {
    throw new Error(OVERSIZE_MESSAGE);
  }

  return uploadAttachment(file, FILE_MODULE_ID.board);
}

/** 등록·수정이 공유하는 입력 읽기. */
function readPostValues(formData: FormData, attachmentId: string) {
  return {
    categoryCode: readString(formData, 'categoryCode'),
    typeCode: readString(formData, 'typeCode'),
    title: readString(formData, 'title'),
    content: readString(formData, 'content'),
    attachmentId,
    isPinned: readBoolean(formData, 'isPinned'),
    isVisible: readBoolean(formData, 'isVisible'),
    isPushEnabled: readBoolean(formData, 'isPushEnabled'),
    startDate: readString(formData, 'startDate'),
    endDate: readString(formData, 'endDate'),
  };
}

/** 시안 A_BOA_007_p / 015_p — 공지사항·FAQ 신규 등록. */
export async function createBoardPostAction(
  _prevState: BoardPostFormState,
  formData: FormData
): Promise<BoardPostFormState> {
  await verifySession();

  const boardType = readBoardType(formData);
  if (boardType === null || boardType === 'inquiry') {
    // 1:1문의는 사용자가 작성하는 글이라 관리자 등록 경로가 없다(사용자 확인 사항).
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  let attachmentId: string;
  try {
    attachmentId = await resolveAttachmentId(formData);
  } catch (error) {
    return {
      status: 'error',
      message: error instanceof Error ? error.message : UPLOAD_FAILED_MESSAGE,
    };
  }

  const validation = validateBoardPostForm(
    readPostValues(formData, attachmentId),
    { boardType, isCategoryEditable: true }
  );
  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  const failure = await runWrite(
    () => createBoardPost(boardType, validation.values),
    SAVE_FAILED_MESSAGE
  );
  if (failure) {
    return failure;
  }

  revalidatePath(BOARD_PATHS[boardType]);
  return { status: 'success' };
}

/** 시안 A_BOA_008_p / 016_p — 공지사항·FAQ 수정. 구분은 읽기 전용이라 검증 대상이 아니다. */
export async function updateBoardPostAction(
  _prevState: BoardPostFormState,
  formData: FormData
): Promise<BoardPostFormState> {
  await verifySession();

  const boardType = readBoardType(formData);
  const bbsId = readString(formData, 'id');
  if (boardType === null || boardType === 'inquiry' || !bbsId) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  let attachmentId: string;
  try {
    attachmentId = await resolveAttachmentId(formData);
  } catch (error) {
    return {
      status: 'error',
      message: error instanceof Error ? error.message : UPLOAD_FAILED_MESSAGE,
    };
  }

  const validation = validateBoardPostForm(
    readPostValues(formData, attachmentId),
    { boardType, isCategoryEditable: false }
  );
  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  const failure = await runWrite(
    () => updateBoardPost(boardType, bbsId, validation.values),
    SAVE_FAILED_MESSAGE
  );
  if (failure) {
    return failure;
  }

  revalidatePath(BOARD_PATHS[boardType]);
  return { status: 'success' };
}

/** 시안 A_BOA_013_p — 1:1문의 답변 저장. */
export async function saveInquiryAnswerAction(
  _prevState: InquiryAnswerFormState,
  formData: FormData
): Promise<InquiryAnswerFormState> {
  await verifySession();

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

  let attachmentId: string;
  try {
    attachmentId = await resolveAttachmentId(formData);
  } catch (error) {
    return {
      status: 'error',
      message: error instanceof Error ? error.message : UPLOAD_FAILED_MESSAGE,
    };
  }

  const validation = validateInquiryAnswerForm(
    {
      answerStatusCode: readString(formData, 'answerStatusCode'),
      answerContent: readString(formData, 'answerContent'),
      attachmentId,
    },
    { doneStatusCode: ANSWER_STATUS_DONE }
  );
  if (!validation.ok) {
    return { status: 'error', errors: validation.errors };
  }

  const failure = await runWrite(
    () => saveInquiryAnswer(bbsId, validation.values),
    SAVE_FAILED_MESSAGE
  );
  if (failure) {
    return failure;
  }

  revalidatePath(BOARD_PATHS.inquiry);
  return { status: 'success' };
}

/**
 * 삭제(논리 삭제) — 확인 얼럿은 화면이 띄우고 여기서는 권한과 입력만 확인한다.
 *
 * 폼 제출이 아니라 얼럿의 [삭제] 클릭에 반응하는 단발 호출이라 `useActionState`의
 * (prevState, formData) 규약 대신 값을 직접 받는다.
 */
export async function deleteBoardPostAction(
  boardType: BoardType,
  bbsId: string
): Promise<BoardPostFormState> {
  await verifySession();

  if (!BOARD_TYPES.includes(boardType) || !bbsId) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const failure = await runWrite(
    () => deleteBoardPost(bbsId),
    DELETE_FAILED_MESSAGE
  );
  if (failure) {
    return failure;
  }

  revalidatePath(BOARD_PATHS[boardType]);
  return { status: 'success' };
}
