import type { LookupOp } from '../transforms/transforms.schema';
import type { Row } from '../transforms/row';

/**
 * push 通道 lookup 回落 —— 用 PAC 已入库的 patients 重建缺席的 `from` 表。
 *
 * 背景与语义见 LookupPushFallbackSchema 的注释(transforms.schema.ts)。这里只做纯逻辑:
 * 取数交给调用方传进来的 fetch(便于单测不碰 prisma),本模块负责
 *   ① 从 input 行收集 (source_unit, external_id) 需求
 *   ② 按 external_id 归组、跨品牌撞号则跳过
 *   ③ 按 select 的 column/map 拼出合成行
 */

/** PAC patients 里回落用得上的列(与 LookupPushFallbackSchema.select.column 白名单对齐) */
export interface PatientFallbackRow {
  externalId: string;
  sourceUnit: string | null;
  gender: string | null;
  birthDate: Date | null;
  name: string | null;
  medicalRecordNumber: string | null;
}

export type PatientFallbackFetch = (args: {
  sourceUnits: string[];
  externalIds: string[];
}) => Promise<PatientFallbackRow[]>;

export interface PushLookupFallbackResult {
  rows: Row[];
  /** 成功重建的合成行数 */
  resolved: number;
  /** 因 external_id 跨 source_unit 撞号而整条跳过的 key 数 */
  ambiguous: number;
  /** input 里要查、但 PAC 库还没有的 external_id 数(对方主档尚未推达) */
  absent: number;
}

/**
 * IN 列表分批大小 —— Postgres 单条语句绑定变量上限 32767,留足余量。
 * (2026-07 存量补摄时踩过 bind-var 溢出,见 clinic-backfill 手册。)
 */
const FETCH_CHUNK = 5000;

export async function buildPushLookupFallbackRows(
  op: LookupOp,
  inputRows: readonly Row[],
  resolveSourceUnit: (row: Row) => string,
  fetch: PatientFallbackFetch,
): Promise<PushLookupFallbackResult> {
  const fallback = op.push_fallback;
  if (!fallback) return { rows: [], resolved: 0, ambiguous: 0, absent: 0 };

  // ① 收集需求:external_id → 该 id 在本批里出现过的 source_unit 集合
  const wanted = new Map<string, Set<string>>();
  for (const row of inputRows) {
    const raw = row[op.left_key];
    if (raw === null || raw === undefined) continue;
    const id = String(raw).trim();
    if (!id) continue;
    const unit = resolveSourceUnit(row);
    const units = wanted.get(id);
    if (units) units.add(unit);
    else wanted.set(id, new Set([unit]));
  }
  if (wanted.size === 0) return { rows: [], resolved: 0, ambiguous: 0, absent: 0 };

  // ② 取数(按 external_id 分批;source_unit 一次性传全,基数很小)
  const allUnits = [...new Set([...wanted.values()].flatMap((s) => [...s]))];
  const ids = [...wanted.keys()];
  const fetched: PatientFallbackRow[] = [];
  for (let i = 0; i < ids.length; i += FETCH_CHUNK) {
    fetched.push(
      ...(await fetch({ sourceUnits: allUnits, externalIds: ids.slice(i, i + FETCH_CHUNK) })),
    );
  }

  // ③ 归组 + 撞号裁决
  const byExternalId = new Map<string, PatientFallbackRow[]>();
  for (const p of fetched) {
    const bucket = byExternalId.get(p.externalId);
    if (bucket) bucket.push(p);
    else byExternalId.set(p.externalId, [p]);
  }

  const rows: Row[] = [];
  let ambiguous = 0;
  let absent = 0;
  for (const id of ids) {
    const candidates = byExternalId.get(id);
    if (!candidates || candidates.length === 0) {
      absent++;
      continue;
    }
    // 同一 external_id 落到多个品牌 → 无法判断 input 行指的是哪一个,整条跳过。
    // 跳过 = 下游填 null → enum_mapping 的 _default 兜底,语义是"不知道",不是"猜一个"。
    if (candidates.length > 1) {
      ambiguous++;
      continue;
    }
    rows.push(synthesize(op, candidates[0]!));
    // resolved 在下面统一按 rows.length 算
  }

  return { rows, resolved: rows.length, ambiguous, absent };
}

function synthesize(op: LookupOp, p: PatientFallbackRow): Row {
  const out: Row = { [op.right_key]: p.externalId };
  for (const [target, spec] of Object.entries(op.push_fallback!.select)) {
    const raw = readColumn(p, spec.column);
    if (raw === null) {
      out[target] = null;
      continue;
    }
    // map 未命中当作"取不到" —— 宁可填 null 走 _default,也不把 PAC 归一值直接漏给
    // 只认宿主原编码的下游 enum_mapping(那会静默产生一个谁也没配过的键)。
    out[target] = spec.map ? (spec.map[raw] ?? null) : raw;
  }
  return out;
}

function readColumn(p: PatientFallbackRow, column: string): string | null {
  switch (column) {
    case 'gender':
      return emptyToNull(p.gender);
    case 'name':
      return emptyToNull(p.name);
    case 'medicalRecordNumber':
      return emptyToNull(p.medicalRecordNumber);
    case 'birthDate':
      return p.birthDate ? p.birthDate.toISOString().slice(0, 10) : null;
    default:
      return null;
  }
}

/** 宿主"未知性别"归一后落库是空串(见 friday patient.yaml 的 _default: ''),等同取不到 */
function emptyToNull(v: string | null): string | null {
  const s = (v ?? '').trim();
  return s === '' ? null : s;
}
