import { Injectable, Logger } from '@nestjs/common';
import type { z } from 'zod';
import { CanonicalResourceSchemas, type CanonicalResourceKey } from '@pac/types';
import type { AssemblerConfig, EmitsConfig, KeywordRule } from './assembler.schema';
import {
  ContractDriftError,
  normalizeCanonical,
  validateCanonical,
  type NormalizeContext,
} from './field-mapper';

/**
 * AssemblerEngine — yaml 驱动的"host raw 多表 → PAC canonical resource 数组"通用引擎。
 *
 * 三入口(ColdImport / Pull / Push)共用 — 入口只负责"如何拿到 raw tables",
 * AssemblerEngine 把 raw tables 投影成 canonical;再走 Synthesizer + Parser。
 *
 * 流程(对单个 AssemblerConfig):
 *   ① 读 primary 主表的每一行作为基础
 *   ② 按 field_mapping 重命名主表字段(canonical key ← host field)
 *   ③ 按 enum_mapping 翻译主表字段的编码值(A01 → endodontic)
 *   ④ 按 join_arrays 关联子表(1:N),把子表行按 foreign_key=主键聚合成数组,
 *      数组元素也走 field_mapping + enum_mapping
 *   ⑤ FieldMapper.normalize:yuan→cents / 时区 / boolean / gender
 *   ⑥ zod canonical schema 校验 → drift 抛 ContractDriftError
 *
 * 失败策略:单行失败计 stats.failed,日志带 [drift] 前缀;不阻塞同批其他行。
 *
 * 边界(yaml 表达不了的部分):
 *   - 多层嵌套(数组里又嵌数组)— 当前不支持,需扩
 *   - 复杂业务推导(NLP 抽取等)— 留 `custom_extractor` 钩子(本期未实现)
 *   - 自表关联 / 递归 — 罕见,真碰到再做
 */
@Injectable()
export class AssemblerEngine {
  private readonly logger = new Logger(AssemblerEngine.name);

  /**
   * 跑一份 yaml,产出 canonical 资源数组。
   *
   * @param tables 由入口提供的 host raw 表字典:`{ tb_encounter: [row, ...], tb_dx: [...], ... }`
   * @param config 该资源的 assembler yaml(已 zod 校验)
   * @param normalize amountUnit / timezone(整批数据共用)
   * @param hostId 用于 drift error 上下文
   * @returns 数组,每元素是已 zod 校验过的 canonical 资源对象;失败行会被
   *           记录在 result.errors,不抛(由调用方决定累计 failed)。
   */
  assemble(opts: {
    tables: Record<string, unknown[]>;
    config: AssemblerConfig;
    normalize: NormalizeContext;
    hostId: string;
  }): AssemblerResult {
    const { tables, config, normalize, hostId } = opts;
    const result: AssemblerResult = {
      canonical: config.canonical,
      emits: config.emits,
      rows: [],
      errors: [],
      stats: { fetched: 0, assembled: 0, failed: 0 },
    };

    const rawPrimaryRows = tables[config.primary.table];
    if (!rawPrimaryRows) {
      // 主表完全没提供 — 不算 drift,该 host 没这资源,跳过
      this.logger.warn(
        `Assembler[${config.canonical}]: primary table "${config.primary.table}" not in provided tables;skip`,
      );
      return result;
    }

    // ── 炸行兜底:dedup_by 配置时,按指定 key 取首行 ──
    // 详见 docs/algorithm/canonical-fact-layer.md §3.5 附录 B 第 3 条。
    const primaryRows = this.applyDedup(
      rawPrimaryRows,
      config.primary.dedup_by,
      config.canonical,
    );
    result.stats.fetched = primaryRows.length;

    // 预先按 foreign_key 把子表分组(性能:O(N) 一次扫,后续 O(1) 查)
    const childGroups: Record<string, Map<string, unknown[]>> = {};
    for (const [arrayKey, joinCfg] of Object.entries(config.join_arrays ?? {})) {
      const childRows = tables[joinCfg.table];
      if (!childRows) {
        this.logger.warn(
          `Assembler[${config.canonical}]: join table "${joinCfg.table}" not provided;skip ${arrayKey}[]`,
        );
        childGroups[arrayKey] = new Map();
        continue;
      }
      const group = new Map<string, unknown[]>();
      for (const child of childRows) {
        if (typeof child !== 'object' || child === null) continue;
        const fk = String((child as Record<string, unknown>)[joinCfg.foreign_key] ?? '');
        if (!fk) continue;
        let arr = group.get(fk);
        if (!arr) {
          arr = [];
          group.set(fk, arr);
        }
        arr.push(child);
      }
      childGroups[arrayKey] = group;
    }

    // 逐主表行投影
    for (const raw of primaryRows) {
      if (typeof raw !== 'object' || raw === null) {
        result.stats.failed++;
        result.errors.push({
          reason: 'primary row not an object',
          sample: raw,
        });
        continue;
      }
      const row = raw as Record<string, unknown>;
      const primaryKeyValue = String(row[config.primary.key] ?? '');

      // ⭐ 提取真原文 link(W3 末加,合规留底):
      // 拆出来的 fact(diagnosis/treatment 等)其 __source_row = 父 EMR 完整原文,
      // 保证 raw_payload 留底是 host 真原文,不是 transforms 中间态。
      // __source_row 缺失时(adapter 没经 transforms,或显式注入失败) fallback 用 row 自身。
      const sourceRow =
        (row['__source_row'] as Record<string, unknown> | undefined) ?? row;

      // ① + ② + ③ 主表字段投影(__source_row 不参与 mapping,不进 canonical)
      const canonical: Record<string, unknown> = {};
      for (const [canonicalKey, hostField] of Object.entries(config.field_mapping)) {
        const v = row[hostField];
        canonical[canonicalKey] = this.applyEnum(
          canonicalKey,
          v,
          config.enum_mapping,
          config.keyword_mapping,
          config.keyword_strip_clauses,
        );
      }

      // ④ join_arrays:嵌套子表数组
      for (const [arrayKey, joinCfg] of Object.entries(config.join_arrays ?? {})) {
        const childs = childGroups[arrayKey]?.get(primaryKeyValue) ?? [];
        canonical[arrayKey] = childs.map((c) =>
          this.projectChild(c as Record<string, unknown>, joinCfg),
        );
      }

      // DEBUG appointment status
      // ⑤ + ⑥ normalize + zod 校验
      try {
        const normalized = normalizeCanonical(
          canonical,
          config.canonical as CanonicalResourceKey,
          normalize,
        );
        // CanonicalResourceSchemas[key] 是 union of zod object types;
        // 这里运行期已知 key 但 ts 类型推断不出来,统一 cast 成宽泛 ZodType
        const schema = CanonicalResourceSchemas[
          config.canonical as CanonicalResourceKey
        ] as unknown as z.ZodType<unknown>;
        const validated = validateCanonical({
          raw: normalized,
          schema,
          hostId,
          resource: config.canonical,
        });
        result.rows.push({ canonical: validated, rawSource: sourceRow });
        result.stats.assembled++;
      } catch (err) {
        result.stats.failed++;
        if (err instanceof ContractDriftError) {
          result.errors.push({
            reason: err.message,
            issues: err.issues,
            sample: canonical,
          });
          this.logger.error(`[drift] ${config.canonical}: ${err.message}`);
        } else {
          result.errors.push({
            reason: err instanceof Error ? err.message : String(err),
            sample: canonical,
          });
          this.logger.error(
            `Assembler[${config.canonical}] row failed: ${
              err instanceof Error ? err.message : String(err)
            }`,
          );
        }
      }
    }

    return result;
  }

  // ─────────────────────────────────────────────────────────
  // 内部:炸行兜底
  // ─────────────────────────────────────────────────────────

  /**
   * 按 dedup_by 字段对主表行 dedup,同 key 取首行。
   *
   * 典型场景(瑞尔 DW):
   *   ETL 把 fact_emr_treatment_out 1:N JOIN 影像子表时没 groupArray,
   *   导致 1 份 EMR 有 59 张影像 → 表里出现 59 行(每行 file_url 不同,其他列重复)。
   *   yaml 配 dedup_by=id 后,只保留首行;丢失的影像信息由专门的 image yaml 投影补回。
   */
  private applyDedup(
    rows: unknown[],
    dedupBy: string | undefined,
    canonical: string,
  ): unknown[] {
    if (!dedupBy) return rows;
    const seen = new Set<string>();
    const result: unknown[] = [];
    let dropped = 0;
    for (const row of rows) {
      if (typeof row !== 'object' || row === null) {
        result.push(row);
        continue;
      }
      const key = String((row as Record<string, unknown>)[dedupBy] ?? '');
      if (!key) {
        result.push(row); // 空 key 不参与 dedup,后续 sanity check 会捕获
        continue;
      }
      if (seen.has(key)) {
        dropped++;
        continue;
      }
      seen.add(key);
      result.push(row);
    }
    if (dropped > 0) {
      this.logger.warn(
        `Assembler[${canonical}]: dedup_by="${dedupBy}" dropped ${dropped} 行炸行重复 ` +
          `(${rows.length} → ${result.length})`,
      );
    }
    return result;
  }

  // ─────────────────────────────────────────────────────────
  // 内部:子表行投影
  // ─────────────────────────────────────────────────────────

  private projectChild(
    childRaw: Record<string, unknown>,
    cfg: { field_mapping: Record<string, string>; enum_mapping?: Record<string, Record<string, string>> },
  ): Record<string, unknown> {
    const elem: Record<string, unknown> = {};
    for (const [canonicalKey, hostField] of Object.entries(cfg.field_mapping)) {
      const v = childRaw[hostField];
      elem[canonicalKey] = this.applyEnum(canonicalKey, v, cfg.enum_mapping);
    }
    return elem;
  }

  /**
   * 应用 enum_mapping:host 编码值 → PAC 枚举字符串
   *
   * 查表顺序(单值字段):
   *   1. 精确匹配 fieldMap[key]                       ← 最高优先,现状不变
   *   2. keyword_mapping 关键词分类(若配)              ← 新增:精确未命中的长尾兜底
   *   3. 特殊键 fieldMap._default(兜底,长尾值统一映射)
   *   4. 原值透传(让 zod 决定是否合法)
   *
   * 关键安全性:关键词层【只接管精确未命中】的值,对已精确命中的零影响;
   *   最坏退化 = _default(= 现状丢弃),错配只发生在"本来就要丢"的数据上 → 无回归。
   *
   * 典型场景:treatName 2万+ distinct,精确字典只 cover top N,剩余长尾以前全靠
   *   `_default: ''` 丢弃(→ 误召);keyword_mapping 用"含词+优先级"把长尾的真治疗救回 category。
   *
   * array 字段(如 treat_stages)只走精确 + _default(关键词只服务单值 category)。
   */
  private applyEnum(
    canonicalKey: string,
    value: unknown,
    mapping: Record<string, Record<string, string>> | undefined,
    keywordMapping?: Record<string, KeywordRule[]>,
    stripClauses?: string[],
  ): unknown {
    const fieldMap = mapping?.[canonicalKey];
    const kwRules = keywordMapping?.[canonicalKey];

    // ⭐ W4 末:支持 array 字段(如 treat_stages = ["开髓","根备"] → ["pulp_extirpation","canal_preparation"])
    if (Array.isArray(value)) {
      if (!fieldMap) return value;
      const out: string[] = [];
      for (const elem of value) {
        if (typeof elem !== 'string' && typeof elem !== 'number') continue;
        const key = String(elem);
        const mapped = fieldMap[key];
        const v = mapped !== undefined ? mapped : fieldMap['_default'];
        // 空字符串 = 不进 PAC enum(host 长尾值映射不出,丢)
        if (v && v !== '') out.push(v);
      }
      return out;
    }

    if (typeof value !== 'string' && typeof value !== 'number') return value;
    const key = String(value);

    // ① 精确命中(最高优先,现状不变)
    if (fieldMap) {
      const mapped = fieldMap[key];
      if (mapped !== undefined) return mapped;
    }
    // ② 关键词分类兜底(仅精确未命中时;'' = 显式丢弃)
    if (kwRules && kwRules.length) {
      const kw = classifyByKeyword(key, kwRules, stripClauses);
      if (kw !== undefined) return kw;
    }
    // ③ _default / 透传
    if (fieldMap) {
      const defaultV = fieldMap['_default'];
      if (defaultV !== undefined) return defaultV;
    }
    return value;
  }

}

/**
 * 关键词分类器(纯函数,host 无关,可单测):normalized 文本 → PAC enum。
 *   1. (可选)剥离条件/未来从句:按 , ; 。切段,丢以 stripClauses 任一词开头的段
 *      例 "充填,必要时根管治疗" + strip=["必要时"] → "充填"(避免被 endodontic 误抢)
 *   2. 按 rules 顺序(= 优先级)扫,文本含某规则 any 中任一词 → 返回该 rule.value(首个命中即停)
 *      优先级让"主操作/高价值"赢复合(种植>冠、根管>冠、操作词>人群词)
 * 返回 undefined = 无任何规则命中(交回 applyEnum 走 _default)。
 *
 * 注:输入应已经过 normalize(标点/空白归一);这里只做切段 + 含词。
 */
export function classifyByKeyword(
  value: string,
  rules: KeywordRule[],
  stripClauses?: string[],
): string | undefined {
  let text = value;
  if (stripClauses && stripClauses.length) {
    text = value
      .split(/[,;。，；]/) // 含全角逗号/分号:防御未归一输入,strip 切段不漏(否则整句一段 → 建议/若 没剥)
      .map((s) => s.trim())
      .filter((seg) => seg && !stripClauses.some((p) => seg.startsWith(p)))
      .join(',');
  }
  if (!text) return undefined;
  for (const rule of rules) {
    const hit = rule.any.some((kw) => kw && text.includes(kw));
    if (!hit) continue;
    // none:排除词(命中 any 但也含 none 中任一 → 不算命中,继续下一规则)。
    //   用于"含词但语义反转"——如 龋 any=[龋] none=[风险,可疑](高龋风险/可疑龋是风险/可疑,非龋洞)。
    if (rule.none && rule.none.some((kw) => kw && text.includes(kw))) continue;
    return rule.value;
  }
  return undefined;
}

// ─────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────

export interface AssemblerResult {
  canonical: string;
  /// v2.1:emits 跟随 result 传给 Synthesizer(yaml 缺 emits 时为 undefined,只 patient 主档允许)
  emits: EmitsConfig | undefined;
  /// rawSource = host 真原文(从 Layer A.5 transforms 注入的 __source_row 提取;
  /// 拆出来的 fact 该字段是父 EMR 完整原文,合规留底用)
  rows: Array<{ canonical: unknown; rawSource: Record<string, unknown> }>;
  errors: Array<{
    reason: string;
    issues?: unknown;
    sample?: unknown;
  }>;
  stats: {
    fetched: number;
    assembled: number;
    failed: number;
  };
}
