import { compareDecimals, parseDecimal, formatDecimal } from '../core/decimal';
import type { ImportedEvent, Transaction } from '../core/types';

/**
 * 多通道联合去重（参考 AutoAccounting 的 DuplicateDetector）。
 *
 * 与 statements.ts 的 deduplicate（基于 ID）不同，本模块处理「跨通道同一笔交易」：
 * 微信通知 + 银行短信 + OCR 截图 可能识别到同一笔交易，需联合去重。
 *
 * 判断规则：
 * 1. 时间完全相同 → 重复
 * 2. 双方都是 AI 生成 → 非重复（AI 不稳定）
 * 3. 时间窗口 + 金额匹配 + 对手方一致 → 重复（中置信度）
 * 4. 时间窗口 + 金额匹配（无对手方信息）→ 重复（低置信度）
 */

export interface DedupConfig {
  /** 时间窗口（分钟），默认 5。 */
  timeWindowMinutes: number;
  /** 是否启用账户关联检查。 */
  checkAccountLink: boolean;
}

export const DEFAULT_DEDUP_CONFIG: DedupConfig = {
  timeWindowMinutes: 5,
  checkAccountLink: true,
};

export type DedupConfidence = 'high' | 'medium' | 'low' | 'none';

export interface DedupResult {
  isDuplicate: boolean;
  confidence: DedupConfidence;
  reason?: string;
  /** 重复的来源事件/交易 id。 */
  duplicateOf?: string;
  /** 匹配到的对比项信息。 */
  matchedItem?: DedupComparable;
}

/** 去重比较所需的归一化形状（从 ImportedEvent 或 Transaction 映射）。 */
export interface DedupComparable {
  time: string;
  amount: string;
  counterparty?: string;
  transactionId?: string;
  rawTransaction?: Transaction;
}

/** 归一化对手方名称用于去重比较（去除括号后缀、空格、特殊字符）。 */
function normalizeCounterparty(cp: string): string {
  return cp
    .replace(/[（(][^)）]*[)）]/g, '')  // 去除括号及内容（如 "(国贸店)"）
    .replace(/[\s\-–—·•|]/g, '')       // 去除空格和分隔符
    .trim();
}

/** 把 ImportedEvent 归一化为去重比较形状。 */
export function toComparable(event: ImportedEvent): DedupComparable {
  return { time: event.occurredAt, amount: event.amount, counterparty: event.counterparty };
}

/** 把传入项（ImportedEvent 或 DedupComparable）归一化为 DedupComparable。 */
function normalize(item: ImportedEvent | DedupComparable): DedupComparable {
  return 'occurredAt' in item
    ? { time: item.occurredAt, amount: item.amount, counterparty: item.counterparty }
    : item;
}

/** 生成去重指纹（时间 + 金额 + 对手方）。 */
export function dedupFingerprint(event: ImportedEvent): string {
  const date = event.occurredAt.slice(0, 16);
  // 使用 parseDecimal 归一化金额，确保不同格式/精度可匹配
  let amount: string;
  try {
    const parsed = parseDecimal(event.amount);
    amount = formatDecimal(parsed.coefficient < 0n ? -parsed.coefficient : parsed.coefficient, parsed.scale);
  } catch {
    amount = event.amount.replace(/^-/, '');
  }
  return `${date}|${amount}|${event.counterparty}`;
}

/**
 * 检查单笔事件是否与已存在的事件/交易重复。
 * existing 可传 ImportedEvent 或已归一化的 DedupComparable（与历史交易去重时用）。
 */
export function checkDuplicate(
  event: ImportedEvent,
  existing: (ImportedEvent | DedupComparable)[],
  config: DedupConfig = DEFAULT_DEDUP_CONFIG,
): DedupResult {
  const eventAmountAbs = event.amount.replace(/^-/, '');
  const maxDiff = config.timeWindowMinutes * 60 * 1000;

  const eventTimeStr = event.occurredAt;
  const hasTimeEvent = eventTimeStr.includes(':');

  const toISO = (s: string) => s.includes('T') ? s : s.replace(' ', 'T');
  let eventTime = NaN;
  if (hasTimeEvent) {
    eventTime = Date.parse(toISO(eventTimeStr));
  }

  for (const item of existing) {
    const isEvent = 'occurredAt' in item;
    const exTimeStr = isEvent ? item.occurredAt : item.time;
    const exAmount = isEvent ? item.amount : item.amount;
    const exCounterparty = isEvent ? item.counterparty : item.counterparty;

    const hasTimeEx = exTimeStr.includes(':');
    const isSameDay = eventTimeStr.substring(0, 10) === exTimeStr.substring(0, 10);
    const exAmountAbs = exAmount.replace(/^-/, '');

    if (!hasTimeEvent || !hasTimeEx) {
      // 至少一方是日期粒度（例如历史账本数据）
      if (isSameDay && compareDecimals(eventAmountAbs, exAmountAbs) === 0) {
        const normEv = normalizeCounterparty(event.counterparty || '');
        const normEx = normalizeCounterparty(exCounterparty || '');
        const sameCounterparty = (normEv && normEx && (normEv === normEx || normEv.includes(normEx) || normEx.includes(normEv))) || (!normEv && !normEx);
        if (sameCounterparty) {
          return {
            isDuplicate: true,
            confidence: 'medium',
            reason: `同天发生、金额相同、对手方匹配（${exTimeStr}）`,
            matchedItem: normalize(item),
          };
        }
        // 同天 + 同金额 + 双方都有对手方但不一致 → 非重复（避免误杀同天同金额的不同交易）
        if (normEv && normEx) {
          continue;
        }
        // 同天 + 同金额 + 至少一方无对手方信息 → 低置信度重复
        return {
          isDuplicate: true,
          confidence: 'low',
          reason: `同天发生、金额相同（${exTimeStr}）`,
          matchedItem: normalize(item),
        };
      }
      continue;
    }

    // 双方均有时分秒，按精确时间窗匹配
    const exTime = Date.parse(toISO(exTimeStr));
    
    // 防护：Date.parse 返回 NaN 时跳过（避免无效时间格式绕过窗口检查）
    if (isNaN(eventTime) || isNaN(exTime)) continue;
    
    const timeDiff = Math.abs(eventTime - exTime);

    // 时间完全相同 → 高置信度重复
    if (timeDiff === 0 && compareDecimals(eventAmountAbs, exAmountAbs) === 0) {
      return {
        isDuplicate: true,
        confidence: 'high',
        reason: `时间相同、金额一致（${exTimeStr}）`,
        matchedItem: normalize(item),
      };
    }

    // 时间窗口内 + 金额匹配
    if (timeDiff <= maxDiff && compareDecimals(eventAmountAbs, exAmountAbs) === 0) {
      // 对手方匹配判定
      const normEvCp = normalizeCounterparty(event.counterparty || '');
      const normExCp = normalizeCounterparty(exCounterparty || '');
      const bothHaveCp = normEvCp.length > 0 && normExCp.length > 0;
      const sameCounterparty = bothHaveCp && (normEvCp === normExCp || normEvCp.includes(normExCp) || normExCp.includes(normEvCp));

      if (bothHaveCp && !sameCounterparty) {
        // 双方都有对手方且不同 → 不是重复（如 AA 分账、同时段不同商户消费）
        continue;
      }

      if (sameCounterparty) {
        return {
          isDuplicate: true,
          confidence: 'medium',
          reason: `时间相近（${Math.round(timeDiff / 60000)}分钟）、金额相同、对手方一致`,
          matchedItem: normalize(item),
        };
      }

      // 一方或双方缺少对手方信息 → 低置信度重复（无法确认是否为不同交易）
      return {
        isDuplicate: true,
        confidence: 'low',
        reason: `时间相近（${Math.round(timeDiff / 60000)}分钟）、金额相同`,
        matchedItem: normalize(item),
      };
    }
  }

  return { isDuplicate: false, confidence: 'none' };
}

/** Helper function to format Date as YYYY-MM-DD */
function formatDateString(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

/**
 * 根据 timeWindowMinutes 计算需要回溯/前瞻的天数。
 * 例如 5 分钟 → 0 天（仅同天），720 分钟（12小时）→ 1 天。
 */
function dateWindowDays(timeWindowMinutes: number): number {
  return Math.ceil(timeWindowMinutes / (24 * 60));
}

export interface DuplicateDetail {
  event: ImportedEvent;
  matchedWith?: DedupComparable;
  reason?: string;
  confidence?: DedupConfidence;
}

/**
 * 批量去重：从一组事件中筛出重复的，保留首个出现的。
 * 返回去重后的列表与被丢弃的重复项及对比详情。
 */
export function batchDedup(
  events: ImportedEvent[],
  config: DedupConfig = DEFAULT_DEDUP_CONFIG,
): { accepted: ImportedEvent[]; duplicates: ImportedEvent[]; duplicateDetails: DuplicateDetail[] } {
  const accepted: ImportedEvent[] = [];
  const duplicates: ImportedEvent[] = [];
  const duplicateDetails: DuplicateDetail[] = [];
  const acceptedByDate = new Map<string, ImportedEvent[]>();
  const windowDays = dateWindowDays(config.timeWindowMinutes);

  for (const event of events) {
    const eventDate = event.occurredAt.slice(0, 10);
    const [y, m, d] = eventDate.split('-').map(Number);

    // 收集 [eventDate - windowDays, eventDate + windowDays] 范围内的已接受事件
    const candidates: ImportedEvent[] = [];
    for (let offset = -windowDays; offset <= windowDays; offset++) {
      const dt = formatDateString(new Date(y, m - 1, d + offset));
      const list = acceptedByDate.get(dt);
      if (list) candidates.push(...list);
    }

    const result = checkDuplicate(event, candidates, config);
    if (result.isDuplicate) {
      duplicates.push(event);
      duplicateDetails.push({
        event,
        matchedWith: result.matchedItem,
        reason: result.reason,
        confidence: result.confidence,
      });
    } else {
      accepted.push(event);
      const list = acceptedByDate.get(eventDate) ?? [];
      list.push(event);
      acceptedByDate.set(eventDate, list);
    }
  }

  return { accepted, duplicates, duplicateDetails };
}

/**
 * 与历史交易（已入账）去重。
 * 用于检查新导入的事件是否已在 main.bean 里（避免重复入账）。
 */
export function dedupAgainstHistory(
  events: ImportedEvent[],
  history: Transaction[],
  config: DedupConfig = DEFAULT_DEDUP_CONFIG,
): { accepted: ImportedEvent[]; duplicates: ImportedEvent[]; duplicateDetails: DuplicateDetail[] } {
  const accepted: ImportedEvent[] = [];
  const duplicates: ImportedEvent[] = [];
  const duplicateDetails: DuplicateDetail[] = [];
  const windowDays = dateWindowDays(config.timeWindowMinutes);

  // 1. 提取历史已保存交易中的 import_id（精确判重集合）
  const historyImportIds = new Set<string>();
  for (const t of history) {
    if (t.metadata?.import_id) {
      for (const id of t.metadata.import_id.split(',')) {
        historyImportIds.add(id.trim());
      }
    }
  }

  // 2. Group history by date YYYY-MM-DD
  const historyByDate = new Map<string, DedupComparable[]>();
  for (const t of history) {
    const dateStr = t.date.slice(0, 10);
    const sourcePosting = t.postings.find(p => p.amount && (
      p.account.startsWith('Assets:') || p.account.startsWith('Liabilities:')
    )) ?? t.postings[0];
    const comparable: DedupComparable = {
      time: dateStr,
      amount: (sourcePosting?.amount ?? '0').replace(/^-/, ''),
      counterparty: (t.payee || t.narration || '').trim(),
      transactionId: t.id,
      rawTransaction: t,
    };
    const list = historyByDate.get(dateStr) ?? [];
    list.push(comparable);
    historyByDate.set(dateStr, list);
  }

  for (const event of events) {
    // 优先：O(1) 精确匹配已有的 import_id
    if (event.id && historyImportIds.has(event.id)) {
      duplicates.push(event);
      duplicateDetails.push({
        event,
        reason: '匹配到历史交易 import_id',
      });
      continue;
    }

    const eventDate = event.occurredAt.slice(0, 10);
    const [y, m, d] = eventDate.split('-').map(Number);

    // 收集 [eventDate - windowDays, eventDate + windowDays] 范围内的历史交易
    const candidates: DedupComparable[] = [];
    for (let offset = -windowDays; offset <= windowDays; offset++) {
      const dt = formatDateString(new Date(y, m - 1, d + offset));
      const list = historyByDate.get(dt);
      if (list) candidates.push(...list);
    }

    const result = checkDuplicate(event, candidates, config);
    if (result.isDuplicate) {
      duplicates.push(event);
      duplicateDetails.push({
        event,
        matchedWith: result.matchedItem,
        reason: result.reason,
        confidence: result.confidence,
      });
    } else {
      accepted.push(event);
    }
  }

  return { accepted, duplicates, duplicateDetails };
}
