import 'server-only';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import {
  BOARD_SETTING_IDS,
  type BoardPost,
  type BoardType,
} from '@/lib/domain/board-post';
import type { BoardPostQuery } from '@/lib/domain/board-post-query';

/**
 * 게시판 Repository — 공지사항·1:1문의·FAQ가 **같은 백엔드 API**를 쓰므로 한 파일이 셋을 모두
 * 담당하고, 게시판 구분은 `BoardType` → `stngId` 변환으로만 처리한다.
 *
 *   GET    /api/v1/mngr/bbs/pagination   (searchStngId 필수)
 *   GET    /api/v1/mngr/bbs/{bbsId}
 *   POST   /api/v1/mngr/bbs
 *   PUT    /api/v1/mngr/bbs/{bbsId}
 *   DELETE /api/v1/mngr/bbs/{bbsId}      (논리 삭제 — DEL_YN='Y')
 *
 * 아래는 백엔드 저장소(edupay-backend origin/develop 821f655)의 실제 구현을 읽고 확인한 것이다 —
 * MngrBbsApiController / MngrBbsServiceImpl / MngrBbsMapper.xml / PaginationUtil / CrudLogInterceptor.
 *
 * - **쓰기는 모두 JSON이다.** 종전에는 등록만 `@RequestBody`가 없어 폼이었는데 821f655에서 붙었다.
 * - **작성자·수정자·삭제자는 보내지 않는다.** `CrudLogInterceptor`(MyBatis 플러그인)가 인증
 *   주체에서 꺼내 자동 기록한다. 프론트가 보내도 덮어써진다.
 * - **`bbsCd`는 유형 코드다.** 조회가 코드관리 BBS_FAQ_CD와 조인해 이름(`bbsFaqNm`)까지 준다.
 *   반면 쓰기는 요청 VO에도 컨트롤러 복사에도 bbsCd가 없어 **유형이 저장되지 않는다**(보고함).
 *   INSERT/UPDATE SQL에는 자리가 있어, 보내는 값은 그대로 두면 VO에 필드가 생기는 즉시 동작한다.
 * - **앱푸쉬는 끝까지 닿지 않는다.** 요청 VO·컨트롤러까지는 pushYn이 오지만 INSERT/UPDATE/SELECT
 *   SQL 어디에도 PUSH_YN이 없다(보고함). 노출기간(START_DT·END_DT)은 이제 조회까지 온다.
 * - **구분 값이 없다.** 협의 중이라 화면 입력은 유지하되 조회·전송 모두 비워 둔다(사용자 확정).
 *
 * 인증: `/api/v1/mngr/**`는 ROLE_ADMIN 전용. 캐시: `no-store`(조건이 매 요청 다르고 관리 데이터다).
 */

const BOARD_LIST_PATH = '/api/v1/mngr/bbs/pagination';
const BOARD_BASE_PATH = '/api/v1/mngr/bbs';


function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === 'object';
}

function readString(source: Record<string, unknown>, key: string): string | null {
  const value = source[key];
  return typeof value === 'string' && value.length > 0 ? value : null;
}

/** 백엔드의 Y/N 플래그 → boolean. 값이 없거나 Y/N이 아니면 "모름"(null)이다. */
function parseYesNo(value: unknown): boolean | null {
  if (value === 'Y') return true;
  if (value === 'N') return false;
  return null;
}

/** `inqCnt`는 VO 타입이 String이라 숫자 문자열로 온다 — 숫자로 못 읽으면 null. */
function parseCount(value: unknown): number | null {
  if (typeof value === 'number') return value;
  if (typeof value === 'string' && value.trim() !== '') {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

/**
 * 백엔드 응답 1건 → 도메인 타입. 백엔드가 주지 않는 항목은 `null`로 둔다.
 *
 * `bbsId`만 없으면 예외로 끊는다(fail-fast) — 행의 key이자 수정·삭제의 입력값이라 없으면 목록
 * 자체가 성립하지 않는다. 제목·내용은 비어 있어도 화면이 `-`로 표시하면 되므로 끊지 않는다.
 */
function toBoardPost(raw: unknown): BoardPost {
  if (!isRecord(raw)) {
    throw new Error('게시판 응답 항목의 형식이 올바르지 않습니다.');
  }

  const id = readString(raw, 'bbsId');
  if (id === null) {
    throw new Error('게시판 응답에 bbsId가 없습니다.');
  }

  return {
    id,
    title: readString(raw, 'bbsNm') ?? '',
    content: readString(raw, 'bbsCn') ?? '',
    // 이슈: 구분 값은 백엔드에 아직 없다(협의 중). bbsCd는 구분이 아니라 유형이다.
    categoryCode: null,
    typeCode: readString(raw, 'bbsCd'),
    typeName: readString(raw, 'bbsFaqNm'),
    attachmentId: readString(raw, 'atchFileId'),
    isPinned: parseYesNo(raw.hghrkYn),
    isVisible: parseYesNo(raw.rlsYn),
    startDate: readString(raw, 'startDt'),
    endDate: readString(raw, 'endDt'),
    isPushEnabled: parseYesNo(raw.pushYn),
    authorName: readString(raw, 'regNm'),
    createdAt: readString(raw, 'regDt'),
    viewCount: parseCount(raw.inqCnt),
    authorPhoneNumber: readString(raw, 'telNo'),
    authorEmail: readString(raw, 'emlAddr'),
    answerContent: readString(raw, 'ansCn'),
    answeredAt: readString(raw, 'ansDt'),
    answererName: readString(raw, 'answrNm'),
    answerStatusCode: readString(raw, 'ansSttsCd'),
  };
}

/** 게시판 전체를 한 번에 받아온다(검색·필터·페이징은 호출부가 처리한다). */
async function requestBoardPage(
  boardType: BoardType,
  params: Record<string, string | number>
): Promise<{ items: BoardPost[]; totalCount: number }> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(BOARD_LIST_PATH, {
    method: 'GET',
    query: { searchStngId: BOARD_SETTING_IDS[boardType], ...params },
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
  });

  if (!result.ok) {
    throw new BackendRequestError(result);
  }

  const data = result.data;
  if (!isRecord(data) || !Array.isArray(data.list)) {
    throw new Error('게시판 목록 응답의 형식이 올바르지 않습니다.');
  }

  const totalCount =
    typeof data.totalCount === 'number' ? data.totalCount : data.list.length;

  return { items: data.list.map(toBoardPost), totalCount };
}

export type BoardPostPage = {
  items: BoardPost[];
  /** 검색·필터를 적용한 전체 건수. 전체를 손에 쥐고 세므로 확정값이다. */
  totalCount: number;
};

/**
 * 검색·필터·페이징이 적용된 목록을 조회한다.
 *
 * 정렬은 하지 않는다 — 백엔드 목록 SQL이 `ORDER BY rnum DESC`(= 최초등록일시 최신순)로 고정돼
 * 있고 그것이 곧 시안의 "최근등록순"이다. 시안의 정렬 select에도 다른 선택지가 없다.
 */
export async function fetchBoardPosts(
  boardType: BoardType,
  query: BoardPostQuery
): Promise<BoardPostPage> {
  const params: Record<string, string | number> = {
    pageIndex: query.page,
    recordCountPerPage: query.pageSize,
  };

  // 백엔드가 거를 수 있는 둘만 넘긴다 — 제목(searchCondition=1)과 노출여부(RLS_YN).
  // 나머지 조건은 화면에서 감췄다(`BOARD_SEARCH_FIELDS`·`BOARD_QUERY_FIELDS`).
  const keyword = query.keyword.trim();
  if (keyword && query.searchField === 'title') {
    params.searchCondition = '1';
    params.searchKeyword = keyword;
  }
  if (query.visibility === 'visible') {
    params.searchUseYn = 'Y';
  } else if (query.visibility === 'hidden') {
    params.searchUseYn = 'N';
  }

  return requestBoardPage(boardType, params);
}

/** 단건 조회 — 수정·답변 팝업의 진입점. */
export async function findBoardPostById(
  bbsId: string
): Promise<BoardPost | null> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(`${BOARD_BASE_PATH}/${bbsId}`, {
    method: 'GET',
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
  });

  if (!result.ok) {
    throw new BackendRequestError(result);
  }

  return result.data === null ? null : toBoardPost(result.data);
}

/** boolean → 백엔드 Y/N 플래그. */
function toYesNo(value: boolean): string {
  return value ? 'Y' : 'N';
}

export type BoardPostWriteInput = {
  categoryCode: string;
  typeCode: string;
  title: string;
  content: string;
  attachmentId: string;
  isPinned: boolean;
  isVisible: boolean;
  isPushEnabled: boolean;
  startDate: string;
  endDate: string;
};

/**
 * 등록/수정 공통 파라미터.
 *
 * 빈 문자열은 보내지 않는다 — 수정 SQL이 `<if test="... != null">`로 동적 SET을 만들기 때문에,
 * 빈 문자열을 보내면 기존 값을 빈 값으로 덮어쓴다(예: 첨부파일을 안 건드렸는데 지워지는 상황).
 * "값을 안 보냄 = 기존 유지"가 백엔드의 규약이라 그에 맞춘다.
 */
function buildWriteParams(
  boardType: BoardType,
  input: BoardPostWriteInput
): Record<string, string> {
  const params: Record<string, string> = {
    stngId: BOARD_SETTING_IDS[boardType],
    bbsNm: input.title,
    bbsCn: input.content,
    hghrkYn: toYesNo(input.isPinned),
    rlsYn: toYesNo(input.isVisible),
    pushYn: toYesNo(input.isPushEnabled),
  };

  // 이슈: bbsCd(유형)는 요청 VO·컨트롤러 복사에 없어 백엔드가 무시한다. 추가되는 즉시
  // 동작하도록 실제 컬럼 이름으로 보낸다. 구분은 백엔드에 자리가 없어 보내지 않는다(협의 중).
  if (input.typeCode) params.bbsCd = input.typeCode;
  if (input.attachmentId) params.atchFileId = input.attachmentId;
  if (input.startDate) params.startDt = input.startDate;
  if (input.endDate) params.endDt = input.endDate;

  return params;
}

export async function createBoardPost(
  boardType: BoardType,
  input: BoardPostWriteInput
): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<null>(BOARD_BASE_PATH, {
    method: 'POST',
    body: buildWriteParams(boardType, input),
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    // 쓰기 API는 성공해도 `data: null`을 돌려준다(ApiResponseVO.success(null)).
    canHaveNullData: true,
  });

  if (!result.ok) {
    throw new BackendRequestError(result);
  }
}

export async function updateBoardPost(
  boardType: BoardType,
  bbsId: string,
  input: BoardPostWriteInput
): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<null>(`${BOARD_BASE_PATH}/${bbsId}`, {
    method: 'PUT',
    body: buildWriteParams(boardType, input),
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    // 쓰기 API는 성공해도 `data: null`을 돌려준다(ApiResponseVO.success(null)).
    canHaveNullData: true,
  });

  if (!result.ok) {
    throw new BackendRequestError(result);
  }
}

export type InquiryAnswerInput = {
  answerStatusCode: string;
  answerContent: string;
  attachmentId: string;
};

/**
 * 1:1문의 답변 저장 — 백엔드에 전용 엔드포인트가 없어 수정 API에 답변 필드를 실어 보낸다.
 *
 * 제목·내용은 보내지 않는다(질문자가 쓴 값이라 관리자가 건드릴 이유가 없고, 위에서 적었듯
 * 안 보내면 기존 값이 유지된다). `stngId`는 수정 SQL의 WHERE 조건이라 반드시 필요하다.
 */
export async function saveInquiryAnswer(
  bbsId: string,
  input: InquiryAnswerInput
): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const payload: Record<string, string> = {
    stngId: BOARD_SETTING_IDS.inquiry,
    ansSttsCd: input.answerStatusCode,
  };
  if (input.answerContent) payload.ansCn = input.answerContent;
  if (input.attachmentId) payload.atchFileId = input.attachmentId;

  const result = await backendFetch<null>(`${BOARD_BASE_PATH}/${bbsId}`, {
    method: 'PUT',
    body: payload,
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    // 쓰기 API는 성공해도 `data: null`을 돌려준다(ApiResponseVO.success(null)).
    canHaveNullData: true,
  });

  if (!result.ok) {
    throw new BackendRequestError(result);
  }
}

/** 삭제 — 백엔드가 물리 삭제가 아니라 `DEL_YN='Y'`로 표시만 바꾼다. */
export async function deleteBoardPost(bbsId: string): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<null>(`${BOARD_BASE_PATH}/${bbsId}`, {
    method: 'DELETE',
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    // 쓰기 API는 성공해도 `data: null`을 돌려준다(ApiResponseVO.success(null)).
    canHaveNullData: true,
  });

  if (!result.ok) {
    throw new BackendRequestError(result);
  }
}
