import 'server-only';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import {
  BOARD_SETTING_IDS,
  formatBoardCategoryLabel,
  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 f7d601a)의 실제 구현을 읽고 확인한 것이다 —
 * MngrBbsApiController / MngrBbsServiceImpl / MngrBbsMapper.xml / PaginationUtil / CrudLogInterceptor.
 *
 * - **쓰기 중 등록만 폼 인코딩이다.** insert에는 `@RequestBody`가 없고 update에는 있다.
 * - **작성자·수정자·삭제자는 보내지 않는다.** `CrudLogInterceptor`(MyBatis 플러그인)가 인증
 *   주체에서 꺼내 자동 기록한다. 프론트가 보내도 덮어써진다.
 * - **`bbsCd`는 유형 코드다.** 조회가 코드관리 BBS_FAQ_CD와 조인해 이름(`bbsFaqNm`)까지 준다.
 *   반면 쓰기는 요청 VO에도 컨트롤러 복사에도 bbsCd가 없어 **유형이 저장되지 않는다**(보고함).
 * - **앱푸쉬는 끝까지 닿지 않는다.** 요청 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';

/**
 * 한 번에 받아올 최대 행 수. **백엔드 페이징을 쓰지 않고 전체를 받아 여기서 자른다** — 사용자가
 * "전체 조회 후 프론트에서 필터링"을 선택했기 때문이다.
 *
 * 그렇게 정한 이유: ① 백엔드 목록 쿼리의 검색 조건이 제목 하나뿐이고 `searchUseYn`은 쿼리에서
 * 아예 쓰이지 않아 시안의 필터를 백엔드에 맡길 수 없다. ② `totalCount`가 전체 건수가 아니라
 * 페이지가 가득 차면 다음 페이지에 닿을 수 없다. 전체를 손에 쥐면 두 문제가 함께 풀린다.
 *
 * 게시물이 이 상한을 넘으면 그 위로는 조회·검색 대상에서 빠진다 — 그 규모가 되면 백엔드에 count와
 * 검색 조건이 필요하다(상한 인상은 임시방편일 뿐이다).
 */
const BOARD_FETCH_LIMIT = 10_000;

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 fetchAllBoardPosts(boardType: BoardType): Promise<BoardPost[]> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(BOARD_LIST_PATH, {
    method: 'GET',
    query: {
      searchStngId: BOARD_SETTING_IDS[boardType],
      pageIndex: 1,
      recordCountPerPage: BOARD_FETCH_LIMIT,
    },
    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('게시판 목록 응답의 형식이 올바르지 않습니다.');
  }

  return data.list.map(toBoardPost);
}

/** 검색 대상 → 비교할 값. 목록·상세에 실제로 보이는 값으로만 거른다. */
const SEARCH_VALUE_BY_FIELD: Record<string, (post: BoardPost) => string> = {
  category: (post) => formatBoardCategoryLabel(post.categoryCode),
  title: (post) => post.title,
  content: (post) => post.content,
  authorName: (post) => post.authorName ?? '',
  authorPhoneNumber: (post) => post.authorPhoneNumber ?? '',
  answerContent: (post) => post.answerContent ?? '',
};

function matchesQuery(post: BoardPost, query: BoardPostQuery): boolean {
  if (query.category && post.categoryCode !== query.category) {
    return false;
  }
  if (query.type && post.typeCode !== query.type) {
    return false;
  }
  if (query.visibility === 'visible' && post.isVisible !== true) {
    return false;
  }
  if (query.visibility === 'hidden' && post.isVisible === true) {
    return false;
  }

  // 답변여부는 별도 플래그가 없어 답변 내용의 유무로 판단한다(진행상태 코드는 잠정값이라
  // 그것으로 판정하면 코드가 확정될 때 함께 틀어진다).
  const hasAnswer = Boolean(post.answerContent);
  if (query.answered === 'answered' && !hasAnswer) {
    return false;
  }
  if (query.answered === 'unanswered' && hasAnswer) {
    return false;
  }

  const keyword = query.keyword.trim().toLowerCase();
  if (!keyword) {
    return true;
  }

  const readValue = SEARCH_VALUE_BY_FIELD[query.searchField];
  return readValue ? readValue(post).toLowerCase().includes(keyword) : true;
}

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

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

  const offset = (query.page - 1) * query.pageSize;

  return {
    items: matched.slice(offset, offset + query.pageSize),
    totalCount: matched.length,
  };
}

/** 단건 조회 — 수정·답변 팝업의 진입점. */
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',
    form: 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);
  }
}
