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              어노테이션 없음 → form
 * ```
 *
 * ⚠️ 백엔드 현황(보고함)
 * 1. **등록이 아직 동작하지 않는다.** 컨트롤러 `insert`의 본문이 전부 주석 처리돼 있고
 *    `return null;`이다 — 봉투 없는 빈 200이 온다. 그래서 저장해도 아무 일도 일어나지 않는다.
 * 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. 브랜드명·메모·등록일·최종수정일 컬럼이 없다 — 기획에 있는 항목이라 화면은 그대로 두고
 *    값만 비워 `-`로 보인다. 등록 시 보내는 브랜드명·메모도 요청 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 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: readString(raw, 'lat'),
      longitude: readString(raw, 'lng'),
      // 조회 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)
    : [];
}

/**
 * TODO(백엔드): 아래 form 전송은 옛 계약이다. 지금 `POST /api/v1/mngr/voc`는 @RequestBody(JSON)
 *   `MngrVocInsertReqVo`를 받고 BizPlay 외부 API로 위임하며, 예금주명(bankDpstrNm)·입금은행코드
 *   (dpstBankCd)·입금계좌번호(dpstActNo)·업태코드(frcsBzstatCd)를 새로 요구한다. lat·lng는 요청
 *   VO에서 빠졌다. 등록 화면이 그 항목을 받게 되면 이 함수를 JSON으로 바꿔 다시 연결한다
 *   (현재 호출부 없음 — `_actions.ts` 참고).
 */
export async function createMerchant(values: MerchantValues): Promise<void> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(VOC_PATH, {
    method: 'POST',
    form: {
      frcsNm: values.name,
      [MERCHANT_CATEGORY_WRITE_FIELD]: values.categoryCode,
      // 요청 VO의 필드 이름이 대문자다(`private String BRNO`) — Spring이 그 이름으로 바인딩한다.
      BRNO: values.businessNumber,
      rprsvNm: values.ownerName,
      rprsvTelno: values.phoneNumber,
      frcsPostAddr: values.address,
      frcsDaddr: values.addressDetail,
      frcsPostZip: values.postalCode,
      lat: values.latitude,
      lng: values.longitude,
      useYn: values.isVisible ? 'Y' : 'N',
      // 요청 VO에 없어 무시된다. 컬럼이 생기면 그대로 저장되도록 미리 싣는다.
      brandNm: values.brandName,
      memo: values.memo,
    },
    accessToken: accessToken ?? undefined,
    cache: 'no-store',
    canHaveNullData: true,
    canHaveEmptyBody: true,
  });

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