import 'server-only';
import { cache } from 'react';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import { buildFileImageUrl } from '@/lib/data/repositories/file-repository';
import type { SystemMenu } from '@/lib/domain/system-menu';
import type { SystemMenuValues } from '@/lib/domain/system-menu-form';

/**
 * 시스템 메뉴 Repository — `TB_SYS_MENU`.
 *
 * ```
 * GET    /api/v1/mngr/menu/pagination   목록(페이징). 트리라 한 번에 다 받는다
 * POST   /api/v1/mngr/menu              어노테이션 없음 → form. upMenuId의 부모가 있어야 한다
 * PUT    /api/v1/mngr/menu/{menuId}     @RequestBody(JSON)
 * DELETE /api/v1/mngr/menu/{menuId}     soft delete(DEL_YN)
 * ```
 *
 * ⚠️ 백엔드 결함(보고함, 미수정)
 * 1. `DELETE`의 인자에 `@PathVariable`이 없어 경로 값이 바인딩되지 않는다. 경로와 쿼리에 같은
 *    값을 함께 실어 보낸다 — 어노테이션이 붙어도 그대로 동작한다.
 * 2. `GET /{menuId}`는 경로가 `{menuId}`인데 인자가 `@PathVariable String userId`이고 본문도 빈
 *    VO로 조회한다. 목록에 모든 필드가 있어 이 API는 쓰지 않는다.
 * 3. 새창여부에 해당하는 컬럼이 없다. 화면에는 시안대로 두되 값을 보내지 않는다(사용자 확정).
 */

const MENU_PATH = '/api/v1/mngr/menu';

// TODO(백엔드): 전체 조회 모드가 없어 큰 페이지 하나로 받는다. 메뉴가 이보다 많아지면 잘린다.
const FETCH_LIMIT = 1000;

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 parseYesNo(value: unknown): boolean {
  return value === 'Y';
}

function toSystemMenu(raw: unknown): SystemMenu[] {
  if (!isRecord(raw)) {
    return [];
  }

  const id = readString(raw, 'menuId');
  if (!id) {
    return [];
  }

  return [
    {
      id,
      parentId: readString(raw, 'upMenuId'),
      name: readString(raw, 'menuName') ?? '',
      idPath: readString(raw, 'menuIdPath'),
      url: readString(raw, 'menuUrl'),
      roleName: readString(raw, 'roleNm'),
      sortOrder: Number.isInteger(Number(raw.sortOrdr)) ? Number(raw.sortOrdr) : 0,
      isActive: parseYesNo(raw.useYn),
      isTopActive: parseYesNo(raw.topYn),
      isDefault: parseYesNo(raw.defaultYn),
      largeIconId: readString(raw, 'atchFileId'),
      largeIconName: readString(raw, 'fileNm'),
      // 미리보기 주소는 서버에서 만든다 — 주소 조립기가 server-only라 화면이 부를 수 없다.
      largeIconUrl: buildFileImageUrl(readString(raw, 'atchFileId')),
      smallIconId: readString(raw, 'smallAtchFileId'),
      smallIconName: readString(raw, 'smallFileNm'),
      smallIconUrl: buildFileImageUrl(readString(raw, 'smallAtchFileId')),
    },
  ];
}

export const fetchSystemMenus = cache(async (): Promise<SystemMenu[]> => {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(`${MENU_PATH}/pagination`, {
    method: 'GET',
    query: { pageIndex: 1, recordCountPerPage: 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.flatMap(toSystemMenu);
});

/**
 * 새창여부는 보내지 않는다 — 저장할 컬럼이 없다(파일 상단 ⚠️ 3).
 * 메뉴권한(roleId)과 메뉴설명(menuExplan)도 화면에서 빠져 보내지 않는다 — update는 동적
 * SET(`<if test="… != null">`)이라 기존 값이 유지되고, 신규는 NULL로 들어간다.
 */
function toWriteParams(values: SystemMenuValues) {
  return {
    menuName: values.name,
    sortOrdr: values.sortOrder,
    topYn: values.isTopActive ? 'Y' : 'N',
    menuUrl: values.url,
    useYn: values.isActive ? 'Y' : 'N',
  };
}

function logFailure(
  action: string,
  result: { code: number; message: string },
  params: Record<string, unknown>
): void {
  console.error(`[system-menu] ${action} 실패`, {
    code: result.code,
    message: result.message,
    params,
  });
}

/**
 * 등록 SQL이 만들 MENU_ID를 그대로 흉내 내 본다 — 백엔드 로그를 볼 수 없어 500의 정체를
 * 가릴 방법이 이것뿐이다.
 *
 * selectKey가 `MAX(menu_id) + 1`을 **형제 안에서만** 구하는데 MENU_ID는 전체 기본키라,
 * 그 값이 다른 가지에 이미 있으면 중복키로 죽는다.
 *
 * TODO(백엔드): selectKey의 `WHERE UP_MENU_ID` 조건이 빠지면 이 진단도 지운다.
 */
async function logGeneratedIdCollision(parentId: string): Promise<void> {
  try {
    const menus = await fetchSystemMenus();
    const siblingMax = menus
      .filter((menu) => menu.parentId === parentId)
      .reduce((max, menu) => Math.max(max, Number(menu.id) || 0), 0);
    const generatedId = String(siblingMax + 1);
    const owner = menus.find((menu) => menu.id === generatedId);

    console.error('[system-menu] 백엔드가 만들 MENU_ID 예측', {
      upMenuId: parentId,
      형제중최댓값: siblingMax,
      만들어질MENU_ID: generatedId,
      이미쓰는메뉴: owner ? `${owner.id} ${owner.name}` : '없음',
      판정: owner ? '중복키로 실패한다' : '중복은 아니다 — 다른 원인',
    });
  } catch {
    console.error('[system-menu] MENU_ID 예측 실패 — 목록을 다시 읽지 못했다');
  }
}

export async function createSystemMenu(values: SystemMenuValues): Promise<void> {
  const accessToken = await getSessionAccessToken();

  // 등록에는 지울 아이콘이 없다. 빈 문자열을 보내면 파일 테이블을 가리키는 값이 되어
  // 제약에 걸릴 수 있으므로 고른 것이 있을 때만 싣는다.
  const params = {
    ...toWriteParams(values),
    upMenuId: values.parentId,
    menuIdPath: values.parentIdPath || undefined,
    // INSERT에 DEFAULT_YN이 들어가는데 화면에는 없는 항목이다. 보내지 않으면 NULL이 박힌다.
    defaultYn: 'N',
    ...(values.largeIconId ? { atchFileId: values.largeIconId } : {}),
    ...(values.smallIconId ? { smallAtchFileId: values.smallIconId } : {}),
  };

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

  if (!result.ok) {
    logFailure('등록', result, params);
    await logGeneratedIdCollision(values.parentId);
    throw new BackendRequestError(result);
  }
}

export async function updateSystemMenu(values: SystemMenuValues): Promise<void> {
  const accessToken = await getSessionAccessToken();

  // 수정은 빈 문자열을 그대로 보낸다 — UPDATE가 null만 건너뛰므로 이래야 아이콘을 지울 수 있다.
  const params = {
    ...toWriteParams(values),
    atchFileId: values.largeIconId,
    smallAtchFileId: values.smallIconId,
  };

  const result = await backendFetch<unknown>(`${MENU_PATH}/${values.id}`, {
    method: 'PUT',
    body: params,
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    canHaveNullData: true,
    canHaveEmptyBody: true,
  });

  if (!result.ok) {
    logFailure('수정', result, params);
    throw new BackendRequestError(result);
  }
}

export async function deleteSystemMenu(menuId: string): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(`${MENU_PATH}/${menuId}`, {
    method: 'DELETE',
    query: { menuId },
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    canHaveNullData: true,
    canHaveEmptyBody: true,
  });

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