import 'server-only';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import { buildFileImageUrl } from '@/lib/data/repositories/file-repository';
import { CONTENT_TYPE_CODE } from '@/lib/domain/content';
import type { ContentItem, ContentKeyword } from '@/lib/domain/content';
import type { ContentQuery } from '@/lib/domain/content-query';
import type {
  ComicFormValues,
  ShortsFormValues,
} from '@/lib/domain/content-form';

/**
 * 금융쇼츠·4컷금융만화 Repository — 두 화면은 같은 테이블(`TB_COM_CNTNTS`)을 쓰고
 * `CNTNTS_TYPE`으로 갈린다(`V` 쇼츠 / `T` 만화).
 *
 * ```
 * GET    /api/v1/mngr/cntntns/pagination    searchCntntsType 필수
 * GET    /api/v1/mngr/cntntns/{cntntsSn}    단건
 * POST   /api/v1/mngr/cntntns               어노테이션 없음 → form(중첩 목록은 인덱스 표기)
 * PUT    /api/v1/mngr/cntntns/{cntntsSn}    @RequestBody(JSON)
 * DELETE /api/v1/mngr/cntntns/{cntntsSn}    soft delete(DEL_YN)
 * ```
 *
 * 이슈: 경로가 `cntnts`가 아니라 **`cntntns`**다(백엔드 오타). 고쳐지면 아래 상수만 바꾼다.
 *
 * ⚠️ 백엔드 결함(보고함, 미수정)
 * 1. `selectPaginationCount`가 `FROM TB_COM_CNTNTS`만 두고 `searchWhere`를 그대로 포함한다.
 *    그 조건은 `c.`·`k.`·`g.` 별칭을 쓰므로 별칭을 모르는 count 쿼리에서 SQL이 깨진다.
 * 2. 학교·학년 조건의 `<if test='searchSchulGradeCd != null != ""'>`가 OGNL에서 항상 참이고,
 *    본문도 값 대신 `#{searchKeyword}`를 비교한다. 파라미터를 보내도 의미가 없어 보내지 않고,
 *    학교·학년 필터는 받아온 뒤 여기서 건다.
 * 3. 목록 SQL이 키워드·학교급·파일을 모두 LEFT JOIN해 한 콘텐츠가 여러 행이 된다. LIMIT이 그
 *    늘어난 행에 걸려 한 페이지에 오는 콘텐츠 수가 `recordCountPerPage`보다 적을 수 있다.
 */

const CONTENT_BASE_PATH = '/api/v1/mngr/cntntns';

type ContentApiKind = 'shorts' | 'comic';

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;
}

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 };
}

/** 한 `atchFileId` 아래 여러 컷이 `fileSn`으로 들어온다. */
function toFileNumbers(raw: unknown): number[] {
  if (!Array.isArray(raw)) {
    return [];
  }
  return raw
    .flatMap((entry) => (isRecord(entry) ? [Number(entry.fileSn)] : []))
    .filter((value) => Number.isInteger(value) && value > 0)
    .sort((a, b) => a - b);
}

function toContentItem(raw: unknown): ContentItem {
  if (!isRecord(raw)) {
    throw new Error('콘텐츠 응답 항목의 형식이 올바르지 않습니다.');
  }

  const cntntsSn = readString(raw, 'cntntsSn');
  if (!cntntsSn) {
    throw new Error('콘텐츠 응답에 cntntsSn이 없습니다.');
  }

  const schulGrd = toSchoolGrades(raw.schulGrdList);

  return {
    cntntsSn,
    cutFileNumbers: toFileNumbers(raw.fileList),
    name: readString(raw, 'cntntsNm') ?? '',
    title: readString(raw, 'cntntsTitle') ?? '',
    subject: readString(raw, 'cntntsSbjct') ?? '',
    message: readString(raw, 'cntntsMssage') ?? '',
    schoolCode: schulGrd.schoolCode,
    grades: schulGrd.grades,
    keywords: toKeywords(raw.keywordList),
    attachmentId: readString(raw, 'atchFileId'),
    thumbnailFileId: readString(raw, 'thumbAtchFileId'),
    isVisible: readString(raw, 'useYn') !== 'N',
    modifiedAt:
      readString(raw, 'lastMdfcnDtStr') ?? readString(raw, 'frstRegDtStr'),
  };
}

function buildListParams(
  kind: ContentApiKind,
  query: ContentQuery,
  pageIndex: number,
  recordCountPerPage: number
): Record<string, string | number> {
  const keyword = query.keyword.trim();

  return {
    // 없으면 `CNTNTS_TYPE = NULL`이 되어 한 건도 오지 않는다.
    searchCntntsType: CONTENT_TYPE_CODE[kind],
    searchCondition: keyword ? '1' : '',
    searchKeyword: keyword,
    pageIndex,
    recordCountPerPage,
  };
}

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

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

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

  const data = result.data;
  // 금융쇼츠는 컨트롤러가 null을 돌려준다(파일 상단 이슈) — 빈 목록으로 다룬다.
  if (data === null || data === undefined) {
    return { items: [], reportedTotalCount: 0 };
  }
  if (!isRecord(data) || !Array.isArray(data.list)) {
    throw new Error('콘텐츠 목록 응답의 형식이 올바르지 않습니다.');
  }

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

export type ContentPage = {
  items: ContentItem[];
  totalCount: number;
  isTotalCountExact: boolean;
};

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

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

  return {
    items,
    // 백엔드 `PaginationUtil`이 count 없이 페이지 행 수를 총건수로 내려주는 결함이 있어
    // 하한값으로 보정한다(다른 목록 화면과 같은 처리).
    totalCount: Math.max(reportedTotalCount, confirmedCount),
    isTotalCountExact:
      items.length < query.pageSize || reportedTotalCount > confirmedCount,
  };
}

/** 단건 조회 — 목록과 같은 SQL이라 키워드·학교급·파일 목록이 함께 온다. */
export async function fetchContentDetail(
  kind: ContentApiKind,
  cntntsSn: string
): Promise<ContentItem | null> {
  void kind;
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(
    `${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsSn)}`,
    {
      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 toContentItem(result.data);
}

// 이슈: 주제·전달메시지를 담을 컬럼이 테이블에 없다. 화면은 기획대로 두고 값은 보내지 않는다.
type ContentWritePayload = {
  cntntsType?: string;
  cntntsNm: string;
  cntntsTitle: string;
  atchFileId: string;
  thumbAtchFileId: string;
  useYn: string;
  keywordList: Array<{ cntntsKeyword: string; cntntsExpln: string }>;
  schulGrdList: Array<{ schulCd: string; grdCd: string | null }>;
};

function toKeywordPayload(keywords: readonly ContentKeyword[]) {
  return keywords.map((item) => ({
    cntntsKeyword: item.keyword,
    cntntsExpln: item.description,
  }));
}

/** 학년마다 한 줄이다. 학년을 고르지 않았으면 학교만 담은 한 줄을 남긴다. */
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 }));
}

export function buildShortsPayload(
  values: ShortsFormValues
): ContentWritePayload {
  return {
    cntntsNm: values.title,
    cntntsTitle: values.title,
    atchFileId: values.videoFileId,
    thumbAtchFileId: values.thumbnailFileId,
    useYn: values.isVisible ? 'Y' : 'N',
    keywordList: toKeywordPayload(values.keywords),
    schulGrdList: toSchoolGradePayload(values.schoolCode, values.grades),
  };
}

export function buildComicPayload(values: ComicFormValues): ContentWritePayload {
  return {
    cntntsNm: values.name,
    cntntsTitle: values.name,
    // 컷은 한 첨부 아래 fileSn 1..N으로 들어간다 — 배열 값이 모두 같은 첨부 아이디다.
    atchFileId: values.cutFileIds[0] ?? '',
    thumbAtchFileId: values.thumbnailFileId,
    useYn: values.isVisible ? 'Y' : 'N',
    keywordList: toKeywordPayload(values.keywords),
    schulGrdList: toSchoolGradePayload(values.schoolCode, values.grades),
  };
}

/**
 * 등록은 `@RequestBody`가 없어 form으로 간다. 중첩 목록은 Spring이 읽는 인덱스 표기
 * (`keywordList[0].cntntsKeyword`)로 편다 — JSON으로 보내면 한 칸도 바인딩되지 않는다.
 */
function toFormParams(
  payload: ContentWritePayload & { cntntsType?: string }
): Record<string, string | number | undefined> {
  const params: Record<string, string | number | undefined> = {
    cntntsType: payload.cntntsType,
    cntntsNm: payload.cntntsNm,
    cntntsTitle: payload.cntntsTitle,
    atchFileId: payload.atchFileId,
    thumbAtchFileId: payload.thumbAtchFileId,
    useYn: payload.useYn,
  };

  payload.keywordList.forEach((item, index) => {
    params[`keywordList[${index}].cntntsKeyword`] = item.cntntsKeyword;
    params[`keywordList[${index}].cntntsExpln`] = item.cntntsExpln;
  });
  payload.schulGrdList.forEach((item, index) => {
    params[`schulGrdList[${index}].schulCd`] = item.schulCd;
    // undefined면 파라미터 자체가 빠져 백엔드에서 null이 된다.
    params[`schulGrdList[${index}].grdCd`] = item.grdCd ?? undefined;
  });

  return params;
}

/**
 * 보낸 것을 서버 콘솔에 그대로 남긴다.
 *
 * 브라우저 네트워크 탭에는 이 요청이 **뜨지 않는다** — 화면은 Server Action을 부르고 백엔드
 * 호출은 서버에서 일어난다(BFF). 그래서 개발 중에는 성공·실패를 가리지 않고 찍고, 운영에서는
 * 실패만 남긴다. JSON은 Swagger의 Request body에 그대로 붙여 넣을 수 있는 모양이다.
 */
function logWrite(
  method: string,
  path: string,
  body: unknown,
  form: Record<string, string | number | undefined> | undefined,
  failure: { code: number; message: string } | null
): void {
  if (failure === null && process.env.NODE_ENV === 'production') {
    return;
  }

  const head = failure
    ? `[contents] ${method} ${path} 실패 (code ${failure.code}: ${failure.message})`
    : `[contents] ${method} ${path}`;

  const lines = [
    head,
    body === undefined ? null : `body(JSON):\n${JSON.stringify(body, null, 2)}`,
    form === undefined
      ? null
      : `form:\n${new URLSearchParams(
          Object.entries(form).flatMap(([key, value]) =>
            value === undefined ? [] : [[key, String(value)] as [string, string]]
          )
        ).toString()}`,
  ].filter(Boolean);

  if (failure) {
    console.error(lines.join('\n'));
  } else {
    console.info(lines.join('\n'));
  }
}

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

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

  logWrite(method, path, body, form, result.ok ? null : result);

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

  return typeof result.data === 'string' ? result.data : null;
}

export async function createContent(
  kind: ContentApiKind,
  payload: ContentWritePayload,
  cntntsType: string
): Promise<string | null> {
  return sendWrite(CONTENT_BASE_PATH, 'POST', undefined, toFormParams({ ...payload, cntntsType }));
}

export async function updateContent(
  kind: ContentApiKind,
  cntntsSn: string,
  payload: ContentWritePayload
): Promise<void> {
  await sendWrite(
    `${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsSn)}`,
    'PUT',
    // 수정 SQL이 `CNTNTS_TYPE = #{cntntsType}`을 조건 없이 덮는다. 빼고 보내면 종류가 지워져
    // 목록(`CNTNTS_TYPE = 'V'|'T'`)에서 사라진다.
    { ...payload, cntntsType: CONTENT_TYPE_CODE[kind] }
  );
}

export async function deleteContent(
  kind: ContentApiKind,
  cntntsSn: string
): Promise<void> {
  await sendWrite(`${CONTENT_BASE_PATH}/${encodeURIComponent(cntntsSn)}`, 'DELETE');
}

export function contentThumbnailUrl(item: ContentItem): string | null {
  return buildFileImageUrl(item.thumbnailFileId);
}

/** 컷 이미지 주소 — 응답이 준 파일 번호를 쓰고, 없으면 1..count로 채운다. */
export function contentCutUrls(item: ContentItem, count: number): string[] {
  const numbers =
    item.cutFileNumbers.length > 0
      ? item.cutFileNumbers
      : Array.from({ length: count }, (_, index) => index + 1);

  return numbers
    .map((fileSn) => buildFileImageUrl(item.attachmentId, fileSn))
    .filter((url): url is string => url !== null);
}
