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 {
  MAX_EXTRA_IMAGES,
  type MerchantDetail,
  type MerchantDetailSummary,
} from '@/lib/domain/merchant-detail';
import type { MerchantDetailValues } from '@/lib/domain/merchant-detail-form';
import type { MerchantDetailQuery } from '@/lib/domain/merchant-detail-query';

/**
 * 가맹몰 상세 Repository — edupay-backend origin/develop 15d409a·22f67cb 기준.
 *
 * ```
 * GET    /api/v1/mngr/voc/detail/pagination   TB_COM_VOUCHER ⟕ TB_COM_VOUCHER_DTL — 가맹점 전체
 *                                             (화면은 상세 있는 건만 목록에, 없는 건은 등록 선택지에)
 * GET    /api/v1/mngr/voc/detail/{frcsNo}
 * POST   /api/v1/mngr/voc/detail              @RequestBody · 가맹점이 있어야 붙는다
 * PUT    /api/v1/mngr/voc/detail/{frcsNo}     @RequestBody
 * DELETE /api/v1/mngr/voc/{frcsNo}            논리 삭제(DEL_YN)
 * ```
 *
 * ⚠️ 백엔드 결함(보고함, 미수정) — 화면은 시안대로 두고 값을 그대로 보낸다(사용자 확정):
 * 1. 목록·단건 SELECT(`includeSql`)에 소개·카테고리 코드·영업시간·휴무일·추가이미지·순서가
 *    없어 수정 프리필이 빈다. 카테고리 이름(`CD_NM`)은 SELECT엔 있으나 VO에 담을 곳이 없다.
 * 2. `searchStartDt/EndDt`를 VO만 받고 WHERE에 쓰지 않는다.
 * 3. 「바우처 사용」을 담을 필드가 없다 — `voucherUseYn`으로 보내되 버려진다.
 *
 * 노출 상태(`useYn`)는 `MngrVocDetailVo`가 `MngrVocVo`에서 상속받아 등록·수정·조회 모두 실린다
 * (처음엔 없다고 잘못 보고했다 — 2026-09-14 정정).
 */

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

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 !== '') return value;
  if (typeof value === 'number') return String(value);
  return null;
}

function readNumber(source: Record<string, unknown>, key: string): number | null {
  const value = source[key];
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim() !== '') {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

function readYesNo(source: Record<string, unknown>, key: string): boolean | null {
  const value = readString(source, key);
  return value === null ? null : value.toUpperCase() === 'Y';
}

/**
 * 등록일은 `d.FRST_REG_DT`가 별칭 없이 오므로 `frstRegDt`(Date → ISO 문자열 또는 epoch)로
 * 받는다. 다른 화면의 `frstRegDtStr`가 있으면 그것을 우선한다.
 */
function readDate(source: Record<string, unknown>, key: string): string | null {
  const formatted = readString(source, `${key}Str`);
  if (formatted) return formatted.slice(0, 10);

  const raw = source[key];
  const date =
    typeof raw === 'number'
      ? new Date(raw)
      : typeof raw === 'string' && raw !== ''
        ? new Date(raw)
        : null;
  if (date === null || Number.isNaN(date.getTime())) return null;
  return date.toISOString().slice(0, 10);
}

function toSummary(raw: unknown): MerchantDetailSummary[] {
  if (!isRecord(raw)) return [];
  const id = readString(raw, 'frcsNo');
  const name = readString(raw, 'frcsNm');
  if (!id || !name) return [];

  const registeredAt = readDate(raw, 'frstRegDt');
  return [
    {
      id,
      name,
      phoneNumber: readString(raw, 'rprsvTelno'),
      address: readString(raw, 'frcsPostAddr'),
      addressDetail: readString(raw, 'frcsDaddr'),
      thumbnailFileId: readString(raw, 'thumbAtchFileId'),
      thumbnailUrl: buildFileImageUrl(readString(raw, 'thumbAtchFileId')),
      categoryCode: readString(raw, 'frcsTypeCd'),
      isVisible: readYesNo(raw, 'useYn'),
      registeredAt,
      // 상세 행은 LEFT JOIN이라 없으면 상세 컬럼이 전부 null이다 — 등록일이 그 표지다.
      hasDetail: registeredAt !== null || readString(raw, 'thumbAtchFileId') !== null,
    },
  ];
}

/**
 * 묶음 안의 이미지 수를 알려 주는 API가 없다(보고함). fileSn이 1부터 빈틈없이 붙으므로
 * 없는 번호가 나올 때까지 HEAD로 더듬는다 — 없는 번호는 백엔드가 500으로 답한다.
 */
async function probeImageUrls(fileId: string | null): Promise<string[]> {
  if (!fileId) return [];
  const urls: string[] = [];
  for (let fileSn = 1; fileSn <= MAX_EXTRA_IMAGES; fileSn += 1) {
    const url = buildFileImageUrl(fileId, fileSn);
    if (!url) break;
    try {
      const response = await fetch(url, { method: 'HEAD', cache: 'no-store' });
      if (!response.ok) break;
    } catch {
      break;
    }
    urls.push(url);
  }
  return urls;
}

async function toDetail(raw: unknown): Promise<MerchantDetail | null> {
  const [summary] = toSummary(raw);
  if (!summary || !isRecord(raw)) return null;

  const imageFileId = readString(raw, 'atchFileId');
  return {
    ...summary,
    imageUrls: await probeImageUrls(imageFileId),
    postalCode: readString(raw, 'frcsPostZip'),
    description: readString(raw, 'frcsCn'),
    openTime: readString(raw, 'startHour'),
    closeTime: readString(raw, 'endHour'),
    closedDays: readString(raw, 'offDayCn'),
    imageFileId,
    sortOrder: readNumber(raw, 'sortOrdr'),
    registeredBy: readString(raw, 'frstRgtrNm'),
    modifiedAt: readDate(raw, 'lastMdfcnDt'),
    modifiedBy: readString(raw, 'lastMdfrNm'),
  };
}

type SummaryFilters = Pick<
  MerchantDetailQuery,
  'keyword' | 'category' | 'visibility' | 'startDate' | 'endDate'
>;

async function fetchSummaryPage(
  filters: Partial<SummaryFilters>,
  pageIndex: number,
  pageSize: number
): Promise<{ items: MerchantDetailSummary[]; rowCount: number; totalCount: number }> {
  const accessToken = await getSessionAccessToken();

  const result = await backendFetch<unknown>(`${DETAIL_PATH}/pagination`, {
    method: 'GET',
    query: {
      ...(filters.keyword ? { searchFrcsNm: filters.keyword } : {}),
      ...(filters.category ? { searchFrcsTypeCd: filters.category } : {}),
      ...(filters.visibility ? { searchUseYn: filters.visibility } : {}),
      ...(filters.startDate ? { searchStartDt: filters.startDate } : {}),
      ...(filters.endDate ? { searchEndDt: filters.endDate } : {}),
      pageIndex,
      recordCountPerPage: 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: [], rowCount: 0, totalCount: 0 };
  }
  if (!isRecord(data) || !Array.isArray(data.list)) {
    throw new Error('가맹몰 상세 목록 응답의 형식이 올바르지 않습니다.');
  }

  return {
    items: data.list.flatMap(toSummary),
    // 매핑에서 걸러진 행이 있어도 페이지가 끝났는지는 원본 행 수로 판단한다.
    rowCount: data.list.length,
    totalCount:
      typeof data.totalCount === 'number' ? data.totalCount : data.list.length,
  };
}

// 백엔드 목록에는 "상세가 있는 건만" 조건이 없다(보고함). 그래서 전 페이지를 받아 여기서 거른다 —
// 가맹점은 많아야 수백 건이라 한두 번의 요청이다. 상한을 넘으면 그 뒤는 잘린다.
// TODO(백엔드): 상세 유무 조건이 생기면 이 훑기를 없애고 페이지 하나만 받는다.
const SCAN_PAGE_SIZE = 200;
const MAX_SCAN_PAGES = 25;

async function fetchAllSummaries(
  filters: Partial<SummaryFilters>
): Promise<MerchantDetailSummary[]> {
  const items: MerchantDetailSummary[] = [];
  let seen = 0;
  for (let pageIndex = 1; pageIndex <= MAX_SCAN_PAGES; pageIndex += 1) {
    const page = await fetchSummaryPage(filters, pageIndex, SCAN_PAGE_SIZE);
    items.push(...page.items);
    seen += page.rowCount;
    if (page.rowCount < SCAN_PAGE_SIZE || seen >= page.totalCount) {
      break;
    }
  }
  return items;
}

/** 목록 — 상세가 있는 가맹점만, 화면 쪽 페이지로 잘라 준다(사용자 확정). */
export async function fetchMerchantDetails(
  query: MerchantDetailQuery
): Promise<{ items: MerchantDetailSummary[]; totalCount: number }> {
  const registered = (await fetchAllSummaries(query)).filter((row) => row.hasDetail);
  const start = (query.page - 1) * query.pageSize;
  return {
    items: registered.slice(start, start + query.pageSize),
    totalCount: registered.length,
  };
}

/** 등록 화면의 업체 선택지 — 아직 상세가 없는 가맹점 전부. */
export async function fetchMerchantDetailCandidates(): Promise<MerchantDetailSummary[]> {
  return (await fetchAllSummaries({})).filter((row) => !row.hasDetail);
}

export async function fetchMerchantDetail(
  frcsNo: string
): Promise<MerchantDetail | null> {
  const accessToken = await getSessionAccessToken();

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

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

function toWritePayload(values: MerchantDetailValues): Record<string, unknown> {
  return {
    frcsNo: values.frcsNo,
    frcsTypeCd: values.categoryCode,
    frcsCn: values.description,
    startHour: values.openTime,
    endHour: values.closeTime,
    offDayCn: values.closedDays,
    thumbAtchFileId: values.thumbnailFileId,
    atchFileId: values.imageFileId || null,
    sortOrdr: values.sortOrder,
    useYn: values.isVisible ? 'Y' : 'N',
    // 요청 VO에 없어 버려진다(⚠️ 3). 자리가 생기면 그대로 저장된다.
    voucherUseYn: values.isVoucherUsable ? 'Y' : 'N',
  };
}

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 createMerchantDetail(
  values: MerchantDetailValues
): Promise<void> {
  await sendWrite(DETAIL_PATH, 'POST', toWritePayload(values));
}

export async function updateMerchantDetail(
  values: MerchantDetailValues
): Promise<void> {
  await sendWrite(
    `${DETAIL_PATH}/${encodeURIComponent(values.frcsNo)}`,
    'PUT',
    toWritePayload(values)
  );
}

export async function deleteMerchantDetail(frcsNo: string): Promise<void> {
  await sendWrite(`${VOC_PATH}/${encodeURIComponent(frcsNo)}`, 'DELETE');
}
