import 'server-only';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch';
import {
  MERCHANT_CATEGORY_READ_FIELD,
  MERCHANT_CATEGORY_WRITE_FIELD,
  type Merchant,
} from '@/lib/domain/merchant';
import type { MerchantQuery } from '@/lib/domain/merchant-query';
import type { MerchantValues } from '@/lib/domain/merchant-form';

/**
 * 가맹점 Repository — `TB_COM_VOUCHER`.
 *
 * ```
 * GET  /api/v1/mngr/voc/pagination   searchCondition 1=가맹점명 2=사업자번호 3=상세주소
 * POST /api/v1/mngr/voc              @RequestBody MngrVocInsertReqVo → TB_COM_VOUCHER INSERT (1703ce8)
 * PUT  /api/v1/mngr/voc              @RequestBody MngrVocUpdateReqVo → UPDATE … WHERE FRCS_NO
 * ```
 *
 * 등록은 1703ce8부터 우리 DB에 바로 들어간다 — 코드는 서버가 `TEST` + 8자리로 채번하고 좌표는 카카오
 * 지오코딩으로 채운다. 응답에 코드가 없어(`data: null`) 호출부는 목록에서 이름·연락처로 찾는다.
 * 예금주·은행·계좌는 `MngrVocVo`에 없어 버려진다(테이블에 컬럼 없음).
 *
 * ⚠️ 백엔드 현황(보고함)
 * 1. 수정 요청 VO(`MngrVocUpdateReqVo`)에 `frcsNo`가 없어 UPDATE가 `WHERE FRCS_NO = NULL`로 아무 행도
 *    바꾸지 않고 성공으로 답한다 — VO에 `frcsNo` 한 줄이 필요하다. 프론트는 이미 싣고 있다.
 * 2. **삭제·단건 조회 API가 없다.**
 * 3. 검색 필터가 어긋난다 — 업종 검색 파라미터는 `searchFrcsTpbizCd`인데 SQL은 `FRCS_DV_CD`를
 *    비교하고, 저장은 `frcsTpbizCd`로 받는다. 저장한 업종으로 검색이 걸리지 않는다.
 * 4. 목록 정렬이 이중 역순이다 — rnum이 등록일 내림차순인데 `ORDER BY RNUM DESC`로 다시
 *    뒤집어 오래된 가맹점부터 나온다.
 * 5. (feature/voc bf17fc5에서 해소) 목록이 `USE_YN`과 `FRCS_DV_CD_NM`을 돌려주기 시작했다 —
 *    develop에선 아직 없어 사용여부가 `-`로 보인다.
 * 6. 브랜드명·메모·최종수정일 컬럼이 없다 — 기획에 있는 항목이라 화면은 그대로 두고 값만 비워
 *    `-`로 보인다(등록일은 1c7b0e0부터 `frstRegDtStr`로 온다). 등록 시 보내는 브랜드명·메모도 요청
 *    VO에 없어 버려진다.
 */

const VOC_PATH = '/api/v1/mngr/voc';

/** 엑셀은 전체를 담는다 — 이 상한을 넘으면 조용히 잘린다. */
const EXCEL_ROW_LIMIT = 10_000;

/** 화면 검색 대상 → 백엔드 `searchCondition`. */
const SEARCH_CONDITION: Record<string, string> = {
  name: '1',
  businessNumber: '2',
  address: '3',
};

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

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

/** 숫자로 오는 값(위도·경도)도 글자로 받는다. */
function readText(source: Record<string, unknown>, key: string): string {
  const value = source[key];
  if (typeof value === 'number' && Number.isFinite(value)) return String(value);
  return readString(source, key);
}

function toMerchant(raw: unknown): Merchant[] {
  if (!isRecord(raw)) {
    return [];
  }

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

  return [
    {
      id: code,
      code,
      // 백엔드에 컬럼이 없다(⚠️ 6) — 응답에 있으면 쓰고, 없으면 빈 값으로 두어 화면이 `-`를 낸다.
      brandName: readString(raw, 'brandNm'),
      memo: readString(raw, 'memo'),
      createdAt: readString(raw, 'frstRegDtStr'),
      updatedAt: readString(raw, 'lastMdfcnDtStr'),
      name: readString(raw, 'frcsNm'),
      // BizPlay 값이 CHAR 컬럼이라 뒤에 공백이 붙어 온다('52 ').
      categoryCode: readString(raw, MERCHANT_CATEGORY_READ_FIELD).trim(),
      businessNumber: readString(raw, 'brno'),
      ownerName: readString(raw, 'rprsvNm'),
      phoneNumber: readString(raw, 'rprsvTelno'),
      address: readString(raw, 'frcsPostAddr'),
      addressDetail: readString(raw, 'frcsDaddr'),
      postalCode: readString(raw, 'frcsPostZip'),
      latitude: readText(raw, 'lat'),
      longitude: readText(raw, 'lng'),
      businessStatusCode: readString(raw, 'frcsBzstatCd').trim(),
      divisionCode: readString(raw, 'frcsDvCd').trim(),
      // 조회 SQL이 USE_YN을 select하지 않는다(⚠️ 5). 값이 없으면 판단하지 않는다 —
      // 종전처럼 노출로 단정하면 「미노출」로 걸러 낸 행까지 전부 「노출」로 보인다.
      isVisible: readUseYn(raw),
    },
  ];
}

/** 백엔드 `useYn`은 'Y'/'N' 문자열이다. 응답에 없으면 판단하지 않고 null로 둔다. */
function readUseYn(raw: Record<string, unknown>): boolean | null {
  const value = raw.useYn;
  return typeof value === 'string' && value !== '' ? value.toUpperCase() === 'Y' : null;
}

export async function fetchMerchants(
  query: MerchantQuery
): Promise<{ items: Merchant[]; totalCount: number }> {
  const accessToken = await getSessionAccessToken();
  const keyword = query.keyword.trim();

  const result = await backendFetch<unknown>(`${VOC_PATH}/pagination`, {
    method: 'GET',
    query: {
      ...(keyword
        ? {
            searchCondition: SEARCH_CONDITION[query.searchField] ?? '1',
            searchKeyword: keyword,
          }
        : {}),
      ...(query.visibility
        ? { searchUseYn: query.visibility === 'visible' ? 'Y' : 'N' }
        : {}),
      pageIndex: query.page,
      recordCountPerPage: query.pageSize,
    },
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    canHaveNullData: true,
  });

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

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

  return {
    items: data.list.flatMap(toMerchant),
    totalCount:
      typeof data.totalCount === 'number' ? data.totalCount : data.list.length,
  };
}

/** 엑셀 다운로드는 검색 조건과 무관하게 전체다(기획 MCH_001 ②). */
export async function fetchAllMerchants(): Promise<Merchant[]> {
  const accessToken = await getSessionAccessToken();

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

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

  const data = result.data;
  return isRecord(data) && Array.isArray(data.list)
    ? data.list.flatMap(toMerchant)
    : [];
}

/** 백엔드 `MngrVocBaseVo` — 등록·수정 요청이 함께 쓰는 가맹점 필드. */
function toMerchantPayload(values: MerchantValues): Record<string, unknown> {
  return {
    frcsNm: values.name,
    rprsvNm: values.ownerName,
    // BizPlay가 채운 행과 같은 모양(숫자만)으로 — 화면은 formatPhoneNumber가 다시 붙인다.
    rprsvTelno: values.phoneNumber.replace(/-/g, ''),
    brno: values.businessNumber.replace(/-/g, ''),
    bankDpstrNm: values.bankHolderName,
    dpstBankCd: values.bankCode,
    // 등록 VO(BizPlayVocInsertVo)는 dpstActno, 수정 VO(MngrVocBaseVo)는 dpstActNo다 — 둘 다 싣는다.
    dpstActno: values.accountNumber,
    dpstActNo: values.accountNumber,
    frcsDvCd: values.divisionCode,
    frcsPostAddr: values.address,
    frcsDaddr: values.addressDetail,
    frcsPostZip: values.postalCode,
    [MERCHANT_CATEGORY_WRITE_FIELD]: values.categoryCode,
    frcsBzstatCd: values.businessStatusCode,
    // 요청 VO에 없어 무시된다. 컬럼이 생기면 그대로 저장되도록 미리 싣는다.
    useYn: values.isVisible ? 'Y' : 'N',
    brandNm: values.brandName,
    memo: values.memo,
    lat: values.latitude,
    lng: values.longitude,
  };
}

/** 등록 응답에서 가맹점 코드를 찾는다 — 문자열, `frcsNo`, BizPlay 응답(`repRec.FRCS_NO`) 어느 모양이든. */
function readCreatedFrcsNo(data: unknown): string | null {
  if (typeof data === 'string' && data !== '') return data;
  if (!isRecord(data)) return null;
  const direct = readString(data, 'frcsNo') || readString(data, 'FRCS_NO');
  if (direct) return direct;
  const repRec = data.repRec;
  return isRecord(repRec) ? readString(repRec, 'FRCS_NO') || readString(repRec, 'frcsNo') || null : null;
}

/** 가맹점 등록. 백엔드가 코드를 돌려주면 그 값, 아니면 null이다. */
export async function createMerchant(values: MerchantValues): Promise<string | null> {
  const accessToken = await getSessionAccessToken();

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

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

/**
 * 가맹점 수정. 수정 VO에는 `frcsNo`가 없다(BizPlay가 사업자번호로 식별) — 우리 DB 저장으로 바뀔 때를
 * 위해 `frcsNo`도 싣는다. `chgDvCd`(변경구분)는 BizPlay 전용이라 비워 보낸다.
 */
export async function updateMerchant(frcsNo: string, values: MerchantValues): Promise<void> {
  const accessToken = await getSessionAccessToken();

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

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