import type { DeriveExpr, DeriveOp } from '../transforms.schema';
import { isEmptyText, type Row } from '../row';

/**
 * derive — 给行加新字段(in-place,output 可以同 input)。
 *
 * 9 个内置 op(纯函数,无副作用;另含 recode 值映射):
 *   - substring   { op, from, start, end? }
 *   - concat      { op, parts: ["${field}" | "literal", ...] }
 *   - lower / upper  { op, from }
 *   - default     { op, from, value } — 原值空时填 value
 *   - trim        { op, from } — 去首尾空白 + 尾部噪声标点(脏中文名归一)
 *   - normalize   { op, from } — trim 升级:头尾空白/标点 + CJK→ASCII 中段标点
 *                                (中括号 () / 中逗号 , / 中分号 ; / 中< >)统一 ASCII
 *                                适用任何中文宿主的 enum_mapping 匹配前归一(不是 jvs-dw 特化)
 *   - coalesce    { op, from: [..] } — 取第一个非空字段值
 *
 * 不允许任意 JS 表达式 / eval。撞墙必须加新 op + 走 review。
 */
/// 尾部噪声标点(中英文)+ 空白;诊断 message 归一用(牙龈炎。/牙龈炎；/牙龈炎\r\n → 牙龈炎)
const TRAILING_NOISE = /[\s。;；,，?？!！、.]+$/u;

/// CJK 全角标点 → ASCII 半角(中段替换;trim 只去首尾,normalize 多走这步)
/// 范围:host enum_mapping 字典编辑时容易写英文标点,DW 实际多为中文标点,
/// 用 normalize 统一到 ASCII 后,字典只需写一种(ASCII 版),覆盖所有写法变体。
/// 任何中文宿主都通用 — 不是 jvs-dw 特化,所以在 transforms 层(通用),不在 yaml(宿主层)
const CJK_PUNCT_MAP: Record<string, string> = {
  '（': '(',  // （ 中括号左
  '）': ')',  // ） 中括号右
  '，': ',',  // , 中逗号
  '；': ';',  // ; 中分号
  '：': ':',  // : 中冒号
  '＜': '<',  // ＜ 中尖括号左
  '＞': '>',  // ＞ 中尖括号右
  '％': '%',  // ％ 中百分号
  '、': ',',  // 、 顿号 → 逗号(语义对齐:同等枚举分隔符)
};
const CJK_PUNCT_KEYS = Object.keys(CJK_PUNCT_MAP).join('');
const CJK_PUNCT_REGEX = new RegExp(`[${CJK_PUNCT_KEYS}]`, 'gu');

/// 全角字母/数字 → 半角(ＲＣＴ→RCT、０-９→0-9、Ａ-Ｚ/ａ-ｚ→ASCII)。
/// host EMR free-text 常夹全角英数(ＲＣＴ治疗后),不折叠则 enum/keyword 字典(半角)永远漏。
/// FF10-19 数字 / FF21-3A 大写 / FF41-5A 小写,统一 -0xFEE0 落到 ASCII;通用,非宿主特化。
const FULLWIDTH_ALNUM_REGEX = /[０-９Ａ-Ｚａ-ｚ]/gu;

function normalizeText(s: string): string {
  return s
    .trim()
    .replace(TRAILING_NOISE, '')
    .replace(CJK_PUNCT_REGEX, (ch) => CJK_PUNCT_MAP[ch] ?? ch)
    .replace(FULLWIDTH_ALNUM_REGEX, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xfee0))
    .trim();
}

export function runDerive(op: DeriveOp, rows: Row[]): Row[] {
  return rows.map((row) => {
    const next: Row = { ...row };
    for (const [outputField, expr] of Object.entries(op.fields)) {
      next[outputField] = evalExpr(expr, next);
    }
    return next;
  });
}

function evalExpr(expr: DeriveExpr, row: Row): unknown {
  switch (expr.op) {
    case 'substring': {
      const v = row[expr.from];
      if (typeof v !== 'string') return v ?? null;
      return expr.end !== undefined ? v.substring(expr.start, expr.end) : v.substring(expr.start);
    }
    case 'concat': {
      const parts = expr.parts.map((p) => {
        const m = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/.exec(p);
        if (m) {
          const val = row[m[1]!];
          return val === undefined || val === null ? '' : String(val);
        }
        return p;
      });
      return parts.join('');
    }
    case 'lower': {
      const v = row[expr.from];
      return typeof v === 'string' ? v.toLowerCase() : (v ?? null);
    }
    case 'upper': {
      const v = row[expr.from];
      return typeof v === 'string' ? v.toUpperCase() : (v ?? null);
    }
    case 'default': {
      const v = row[expr.from];
      return isEmptyText(v) ? expr.value : v;
    }
    case 'trim': {
      const v = row[expr.from];
      if (typeof v !== 'string') return v ?? null;
      return v.trim().replace(TRAILING_NOISE, '').trim();
    }
    case 'normalize': {
      const v = row[expr.from];
      if (typeof v !== 'string') return v ?? null;
      return normalizeText(v);
    }
    case 'coalesce': {
      for (const f of expr.from) {
        const v = row[f];
        if (!isEmptyText(v)) return v;
      }
      return null;
    }
    case 'recode': {
      const v = row[expr.from];
      if (typeof v !== 'string') return v ?? null;
      return Object.prototype.hasOwnProperty.call(expr.map, v) ? expr.map[v] : v;
    }
  }
}
