/**
 * 无障碍文本解析器（微信/支付宝/银行账单详情页的文本解析）。
 *
 * 从 automationPipeline.ts 提取，职责：
 * - 解析微信/支付宝/银行无障碍文本为 ImportedEvent
 * - 纯文本匹配逻辑，不依赖 RN/Expo 原生模块
 */

import { compareDecimals } from '../../domain/core/decimal';
import { PAYMENT_PACKAGES } from '../../domain/core/constants';
import type { ImportedEvent } from '../../domain/core/types';
import { logger } from '../../utils/logger';
import { sanitizeLogText } from '../../utils/sanitize';

/**
 * 解析微信账单详情无障碍文本。
 * 特征：包含 "交易单号" 或 "退款单号" 或 "本服务由财付通提供"
 */
function parseWechatTexts(texts: string[], packageName: string): ImportedEvent | null {
  if (!texts.includes('交易单号') && !texts.includes('退款单号') && !texts.includes('本服务由财付通提供')) {
    return null;
  }

  let amountStr = '0';
  let direction: 'expense' | 'income' = 'expense';
  const amountPattern = /^([+-])?(\d+(\.\d{1,2})?)$/;

  for (const text of texts) {
    const match = text.match(amountPattern);
    if (match) {
      amountStr = match[2];
      if (match[1] === '+') direction = 'income';
      break;
    }
  }

  const amtIndex = texts.findIndex(t => amountPattern.test(t));
  const merchant = (amtIndex > 0 ? texts[amtIndex - 1] : texts[0] || '').split(',')[0].replace(/-退款$/, '');

  const timeLabelIndex = texts.findIndex(t => t.includes('时间'));
  const time = (timeLabelIndex !== -1 && timeLabelIndex + 1 < texts.length)
    ? texts[timeLabelIndex + 1]
    : new Date().toISOString().replace('T', ' ').substring(0, 19);

  if (compareDecimals(amountStr, '0') > 0 && merchant) {
    return {
      id: 'nodes_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
      occurredAt: time,
      amount: amountStr,
      currency: 'CNY',
      direction,
      counterparty: merchant,
      memo: texts.includes('已退款') ? '退款' : '微信支付',
      raw: { packageName },
    };
  }
  return null;
}

/**
 * 解析支付宝账单详情无障碍文本。
 * 特征：包含 "创建时间" 或 "账单详情" 或 "对此订单有疑问"
 */
function parseAlipayTexts(texts: string[], packageName: string): ImportedEvent | null {
  const isDetailPage = texts.includes('创建时间') || texts.includes('账单详情') || texts.includes('对此订单有疑问');
  if (!isDetailPage) {
    logger.debug('accessibilityParser', `[无障碍] 支付宝文本解析跳过：未识别为账单详情页特征`);
    return null;
  }

  let amountStr = '0';
  let direction: 'expense' | 'income' = 'expense';
  let memo = '支付宝账单';

  for (const text of texts) {
    const trimmed = text.trim();
    // 匹配 "支出12.93元" / "收入50.00元" / "-12.93元" / "12.93元" / "12.93"
    const match = trimmed.match(/^(?:(支出|收入|[+-]))?\s*(\d+(\.\d+)?)元?$/);
    if (match && match[2]) {
      const val = match[2];
      // 避免匹配整年或订单纯数字（如 "2026"），必须带小数点或包含支出/收入/元标志
      if (val.includes('.') || trimmed.endsWith('元') || trimmed.startsWith('支出') || trimmed.startsWith('收入') || trimmed.startsWith('-') || trimmed.startsWith('+')) {
        amountStr = val;
        const prefix = match[1];
        if (prefix === '+' || prefix === '收入') {
          direction = 'income';
        } else if (prefix === '-' || prefix === '支出') {
          direction = 'expense';
        } else if (texts.some(t => t.includes('收款') || t.includes('收入') || t.includes('收益') || t.includes('收到') || t.includes('红包'))) {
          direction = 'income';
        }
        break;
      }
    }
  }

  const amtIndex = texts.findIndex(t => /^(?:(支出|收入|[+-]))?\s*\d+(\.\d+)?元?$/.test(t.trim()));
  const merchant = amtIndex > 0 ? texts[amtIndex - 1] : texts[0] || '';

  // 时间提取：支持多种标签（支付时间/创建时间/交易时间/完成时间）
  const TIME_LABELS = ['支付时间', '创建时间', '交易时间', '完成时间'];
  let time = '';
  for (const label of TIME_LABELS) {
    const idx = texts.indexOf(label);
    if (idx !== -1 && idx + 1 < texts.length) {
      time = texts[idx + 1];
      break;
    }
  }
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  const memoLabelIndex = texts.indexOf('商品说明');
  if (memoLabelIndex !== -1 && memoLabelIndex + 1 < texts.length) {
    memo = texts[memoLabelIndex + 1];
  }

  // 提取「账单分类」作为分类提示（拼入 memo 供分类匹配使用）
  const catHintIndex = texts.indexOf('账单分类');
  if (catHintIndex !== -1 && catHintIndex + 1 < texts.length) {
    const catHint = texts[catHintIndex + 1];
    if (catHint && !memo.includes(catHint)) {
      memo = memo ? `${memo} ${catHint}` : catHint;
    }
  }



  const catLabelIndex = texts.indexOf('账单分类');
  if (catLabelIndex !== -1 && catLabelIndex + 1 < texts.length) {
    const rawCat = texts[catLabelIndex + 1];
    if (rawCat.includes('理财') || rawCat.includes('收益') || memo.includes('收益发放') || rawCat.includes('红包')) {
      direction = 'income';
    }
  }

  // 提取付款方式（供 resolveSourceAccount 解析资金来源账户）
  const methodLabelIndex = texts.indexOf('付款方式');
  const method = (methodLabelIndex !== -1 && methodLabelIndex + 1 < texts.length)
    ? texts[methodLabelIndex + 1].trim()
    : '';

  if (compareDecimals(amountStr, '0') > 0 && merchant) {
    const raw: Record<string, string> = { packageName };
    if (method) raw['收/付款方式'] = method;
    return {
      id: 'nodes_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
      occurredAt: time,
      amount: amountStr,
      currency: 'CNY',
      direction,
      counterparty: merchant,
      memo,
      raw,
    };
  }

  logger.debug('accessibilityParser', `[无障碍] 支付宝详情页解析未成功: 金额="${amountStr}", 商户="${merchant}"`, { texts: texts.map(sanitizeLogText) });
  return null;
}

// ─── 银行账单详情页解析（通用，按页面特征词分派，覆盖兴业/建行/招行等） ───

/** 银行详情页特征词（命中任一即视为银行详情页）。 */
const BANK_DETAIL_PAGE_LABELS = ['明细详情', '交易明细详情', '交易明细', '交易摘要', '记账日期', '记账日', '交易账户'];

/** 银行详情页金额标签（标签下一节点为金额值）。 */
const BANK_AMOUNT_LABELS = ['支出金额', '收入金额', '还款', '交易金额', '金额'];

/** 银行详情页商户/对手标签（标签下一节点为对手方）。 */
const BANK_COUNTERPARTY_LABELS = ['对方信息', '对方账户', '对方名称', '收款方全称', '交易摘要', '交易地点/附言'];

/** 银行详情页付款方式/资金来源标签（标签下一节点为资金来源账户描述）。 */
const BANK_METHOD_LABELS = ['交易账户', '付款方式', '付款账户', '交易账户卡号'];

/** 银行详情页时间标签（标签下一节点为时间值）。 */
const BANK_TIME_LABELS = ['交易时间', '支付时间', '记账日', '记账日期'];

/** 银行详情页备注/用途标签（标签下一节点为备注）。 */
const BANK_MEMO_LABELS = ['用途', '交易地点/附言', '交易摘要', '商品说明', '附言', '备注'];

/**
 * 银行详情页金额正则：兼容 ¥/￥ 前缀（含可选空格）、符号前置或后置负号、千分位逗号。
 * 命中后剥离货币符号、千分位逗号、空格，返回纯净的数字字符串。
 *
 * 支持：
 * - "¥49.00" / "￥49.00"
 * - "¥-12.32" / "-¥12.32" / "¥12.32-"（建行还款）
 * - "¥ 0.79"（金额前带空格）
 * - "11,565.20"（兴业账户余额，千分位）
 * - "49.00" / "66.8"（纯数字，1-2 位小数）
 * 不匹配：纯整数（如 "2026"，避免与年份/卡号混淆）
 */
const BANK_AMOUNT_PATTERN = /^[¥￥]?\s*([+-]?)\s*([\d,]+\.\d{1,2})\s*元?$|^[¥￥]?\s*([\d,]+\.\d{1,2})\s*([+-])\s*元?$/;

/** 从金额字符串里检测负号（前置或后置）。 */
function detectNegativeSign(raw: string): boolean {
  const trimmed = raw.trim();
  const match = trimmed.match(BANK_AMOUNT_PATTERN);
  if (!match) return false;
  // 前置符号 match[1] 或后置符号 match[4]
  return match[1] === '-' || match[4] === '-';
}

/** 解析单个金额字符串为纯数字（剥离 ¥/￥/, /符号），无法解析返回 null。不做方向判定。 */
function parseBankAmountValue(raw: string): string | null {
  const trimmed = raw.trim();
  const match = trimmed.match(BANK_AMOUNT_PATTERN);
  if (!match) return null;
  // 前置符号分支：match[2]=金额；后置符号分支：match[3]=金额
  const numStr = match[2] || match[3] || '';
  if (!numStr) return null;
  const amount = numStr.replace(/,/g, '');
  if (compareDecimals(amount, '0') === 0) return null;
  return amount;
}

/**
 * 判定交易方向。优先级：
 * 1. 金额标签本身（"支出金额"→expense，"收入金额"→income，"还款"→expense）
 * 2. 金额字符串里的负号（"¥-12.32"→expense）
 * 3. 兜底 expense（银行详情页多为支出通知）
 */
function detectBankDirection(amountLabel: string | undefined, amountRaw: string): 'expense' | 'income' {
  if (amountLabel) {
    if (amountLabel === '支出金额' || amountLabel === '还款' || amountLabel.includes('支出')) return 'expense';
    if (amountLabel === '收入金额' || amountLabel.includes('收入')) return 'income';
  }
  if (detectNegativeSign(amountRaw)) return 'expense';
  return 'expense'; // 银行详情页兜底视为支出（典型场景）
}

/**
 * 从银行详情页文本中提取金额与方向。
 * 方向判定优先用金额标签（兴业"支出金额"明确为支出），其次用金额符号。
 */
function extractBankAmount(texts: string[]): { amount: string; direction: 'expense' | 'income' } | null {
  // 1. 优先按金额标签提取（兴业"支出金额"→下一节点；建行"还款"→下一节点）
  for (const label of BANK_AMOUNT_LABELS) {
    const idx = texts.indexOf(label);
    if (idx !== -1 && idx + 1 < texts.length) {
      const rawValue = texts[idx + 1];
      const amount = parseBankAmountValue(rawValue);
      if (amount) {
        const direction = detectBankDirection(label, rawValue);
        return { amount, direction };
      }
    }
  }
  // 2. 兜底：遍历所有节点，找第一个符合银行金额格式的（带 ¥ 或千分位，避免误匹配）
  // 此场景无金额标签，方向只能靠符号判定；无符号时兜底 expense
  for (const text of texts) {
    const trimmed = text.trim();
    // 必须带 ¥/￥ 或千分位逗号，才视为银行金额（避免误吞支付宝"66.8"这种纯数字）
    if (/[¥￥]/.test(trimmed) || /^\d{1,3}(,\d{3})+\.\d{1,2}$/.test(trimmed)) {
      const amount = parseBankAmountValue(trimmed);
      if (amount) {
        const direction = detectBankDirection(undefined, trimmed);
        return { amount, direction };
      }
    }
  }
  return null;
}

/**
 * 规范化云闪付时间字符串。
 * 输入形如 "延迟扣费|2026年6月27日 13:30:15"，输出 "2026-06-27 13:30"。
 * 处理：去掉"延迟扣费|/即时扣费|"前缀、中文年月日转 -、补零。
 */
function normalizeUnionPayTime(raw: string): string {
  if (!raw) return '';
  let s = raw
    // 去掉 "|" 前的描述性前缀（延迟扣费| / 即时扣费| 等）
    .replace(/^[^|]*\|/, '')
    .replace(/^(?:延迟扣费|延迟扣款|即时扣费|即时扣款)\s*/, '')
    .replace(/年/g, '-')
    .replace(/月/g, '-')
    .replace(/日\s*/, ' ')
    .trim();
  // 补零 + 截取到分钟：2026-6-27 13:30:15 -> 2026-06-27 13:30
  const m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})/);
  if (m) {
    return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')} ${m[4].padStart(2, '0')}:${m[5]}`;
  }
  // 纯日期补零
  const mDate = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
  if (mDate) {
    return `${mDate[1]}-${mDate[2].padStart(2, '0')}-${mDate[3].padStart(2, '0')}`;
  }
  return s;
}

/**
 * 通用 App 时间规范化（滴滴/美团/京东/抖音共用）。
 *
 * 处理这些 App 无障碍节点的常见时间格式问题：
 * - 日期与时间粘连：`2026-04-2516:24:03`（无空格）→ 补空格
 * - 标准格式：`2026-07-24 18:51:36` → 截取到分钟
 * - 中文年月日：`2026年7月25日 12:26:11` → 转 -
 *
 * 输入也可能是从粘连长文本里正则提取出的时间片段。统一输出 `YYYY-MM-DD HH:mm`。
 */
function normalizeAppTime(raw: string): string {
  if (!raw) return '';
  let s = raw
    .replace(/年/g, '-')
    .replace(/月/g, '-')
    .replace(/日\s*/, ' ')
    .replace(/\//g, '-')
    .trim();
  // 修复日期时间粘连：2026-04-2516:24:03 → 2026-04-25 16:24:03
  s = s.replace(/^(\d{4}-\d{1,2}-\d{1,2})(\d{2}:\d{2})/, '$1 $2');
  // 补零 + 截取到分钟
  const m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})/);
  if (m) {
    return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')} ${m[4].padStart(2, '0')}:${m[5]}`;
  }
  // 纯日期补零
  const mDate = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
  if (mDate) {
    return `${mDate[1]}-${mDate[2].padStart(2, '0')}-${mDate[3].padStart(2, '0')}`;
  }
  return s;
}

/** 从粘连长文本中正则提取时间（标签 + 时间值粘连在同一节点）。 */
function extractTimeFromBlob(text: string, label: string): string {
  // 匹配 "创建时间2026-04-25 16:24:03" / "还款时间2026-06-1320:18:44" 等
  const re = new RegExp(`${label}\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}[\\s\\d:.-]*)`);
  const m = text.match(re);
  return m ? m[1].trim() : '';
}

/**
 * 解析滴滴出行（com.sdu.didi.psnger）订单详情页无障碍文本。
 *
 * 滴滴详情页特征：
 * - 标题含「订单详情」
 * - 商户名是标题节点，格式「特惠快车打车-方师傅-04月25日行程」（取"-"前主体）
 * - 金额是标题后的纯数字节点（10.80），有时粘连成「订单金额10.80」
 * - 时间常粘连在长节点里「创建时间2026-04-25 16:24:03...」
 * - 无付款方式字段
 */
function parseDidiTexts(texts: string[], packageName: string): ImportedEvent | null {
  if (!texts.some(t => t.includes('订单详情'))) return null;

  // 商户提取：找标题节点（含"打车"/"行程"），取"-"前主体
  let counterparty = '';
  for (const t of texts) {
    if (/打车|行程|快车|专车|拼车/.test(t)) {
      // 「特惠快车打车-方师傅-04月25日行程」→「特惠快车打车」
      const main = t.split('-')[0].trim();
      if (main && main.length >= 2) {
        counterparty = main;
        break;
      }
    }
  }

  // 金额提取：纯数字节点（10.80）或粘连「订单金额10.80」/「订单金额10,80」
  let amountStr = '';
  for (const t of texts) {
    const trimmed = t.trim();
    // 纯金额节点
    const m1 = trimmed.match(/^(\d+[.,]\d{2})$/);
    if (m1) { amountStr = m1[1].replace(',', '.'); break; }
    // 粘连节点：订单金额10.80 / 订单金额10,80
    const m2 = trimmed.match(/订单金额\s*(\d+[.,]\d{2})/);
    if (m2) { amountStr = m2[1].replace(',', '.'); break; }
  }
  if (!amountStr) return null;

  // 时间提取：优先「创建时间」标签下一节点；否则从粘连节点提取
  let time = '';
  const createIdx = texts.indexOf('创建时间');
  if (createIdx !== -1 && createIdx + 1 < texts.length) {
    time = texts[createIdx + 1].trim();
  }
  if (!time) {
    // 扫描粘连节点
    for (const t of texts) {
      const extracted = extractTimeFromBlob(t, '创建时间');
      if (extracted) { time = extracted; break; }
    }
  }
  time = normalizeAppTime(time);
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  return {
    id: 'nodes_didi_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
    occurredAt: time,
    amount: amountStr,
    currency: 'CNY',
    direction: 'expense',
    counterparty: counterparty || '滴滴出行',
    memo: '滴滴出行',
    raw: { packageName },
  };
}

/**
 * 解析美团（com.sankuai.meituan）消费详情页无障碍文本。
 *
 * 美团详情页特征：
 * - 标题含「消费详情」
 * - 金额是纯数字节点（9.36），但附近常混入 base64 图片哈希节点（干扰）
 * - 商户名在金额前一个有效节点（跳过 base64 哈希、「订单状态」等）
 * - 时间标签「支付时间」，付款方式标签「支付方式」
 * - 方向：美团月付消费默认 expense（日志曾误判 income）
 */
function parseMeituanTexts(texts: string[], packageName: string): ImportedEvent | null {
  if (!texts.some(t => t.includes('消费详情'))) return null;

  // 金额提取：纯数字节点（9.36），跳过 base64/png 干扰
  let amountStr = '';
  let amountIdx = -1;
  for (let i = 0; i < texts.length; i++) {
    const t = texts[i].trim();
    if (t.startsWith('png;base64') || /^[a-f0-9]{20,}$/i.test(t)) continue; // 跳过图片哈希
    const m = t.match(/^(\d+\.\d{2})$/);
    if (m) { amountStr = m[1]; amountIdx = i; break; }
  }
  if (!amountStr) return null;

  // 商户提取：金额前一个有效节点（跳过 base64 哈希、「订单状态」等）
  let counterparty = '';
  const SKIP_BEFORE_AMOUNT = ['订单状态', '消费详情', '返回'];
  for (let j = amountIdx - 1; j >= 0; j--) {
    const candidate = texts[j].trim();
    if (!candidate) continue;
    if (candidate.startsWith('png;base64') || /^[a-f0-9]{20,}$/i.test(candidate)) continue;
    if (SKIP_BEFORE_AMOUNT.includes(candidate)) continue;
    counterparty = candidate;
    break;
  }

  // 时间提取：「支付时间」标签下一节点
  let time = '';
  const payTimeIdx = texts.indexOf('支付时间');
  if (payTimeIdx !== -1 && payTimeIdx + 1 < texts.length) {
    time = texts[payTimeIdx + 1].trim();
  }
  time = normalizeAppTime(time);
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  // 付款方式：「支付方式」标签下一节点
  let method = '';
  const methodIdx = texts.indexOf('支付方式');
  if (methodIdx !== -1 && methodIdx + 1 < texts.length) {
    const candidate = texts[methodIdx + 1].trim();
    if (candidate && !candidate.startsWith('png;base64') && !/^[a-f0-9]{20,}$/i.test(candidate)) {
      method = candidate;
    }
  }

  const raw: Record<string, string> = { packageName };
  if (method) raw['收/付款方式'] = method;

  return {
    id: 'nodes_meituan_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
    occurredAt: time,
    amount: amountStr,
    currency: 'CNY',
    direction: 'expense',
    counterparty: counterparty || '美团',
    memo: method || '美团',
    raw,
  };
}

/**
 * 解析京东/京东外卖（com.jingdong.app.mall）账单详情页无障碍文本。
 *
 * 京东详情页特征：
 * - 标题含「账单详情」
 * - 金额是带负号数字节点（-12.30），direction=expense
 * - 商户名在金额前节点（京东外卖）
 * - 时间常粘连「创建时间2026-07-2418:54:58」
 * - 付款方式标签「付款方式」，值如「白条」
 */
function parseJingdongTexts(texts: string[], packageName: string): ImportedEvent | null {
  if (!texts.some(t => t.includes('账单详情'))) return null;

  // 金额提取：带负号数字节点（-12.30），或粘连文本里
  let amountStr = '';
  let amountIdx = -1;
  for (let i = 0; i < texts.length; i++) {
    const t = texts[i].trim();
    const m = t.match(/^(-?\d+\.\d{2})$/);
    if (m) { amountStr = m[1].replace(/^-/, ''); amountIdx = i; break; }
  }
  if (!amountStr) return null;

  // 商户提取：金额前节点
  let counterparty = '';
  if (amountIdx > 0) {
    for (let j = amountIdx - 1; j >= 0; j--) {
      const candidate = texts[j].trim();
      if (!candidate || candidate === '账单详情' || candidate === '返回') continue;
      counterparty = candidate;
      break;
    }
  }

  // 时间提取：「创建时间」标签下一节点，或粘连节点
  let time = '';
  const createIdx = texts.indexOf('创建时间');
  if (createIdx !== -1 && createIdx + 1 < texts.length) {
    time = texts[createIdx + 1].trim();
  }
  if (!time) {
    for (const t of texts) {
      const extracted = extractTimeFromBlob(t, '创建时间');
      if (extracted) { time = extracted; break; }
    }
  }
  time = normalizeAppTime(time);
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  // 付款方式：「付款方式」标签下一节点
  let method = '';
  const methodIdx = texts.indexOf('付款方式');
  if (methodIdx !== -1 && methodIdx + 1 < texts.length) {
    const candidate = texts[methodIdx + 1].trim();
    if (candidate && candidate !== '付款形式') method = candidate;
  }

  const raw: Record<string, string> = { packageName };
  if (method) raw['收/付款方式'] = method;

  return {
    id: 'nodes_jingdong_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
    occurredAt: time,
    amount: amountStr,
    currency: 'CNY',
    direction: 'expense',
    counterparty: counterparty || '京东',
    memo: method || '京东',
    raw,
  };
}

/**
 * 解析抖音（com.ss.android.ugc.aweme）月付还款页无障碍文本。
 *
 * 抖音还款页特征：
 * - 标题含「抖音月付还款」
 * - 商户即标题（抖音月付还款）
 * - 金额是纯数字节点（5.88），direction=expense（还款是支出）
 * - 时间标签「还款时间」，有时粘连
 * - 付款方式标签「支付方式」，值如「支支付宝」（OCR 错识"支付宝"为"支支付宝"）
 */
function parseDouyinTexts(texts: string[], packageName: string): ImportedEvent | null {
  const isDouyinRepay = texts.some(t => t.includes('抖音月付还款'))
    || (texts.some(t => t.includes('还款时间')) && texts.some(t => /抖音|月付/.test(t)));
  if (!isDouyinRepay) return null;

  // 金额提取：纯数字节点（5.88），跳过非金额
  let amountStr = '';
  for (const t of texts) {
    const m = t.trim().match(/^(\d+\.\d{2})$/);
    if (m) { amountStr = m[1]; break; }
  }
  if (!amountStr) return null;

  // 时间提取：「还款时间」标签下一节点，或粘连节点
  let time = '';
  const repayIdx = texts.indexOf('还款时间');
  if (repayIdx !== -1 && repayIdx + 1 < texts.length) {
    time = texts[repayIdx + 1].trim();
  }
  if (!time) {
    for (const t of texts) {
      const extracted = extractTimeFromBlob(t, '还款时间');
      if (extracted) { time = extracted; break; }
    }
  }
  time = normalizeAppTime(time);
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  // 付款方式：「支付方式」标签下一节点
  let method = '';
  const methodIdx = texts.indexOf('支付方式');
  if (methodIdx !== -1 && methodIdx + 1 < texts.length) {
    method = texts[methodIdx + 1].trim();
  }

  const raw: Record<string, string> = { packageName };
  if (method) raw['收/付款方式'] = method;

  return {
    id: 'nodes_douyin_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
    occurredAt: time,
    amount: amountStr,
    currency: 'CNY',
    direction: 'expense',
    counterparty: '抖音月付还款',
    memo: method ? `${method} 抖音月付还款` : '抖音月付还款',
    raw,
  };
}

/**
 * 解析云闪付（com.unionpay）交易详情页无障碍文本。
 *
 * 云闪付详情页特征（与支付宝/银行不同，无标准标签）：
 * - 标题含「云闪付交易详情」
 * - 商户名裸露在「-¥金额」上一节点（如 "长沙公交...公司(乘车码)" → "-¥1.16"）
 * - 付款方式有标签「付款方式」，值为「招商银行银联信用卡[1589]」等
 * - 时间在「扣款时间」标签后，格式「延迟扣费|2026年6月27日 13:30:15」
 * - 订单金额标签「订单金额」，值为「¥1.40」
 *
 * @returns 识别成功返回 ImportedEvent，否则 null（交由 parseBankTexts 兜底）
 */
function parseUnionPayTexts(texts: string[], packageName: string): ImportedEvent | null {
  // 特征词判定：标题或「乘车码/云闪付」+ 金额符号
  const isUnionPayDetail = texts.some(t => t.includes('云闪付交易详情'))
    || (texts.some(t => t.includes('乘车码')) && texts.some(t => /[¥￥]/.test(t)));
  if (!isUnionPayDetail) return null;

  // 金额提取：找形如 "-¥1.16" / "+¥0.10" 的节点（云闪付金额带符号+货币符号）
  let amountStr = '';
  let direction: 'expense' | 'income' = 'expense';
  let amountIdx = -1;
  for (let i = 0; i < texts.length; i++) {
    const t = texts[i].trim();
    // 匹配 -¥1.16 / +¥0.10 / ¥1.40 / -￥1.16
    const m = t.match(/^([+-])[¥￥]\s*([\d,]+\.\d{1,2})$|^[¥￥]\s*([\d,]+\.\d{1,2})$/);
    if (m) {
      if (m[1] === '-') {
        amountStr = m[2]; direction = 'expense'; amountIdx = i; break;
      } else if (m[1] === '+') {
        amountStr = m[2]; direction = 'income'; amountIdx = i; break;
      } else if (m[3]) {
        // 无符号金额（如订单金额 ¥1.40），暂存但不立即 break，优先取带负号的实付金额
        if (!amountStr) { amountStr = m[3]; amountIdx = i; }
      }
    }
  }
  if (!amountStr) return null;

  // 商户提取：金额节点的上一节点（云闪付商户名裸露，如 "长沙公交...公司(乘车码）"）
  // 跳过"优惠信息/当前状态"等非商户节点
  let counterparty = '';
  if (amountIdx > 0) {
    for (let j = amountIdx - 1; j >= 0; j--) {
      const candidate = texts[j].trim();
      if (!candidate) continue;
      if (['优惠信息', '当前状态', '客服按钮'].includes(candidate)) continue;
      // 商户名通常含"公司/店/中心/站"等，或带括号说明
      counterparty = candidate;
      break;
    }
  }
  // 清洗全角括号统一为半角，去掉"乘车码"后缀可选（保留更具体）
  counterparty = counterparty.replace(/（/g, '(').replace(/）/g, ')');

  // 时间提取：「扣款时间」标签下一节点，格式 "延迟扣费|2026年6月27日 13:30:15"
  let time = '';
  const payTimeIdx = texts.indexOf('扣款时间');
  if (payTimeIdx !== -1 && payTimeIdx + 1 < texts.length) {
    time = texts[payTimeIdx + 1].trim();
  }
  if (!time) {
    const createIdx = texts.findIndex(t => t.includes('时间'));
    if (createIdx !== -1 && createIdx + 1 < texts.length) time = texts[createIdx + 1].trim();
  }
  // 清洗时间：去掉"延迟扣费|"等描述性前缀，规范化为 "YYYY-MM-DD HH:mm"
  time = normalizeUnionPayTime(time);
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  // 付款方式提取：「付款方式」标签下一节点（招商银行银联信用卡[1589]）
  let method = '';
  const methodIdx = texts.indexOf('付款方式');
  if (methodIdx !== -1 && methodIdx + 1 < texts.length) {
    method = texts[methodIdx + 1].trim();
  }

  const raw: Record<string, string> = { packageName };
  if (method) raw['收/付款方式'] = method;

  if (compareDecimals(amountStr, '0') > 0) {
    return {
      id: 'nodes_unionpay_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
      occurredAt: time,
      amount: amountStr,
      currency: 'CNY',
      direction,
      counterparty,
      memo: method ? `${method}` : '云闪付',
      raw,
    };
  }
  return null;
}

/**
 * 解析银行账单详情无障碍文本（通用，覆盖兴业/建行/招行/工行等）。
 *
 * 分派依据：页面特征词（明细详情/交易摘要/记账日 等），而非包名白名单。
 * 各家银行详情页结构高度相似：标签 + 下一节点为值，复用支付宝解析器的提取模式。
 */
function parseBankTexts(texts: string[], packageName: string): ImportedEvent | null {
  // 页面特征词判定
  const hit = BANK_DETAIL_PAGE_LABELS.some(label => texts.includes(label));
  if (!hit) return null;

  // 金额提取（含方向判定）
  const amountResult = extractBankAmount(texts);
  if (!amountResult) {
    logger.debug('accessibilityParser', `[无障碍] 银行详情页金额提取失败`, { texts: texts.map(sanitizeLogText) });
    return null;
  }

  // 定位金额节点索引（用于商户兜底：金额前一个节点常是商户名，如招行"GOOGLE *ChatGPT"→"¥143.97"）
  let amountIdx = -1;
  for (let i = 0; i < texts.length; i++) {
    if (parseBankAmountValue(texts[i].trim())) { amountIdx = i; break; }
  }

  // 时间提取：含关键词的节点取下一节点（兼容带冒号标签"交易时间:"/精确标签"交易时间"）
  let time = '';
  for (const label of BANK_TIME_LABELS) {
    const idx = texts.findIndex(t => t.includes(label));
    if (idx !== -1 && idx + 1 < texts.length) {
      time = texts[idx + 1].trim();
      break;
    }
  }
  if (!time) time = new Date().toISOString().replace('T', ' ').substring(0, 19);

  // 商户/对手提取：标签下一节点
  let counterparty = '';
  for (const label of BANK_COUNTERPARTY_LABELS) {
    const idx = texts.findIndex(t => t.includes(label));
    if (idx !== -1 && idx + 1 < texts.length) {
      const candidate = texts[idx + 1].trim();
      // 跳过 base64 图片、空串、纯数字（如卡号尾号）
      if (candidate && !candidate.startsWith('png;base64') && !/^\d+$/.test(candidate)) {
        counterparty = candidate;
        break;
      }
    }
  }
  // 兜底商户：金额前一个有效节点（招行等无商户标签的银行，商户名裸露在金额前）
  if (!counterparty && amountIdx > 0) {
    for (let j = amountIdx - 1; j >= 0; j--) {
      const candidate = texts[j].trim();
      if (!candidate) continue;
      if (candidate.startsWith('png;base64')) continue;
      if (/^\d+$/.test(candidate)) continue; // 跳过纯数字（卡号）
      if (BANK_DETAIL_PAGE_LABELS.includes(candidate)) continue; // 跳过特征词（如"交易明细"）
      if (['图标', '明细详情', '交易明细详情', '返回'].includes(candidate)) continue;
      counterparty = candidate;
      break;
    }
  }
  if (!counterparty) counterparty = '';

  // 付款方式/资金来源提取（写入 raw，供 resolveSourceAccount 解析）
  let method = '';
  for (const label of BANK_METHOD_LABELS) {
    const idx = texts.findIndex(t => t.includes(label));
    if (idx !== -1 && idx + 1 < texts.length) {
      const candidate = texts[idx + 1].trim();
      if (candidate && !candidate.startsWith('png;base64')) {
        method = candidate;
        break;
      }
    }
  }
  // 兜底：银行通知常不含持卡账户字段（如兴业被动扣款），用包名友好名作为资金来源，
  // 避免落入 resolveSourceAccount 的 Assets:Unknown 兜底（污染账本）。
  // 如兴业 com.cib.cibmb → "兴业银行" → Assets:兴业银行，比 Unknown 更合理。
  if (!method) {
    method = PAYMENT_PACKAGES[packageName] || '';
  }

  // 备注提取：含关键词的节点取下一节点
  let memo = '';
  for (const label of BANK_MEMO_LABELS) {
    const idx = texts.findIndex(t => t.includes(label));
    if (idx !== -1 && idx + 1 < texts.length) {
      const candidate = texts[idx + 1].trim();
      if (candidate && !candidate.startsWith('png;base64')) {
        memo = candidate;
        break;
      }
    }
  }

  const raw: Record<string, string> = { packageName };
  if (method) raw['收/付款方式'] = method;
  // 还原文案也拼入 memo，便于后续还款识别规则命中
  if (texts.some(t => t === '还款') && !memo.includes('还款')) {
    memo = memo ? `${memo} 还款` : '还款';
  }

  return {
    id: 'nodes_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9),
    occurredAt: time,
    amount: amountResult.amount,
    currency: 'CNY',
    direction: amountResult.direction,
    counterparty,
    memo,
    raw,
  };
}

/**
 * 直接通过无障碍文本进行解析（无需截图 OCR，速度更快且省电）。
 *
 * 分派顺序：
 * 1. 微信/支付宝（按包名，保持原有行为）
 * 2. 银行（按页面特征词，覆盖兴业/建行/招行等各家银行详情页）
 * @returns 解析出的 ImportedEvent，未识别返回 null
 */
export function parseAccessibilityTexts(texts: string[], packageName: string): ImportedEvent | null {
  if (packageName === 'com.tencent.mm') {
    return parseWechatTexts(texts, packageName);
  }
  if (packageName === 'com.eg.android.AlipayGphone') {
    return parseAlipayTexts(texts, packageName);
  }
  // 云闪付：详情页无标准标签，需专用解析（商户名裸露在金额上一节点）
  if (packageName === 'com.unionpay') {
    const event = parseUnionPayTexts(texts, packageName);
    if (event) return event;
  }
  // 滴滴出行：订单详情页，商户名是标题节点
  if (packageName === 'com.sdu.didi.psnger') {
    const event = parseDidiTexts(texts, packageName);
    if (event) return event;
  }
  // 美团：消费详情页，商户名在金额前节点（跳过 base64 哈希干扰）
  if (packageName === 'com.sankuai.meituan') {
    const event = parseMeituanTexts(texts, packageName);
    if (event) return event;
  }
  // 京东/京东外卖：账单详情页，金额带负号
  if (packageName === 'com.jingdong.app.mall') {
    const event = parseJingdongTexts(texts, packageName);
    if (event) return event;
  }
  // 抖音：月付还款页，标题即商户
  if (packageName === 'com.ss.android.ugc.aweme') {
    const event = parseDouyinTexts(texts, packageName);
    if (event) return event;
  }
  // 银行及其他 App：按页面特征词分派（不依赖包名白名单，覆盖各家银行）
  return parseBankTexts(texts, packageName);
}
