"""发票与支付记录匹配

将提取到的发票数据与支付记录进行金额匹配，
输出以支付记录为主键的结果列表。

支付记录由统一提取模块（extractor）根据 LLM 返回的「invoice_type」字段分类而来，
不再按文件类型假设文档类型。

## 业务约束

- 发票数 >= 付款记录数（最少一张发票对应一张付款记录）
- 发票总金额 >= 付款总金额（发票只能比付款多，不能少）
- 若发票数 == 付款数，走一对一匹配，无需一对多

## 匹配流程

1. 接收分类好的发票和支付记录列表
2. 解析发票和刷卡记录的金额，进行总额校验
   - 发票总额 < 刷卡总额时发出 warning
3. 按金额降序排序
4. 文件名匹配（最高优先级）：发票和刷卡记录的文件名（不含后缀）一致时直接匹配
5. 根据数量关系选择匹配策略：
   - 数量相等 → 一对一匹配：按金额从大到小依次配对，相对容差内即匹配
   - 发票更多 → 一对多匹配：对每张刷卡记录贪心凑金额，相对容差内结束
6. 构建以支付记录为主键的结果列表
7. 未匹配的发票单独作为一条记录（无刷卡信息）
8. 清理内部字段，输出支付记录列表

## 容差计算

使用相对容差（默认 3%），以刷卡金额为基准：
- ¥2900 发票 vs ¥2850 刷卡 → 差 ¥50，容差 ¥85.5 → 匹配成功
- ¥100 发票 vs ¥95 刷卡 → 差 ¥5，容差 ¥3.0 → 不匹配（需精确匹配或调整）

## 一对多匹配细节

- 对每张刷卡记录，维护 remaining（剩余待匹配金额）
- 遍历未分配的发票（按金额降序）：
  - 若发票金额 + 容差 >= remaining，视为最后一张，匹配后退出
  - 否则发票金额不超过 remaining + 容差即可匹配
- 匹配后 remaining 为负且超出容差时回滚最后一张发票
- 每张发票只会被分配一次

## 对外接口

    match_invoices_to_cards(invoices, cards, tolerance) -> list[dict]
"""

from pathlib import Path
from typing import Any

from .. import get_logger

log = get_logger("matcher")


def _safe_float(value: str | None, default: float = 0.0) -> float:
    """安全转换为浮点数"""
    if value is None or str(value).strip() == "":
        return default
    try:
        return float(str(value).replace(",", ""))
    except (ValueError, TypeError):
        return default


def _build_invoice_summary(invoices: list[dict[str, Any]]) -> str:
    """将多张发票信息汇总为备注字符串"""
    parts = []
    for inv in invoices:
        inv_type = inv.get("invoice_type", "unknown")
        amount = inv.get("total_amount", "unknown")

        if inv_type == "train":
            label = inv.get("person_name") or inv.get("invoice_number", "unknown")
        elif inv_type == "hotel":
            label = "hotel"
        else:
            label = inv.get("item_name") or inv.get("invoice_number", "unknown")

        parts.append(f"{inv_type}[{label}]¥{amount}")
    return " | ".join(parts)


def _relative_tolerance(base: float, rate: float = 0.03) -> float:
    """根据基准金额计算相对容差（默认 3%）"""
    return abs(base) * rate


def match_invoices_to_cards(
    invoices: list[dict[str, Any]],
    cards: list[dict[str, Any]] | None = None,
    tolerance: float = 0.03,
) -> list[dict[str, Any]]:
    """将发票与支付记录按金额匹配，输出以支付记录为主键的结果列表

    支付记录由统一提取模块（extractor）根据 LLM 返回的「invoice_type」字段分类而来。

    业务约束：
        - 发票数 >= 付款记录数
        - 发票总金额 >= 付款总金额（发票只能比付款多，不能少）
        - 若发票数 == 付款数，一对一匹配，无需一对多

    Args:
        invoices: 发票列表，需包含 "total_amount" 字段
        cards: 支付记录列表，需包含 "card_amount" 字段（由 extractor 分类提供）
        tolerance: 金额匹配容差比例（默认 0.03 = 3%）

    Returns:
        以支付记录为主键的结果列表，每条记录包含：
        - card_date, card_no, card_amount（支付信息）
        - 关联发票列表（_matched_invoices）
        - 发票详情备注
        - 未匹配发票单独作为一条无刷卡信息的记录
    """
    if not cards:
        log.warning("无支付记录可供匹配，发票将保持原状")
        return _invoices_to_records(invoices)

    # 解析金额
    for card in cards:
        card["_amount"] = _safe_float(card.get("card_amount"))
    for inv in invoices:
        inv["_amount"] = _safe_float(inv.get("total_amount"))

    # 数据校验
    total_invoices = sum(inv["_amount"] for inv in invoices)
    total_cards = sum(card["_amount"] for card in cards)
    log.info(
        f"金额校验: 发票总额 ¥{total_invoices:.2f}, 刷卡总额 ¥{total_cards:.2f}, "
        f"发票数 {len(invoices)}, 刷卡数 {len(cards)}"
    )

    total_tolerance = _relative_tolerance(max(total_invoices, total_cards), tolerance)
    if total_invoices < total_cards - total_tolerance:
        log.warning(
            f"发票总额 (¥{total_invoices:.2f}) 小于刷卡总额 (¥{total_cards:.2f}), "
            f"超出容差 {tolerance * 100:.0f}%, 匹配结果可能有偏差"
        )

    # 按金额降序排序
    cards.sort(key=lambda c: c["_amount"], reverse=True)
    invoices.sort(key=lambda i: i["_amount"], reverse=True)

    # 执行匹配，返回 {card_index: [invoice_indices]} 的映射
    card_to_invoices = _match(cards, invoices, tolerance)

    # 构建支付记录列表
    records = _build_payment_records(cards, invoices, card_to_invoices)

    # 清理内部字段
    for inv in invoices:
        inv.pop("_amount", None)
    for card in cards:
        card.pop("_amount", None)

    # 统计
    matched_invoices = sum(len(inv_list) for inv_list in card_to_invoices.values())
    unmatched_count = len(invoices) - matched_invoices
    log.info(f"匹配完成: {len(records)} 条支付记录, {matched_invoices}/{len(invoices)} 张发票已关联")
    if unmatched_count:
        log.info(f"未匹配发票: {unmatched_count} 张（已单独列为记录）")

    return records


def _match(
    cards: list[dict[str, Any]],
    invoices: list[dict[str, Any]],
    tolerance: float,
) -> dict[int, list[int]]:
    """执行匹配，返回 {card_index: [invoice_indices]} 的映射

    tolerance 为相对容差比例（如 0.03 表示 3%）

    匹配优先级（从高到低）：
        1. 文件名匹配：发票和刷卡记录的文件名（不含后缀）一致时直接匹配
        2. 精确匹配：金额差 <= 0.01 元
        3. 一对一 / 一对多贪心匹配：按金额容差匹配
    """
    result: dict[int, list[int]] = {}
    assigned: set[int] = set()

    # ---- 阶段 0：文件名匹配（最高优先级）----
    _match_by_filename(invoices, cards, assigned, result)

    if len(invoices) == len(cards):
        _match_one_to_one(invoices, cards, tolerance, assigned, result)
    else:
        _match_one_to_many(invoices, cards, tolerance, assigned, result)

    return result


def _match_by_filename(
    invoices: list[dict[str, Any]],
    cards: list[dict[str, Any]],
    assigned: set[int],
    result: dict[int, list[int]],
) -> None:
    """文件名匹配：发票和刷卡记录的文件名（不含后缀）一致时直接匹配"""
    for card_idx, card in enumerate(cards):
        card_name = card.get("_source_file", "")
        if not card_name:
            continue
        card_stem = Path(card_name).stem

        for idx, inv in enumerate(invoices):
            if idx in assigned:
                continue
            inv_name = inv.get("_source_file", "")
            if not inv_name:
                continue
            inv_stem = Path(inv_name).stem

            if inv_stem == card_stem:
                assigned.add(idx)
                result[card_idx] = [idx]
                log.info(f"[文件名匹配] {inv_name} ↔ {card_name}")
                break


def _match_one_to_one(
    invoices: list[dict[str, Any]],
    cards: list[dict[str, Any]],
    tolerance: float,
    assigned: set[int],
    result: dict[int, list[int]],
) -> None:
    """一对一匹配：发票数等于刷卡数，按金额从大到小依次配对"""
    for card_idx, card in enumerate(cards):
        if card_idx >= len(invoices):
            break
        inv = invoices[card_idx]
        diff = abs(inv["_amount"] - card["_amount"])
        card_tol = _relative_tolerance(card["_amount"], tolerance)
        if diff <= card_tol:
            assigned.add(card_idx)
            result[card_idx] = [card_idx]
            log.info(
                f"[一对一] {inv.get('invoice_number', 'unknown')} ¥{inv['_amount']:.2f} "
                f"↔ {card.get('_source_file', 'unknown')} ¥{card['_amount']:.2f}"
            )
        else:
            log.warning(
                f"[一对一] 金额偏差超出容差: "
                f"{inv.get('invoice_number', 'unknown')} ¥{inv['_amount']:.2f} "
                f"vs ¥{card['_amount']:.2f} (差 ¥{diff:.2f}, 容差 ¥{card_tol:.2f})"
            )


def _match_one_to_many(
    invoices: list[dict[str, Any]],
    cards: list[dict[str, Any]],
    tolerance: float,
    assigned: set[int],
    result: dict[int, list[int]],
) -> None:
    """一对多匹配：一张刷卡可能对应多张发票，按金额从大到小贪心匹配"""

    # ---- 阶段 1：精确匹配（金额差 <= 0.01 元视为相等）----
    exact_tolerance = 0.01
    for card_idx, card in enumerate(cards):
        card_amount = card["_amount"]
        if card_amount <= 0:
            continue

        for idx, inv in enumerate(invoices):
            if idx in assigned:
                continue
            inv_amount = inv["_amount"]
            if inv_amount <= 0:
                continue

            if abs(inv_amount - card_amount) <= exact_tolerance:
                assigned.add(idx)
                result[card_idx] = [idx]
                log.info(
                    f"[一对多-精确] {inv.get('invoice_number', 'unknown')} ¥{inv_amount:.2f} "
                    f"↔ {card.get('_source_file', 'unknown')} ¥{card_amount:.2f}"
                )
                break

    # ---- 阶段 2：贪心匹配（仅处理未精确匹配的刷卡记录）----
    for card_idx, card in enumerate(cards):
        if card_idx in result:
            continue

        card_amount = card["_amount"]
        if card_amount <= 0:
            continue

        card_tol = _relative_tolerance(card_amount, tolerance)

        remaining = card_amount
        matched_indices: list[int] = []

        for idx, inv in enumerate(invoices):
            if idx in assigned:
                continue
            if remaining <= card_tol:
                break

            inv_amount = inv["_amount"]
            if inv_amount <= 0:
                continue

            if inv_amount + card_tol >= remaining:
                is_match = True
            else:
                is_match = inv_amount <= remaining + card_tol

            if is_match:
                assigned.add(idx)
                matched_indices.append(idx)
                remaining -= inv_amount
                if remaining <= card_tol:
                    break

        # 回滚：如果匹配后 remaining 为负且超出容差
        if remaining < -card_tol and matched_indices:
            last_idx = matched_indices.pop()
            assigned.discard(last_idx)
            remaining += invoices[last_idx]["_amount"]

        # 记录匹配结果
        if matched_indices:
            result[card_idx] = matched_indices
            for idx in matched_indices:
                inv = invoices[idx]
                log.info(
                    f"[一对多-贪心] {inv.get('invoice_number', 'unknown')} ¥{inv['_amount']:.2f} "
                    f"→ {card.get('_source_file', 'unknown')} ¥{card['_amount']:.2f}"
                )


def _build_payment_records(
    cards: list[dict[str, Any]],
    invoices: list[dict[str, Any]],
    card_to_invoices: dict[int, list[int]],
) -> list[dict[str, Any]]:
    """构建以支付记录为主键的结果列表"""
    records: list[dict[str, Any]] = []

    for card_idx, inv_indices in card_to_invoices.items():
        card = cards[card_idx]
        matched_invs = [invoices[idx] for idx in inv_indices]

        record = {
            "card_date": card.get("card_date", ""),
            "card_no": card.get("card_no", ""),
            "card_amount": str(card["_amount"]),
            "relative_invoice_count": str(len(matched_invs)),
            "invoice_detail": _build_invoice_summary(matched_invs),
            "remark": "",
            "_source_file": card.get("_source_file", ""),
            "_matched_invoices": matched_invs,
        }
        records.append(record)

    # 未匹配的发票，单独作为记录
    matched_indices = set()
    for inv_indices in card_to_invoices.values():
        matched_indices.update(inv_indices)

    unmatched = [inv for idx, inv in enumerate(invoices) if idx not in matched_indices]
    for inv in unmatched:
        record = {
            "card_date": "",
            "card_no": "",
            "card_amount": "",
            "relative_invoice_count": "1",
            "invoice_detail": _build_invoice_summary([inv]),
            "remark": "unmatched",
            "_matched_invoices": [inv],
        }
        records.append(record)

    return records


def _invoices_to_records(invoices: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """无刷卡记录时，将每张发票转为独立记录"""
    records = []
    for inv in invoices:
        record = {
            "card_date": "",
            "card_no": "",
            "card_amount": "",
            "relative_invoice_count": "1",
            "invoice_detail": _build_invoice_summary([inv]),
            "remark": "",
            "_matched_invoices": [inv],
        }
        records.append(record)
    return records
