import 'server-only';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import type {
  ContentKeyword,
  QuizAnswerText,
  QuizItem,
} from '@/lib/domain/content';
import type { ContentQuery } from '@/lib/domain/content-query';
import type { QuizFormValues } from '@/lib/domain/content-form';

/**
 * 금융OX퀴즈 Repository — 쇼츠·만화와 테이블(`TB_COM_CNTNTS_QUIZ`)은 다르지만 콘텐츠관리
 * API 아래로 들어와 규약은 같다.
 *
 * ```
 * GET    /api/v1/mngr/cntntns/quiz/pagination      학교·학년·난이도·검색어
 * GET    /api/v1/mngr/cntntns/quiz/{quizId}        단건
 * POST   /api/v1/mngr/cntntns/quiz                 @RequestBody(JSON)
 * PUT    /api/v1/mngr/cntntns/quiz/{quizId}        @RequestBody(JSON)
 * DELETE /api/v1/mngr/cntntns/quiz/{quizId}        soft delete(DEL_YN)
 * GET    /api/v1/mngr/cntntns/quiz/excel/download  엑셀 다운로드
 * ```
 *
 * 학교·학년은 `TB_COM_QUIZ_SCHUL_GRD`에 따로 저장되고 `schulGrdList`로 오간다 — 쇼츠·만화와
 * 같은 모양이라 화면 쪽 취급도 같다.
 *
 * ⚠️ 백엔드 결함(보고함, 미수정)
 * 1. 검색어가 질문·정답만 본다(`QUIZ_ANSWER`·`QUIZ_QUESTION`). 화면의 「키워드」 검색은
 *    `QUIZ_TITLE`을 봐야 하는데 조건에 없어 결과가 비어 온다.
 * 2. 난이도 필터가 SQL에 없다 — `searchLevelCd`를 받지만 `searchWhere`가 쓰지 않는다.
 *    화면 선택지는 기획대로 두고 값만 실어 보낸다.
 */

const QUIZ_PATH = '/api/v1/mngr/cntntns/quiz';

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];
  if (typeof value === 'string' && value.length > 0) return value;
  if (typeof value === 'number') return String(value);
  return null;
}

/** `schulGrdList` → 화면이 쓰는 학교 코드 + 학년 코드 목록(쇼츠·만화와 같은 규칙). */
/** 키워드 목록 — 백엔드 `CntntsKeywordVo` 배열. 키워드가 빈 줄은 버린다. */
function toKeywords(raw: unknown): ContentKeyword[] {
  if (!Array.isArray(raw)) {
    return [];
  }
  return raw.flatMap((entry) => {
    if (!isRecord(entry)) {
      return [];
    }
    const keyword = readString(entry, 'cntntsKeyword');
    if (!keyword) {
      return [];
    }
    return [{ keyword, description: readString(entry, 'cntntsExpln') ?? '' }];
  });
}

function toSchoolGrades(raw: unknown): { schoolCode: string; grades: string[] } {
  if (!Array.isArray(raw)) {
    return { schoolCode: '', grades: [] };
  }

  const grades: string[] = [];
  let schoolCode = '';

  for (const entry of raw) {
    if (!isRecord(entry)) {
      continue;
    }
    schoolCode = schoolCode || (readString(entry, 'schulCd') ?? '');
    const grade = readString(entry, 'grdCd');
    if (grade && !grades.includes(grade)) {
      grades.push(grade);
    }
  }

  return { schoolCode, grades };
}

function toSchoolGradePayload(schoolCode: string, grades: readonly string[]) {
  if (!schoolCode) {
    return [];
  }
  if (grades.length === 0) {
    // 학년 없는 학교(전체연령 등)는 학교만 남긴다 — 빈 문자열을 넣으면 GRD_CD에 ''가 박힌다.
    return [{ schulCd: schoolCode, grdCd: null }];
  }
  return grades.map((grade) => ({ schulCd: schoolCode, grdCd: grade }));
}

function toQuizItem(raw: unknown): QuizItem {
  if (!isRecord(raw)) {
    throw new Error('퀴즈 응답 항목의 형식이 올바르지 않습니다.');
  }

  const quizId = readString(raw, 'cntntsId');
  if (!quizId) {
    throw new Error('퀴즈 응답에 cntntsId가 없습니다.');
  }

  const schulGrd = toSchoolGrades(raw.schulGrdList);

  return {
    quizId,
    keywords: toKeywords(raw.keywordList),
    level: readString(raw, 'cntntsQuizLev') ?? '',
    question: readString(raw, 'cntntsQuizQuestion') ?? '',
    explanation: readString(raw, 'cntntsQuizExplan') ?? '',
    answer: readString(raw, 'cntntsQuizAnswer') ?? '',
    schoolCode: schulGrd.schoolCode,
    grades: schulGrd.grades,
    answerMessages: toAnswerMessages(raw.descrList),
    isVisible: readString(raw, 'useYn') !== 'N',
    modifiedAt:
      readString(raw, 'lastMdfcnDtStr') ?? readString(raw, 'frstRegDtStr'),
  };
}

function buildListParams(
  query: ContentQuery,
  pageIndex: number,
  recordCountPerPage: number
): Record<string, string | number> {
  // 검색어는 보내지 않는다 — 백엔드 조건이 이름이 바뀌기 전 컬럼을 보고 있어 실으면 목록이
  // SQL 오류로 죽는다. 난이도도 받기만 하고 쓰지 않아 함께 뺀다. 둘 다 화면에서 감췄다.
  return {
    searchSchulGradeCd: query.school,
    searchGradeCd: query.grade,
    pageIndex,
    recordCountPerPage,
  };
}

async function requestList(
  query: ContentQuery,
  pageIndex: number,
  recordCountPerPage: number
): Promise<{ items: QuizItem[]; reportedTotalCount: number }> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(`${QUIZ_PATH}/pagination`, {
    method: 'GET',
    query: buildListParams(query, pageIndex, recordCountPerPage),
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    canHaveNullData: true,
    canHaveEmptyBody: true,
  });

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

  const data = result.data;
  if (data === null || data === undefined) {
    return { items: [], reportedTotalCount: 0 };
  }
  if (!isRecord(data) || !Array.isArray(data.list)) {
    throw new Error('퀴즈 목록 응답의 형식이 올바르지 않습니다.');
  }

  return {
    items: data.list.map(toQuizItem),
    reportedTotalCount:
      typeof data.totalCount === 'number' ? data.totalCount : data.list.length,
  };
}

export type QuizPage = {
  items: QuizItem[];
  totalCount: number;
  isTotalCountExact: boolean;
};

export async function fetchQuizzes(query: ContentQuery): Promise<QuizPage> {
  const { items, reportedTotalCount } = await requestList(
    query,
    query.page,
    query.pageSize
  );

  const confirmedCount = (query.page - 1) * query.pageSize + items.length;

  return {
    items,
    totalCount: Math.max(reportedTotalCount, confirmedCount),
    isTotalCountExact:
      items.length < query.pageSize || reportedTotalCount > confirmedCount,
  };
}

export async function fetchQuizDetail(quizId: string): Promise<QuizItem | null> {
  const accessToken = await getSessionAccessToken();

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

  if (!result.ok) {
    throw new BackendRequestError(result);
  }
  if (result.data === null || result.data === undefined) {
    return null;
  }

  return toQuizItem(result.data);
}

/** 키워드 목록의 필드명. 백엔드 `CntntsKeywordVo`와 같다(콘텐츠 API와 동일한 구조다). */
const QUIZ_KEYWORD_FIELD = {
  /** 목록 자체의 이름. */
  list: 'keywordList',
  /** 항목의 키워드 필드. */
  keyword: 'cntntsKeyword',
  /** 항목의 설명 필드. */
  description: 'cntntsExpln',
} as const;

type QuizKeywordPayload = Record<string, string>;

/**
 * 정답별 노출 문구 — 백엔드 `descrList`(`CntntsQuizDescrVo` 배열, `TB_COM_CNTNTS_QUIZ_DESCR`).
 *
 * 칸이 하나(`CNTNTS_QUIZ_DESCR`)뿐이라 코드가 아니라 **문구 자체**를 싣는다. 학습자 앱이
 * 이 값을 그대로 말풍선에 띄우기 때문이다(`CmmBotChatServiceImpl`이 조회해 응답에 넣는다).
 */
type QuizAnswerDescrPayload = {
  cntntsQuizAnswer: string;
  cntntsQuizDescr: string;
};

/** `descrList` → 화면이 수정 폼에 되살릴 정답별 문구. */
function toAnswerMessages(raw: unknown): QuizAnswerText[] {
  if (!Array.isArray(raw)) {
    return [];
  }
  return raw.flatMap((entry) => {
    if (!isRecord(entry)) {
      return [];
    }
    const answer = readString(entry, 'cntntsQuizAnswer');
    if (!answer) {
      return [];
    }
    return [{ answer, message: readString(entry, 'cntntsQuizDescr') ?? '' }];
  });
}

type QuizWritePayload = {
  cntntsQuizLev: string;
  cntntsQuizQuestion: string;
  cntntsQuizExplan: string;
  cntntsQuizAnswer: string;
  useYn: string;
  keywordList: QuizKeywordPayload[];
  descrList: QuizAnswerDescrPayload[];
  schulGrdList: Array<{ schulCd: string; grdCd: string | null }>;
};

function buildQuizPayload(values: QuizFormValues): QuizWritePayload {
  return {
    cntntsQuizLev: values.level,
    cntntsQuizQuestion: values.question,
    cntntsQuizExplan: values.explanation,
    cntntsQuizAnswer: values.answer,
    useYn: values.isVisible ? 'Y' : 'N',
    keywordList: values.keywords.map((item) => ({
      [QUIZ_KEYWORD_FIELD.keyword]: item.keyword,
      [QUIZ_KEYWORD_FIELD.description]: item.description,
    })),
    // `text`에는 검증 단계가 고른 문구를 넣어 둔다(코드를 골랐으면 그 코드명).
    descrList: values.answerMessages.map((message) => ({
      cntntsQuizAnswer: message.answer,
      cntntsQuizDescr: message.text,
    })),
    schulGrdList: toSchoolGradePayload(values.schoolCode, values.grades),
  };
}

async function sendWrite(
  path: string,
  method: 'POST' | 'PUT' | 'DELETE',
  body?: unknown
): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(path, {
    method,
    ...(body === undefined ? {} : { body }),
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    canHaveNullData: true,
    canHaveEmptyBody: true,
  });

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

export async function createQuiz(values: QuizFormValues): Promise<void> {
  await sendWrite(QUIZ_PATH, 'POST', buildQuizPayload(values));
}

export async function updateQuiz(
  quizId: string,
  values: QuizFormValues
): Promise<void> {
  await sendWrite(
    `${QUIZ_PATH}/${encodeURIComponent(quizId)}`,
    'PUT',
    buildQuizPayload(values)
  );
}

export async function deleteQuiz(quizId: string): Promise<void> {
  await sendWrite(`${QUIZ_PATH}/${encodeURIComponent(quizId)}`, 'DELETE');
}
