"""单据元信息识别器：从 OCR 文本中提取供应商、单据日期、应付金额。

识别逻辑：
- 供应商：取 OCR 顶部前 20 行，关键词匹配（供货单/供应商/供货方/批发/酒行/商行等）
- 日期：全文正则匹配，标准化为 YYYYMMDD
- 总金额：关键词优先（应付金额/实付金额/金额合计），兜底取总计行最大金额
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field, asdict
from typing import List, Optional, Tuple


# 供应商关键词（命中后取该行清理结果）
SUPPLIER_KEYWORDS = (
    "供货单", "供应商", "供货方", "批发", "酒行", "商行",
    "经销", "专卖店", "配送单", "送货单", "采购单", "订单",
    "销售单", "出库单", "经营部", "商贸", "有限公司", "发证机构",
)

# 供应商名称中需要剔除的冗余噪声词
SUPPLIER_NOISE_WORDS = (
    "标题", "采购单", "销售单", "入库单", "出库单", "送货单", 
    "单据", "订单", "配送单", "清单", "供货单", "预览", "详情",
    "打印", "副本", "记账", "联", "存根",
)

# 总金额关键词（命中后取该行最近一个金额数字）
AMOUNT_KEYWORDS = (
    "应付金额", "实付金额", "金额合计", "应付合计", "合计金额",
    "合计", "总计", "总计金额",
)

# 日期关键词（用于辅助定位日期）
DATE_KEYWORDS = (
    "单据日期", "下单时间", "日期", "时间", "开单日期", "制单日期", "打印时间", "业务日期", "下单日期",
)

# 日期正则：4 种格式
DATE_PATTERNS = [
    # 2026年07月17日 / 2026年7月17日
    re.compile(r'(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日?'),
    # 2026-07-17 / 2026/07/17 / 2026.07.17
    re.compile(r'(\d{4})[-./](\d{1,2})[-./](\d{1,2})'),
    # 20260717 紧邻 8 位数字
    re.compile(r'(?<!\d)(\d{4})(\d{2})(\d{2})(?!\d)'),
]

# 金额数字正则（保留 2 位小数或整数）
AMOUNT_PATTERN = re.compile(r'(\d{1,7}(?:,\d{3})*(?:\.\d+)?)')


@dataclass
class OrderMetadata:
    supplier: str = ''
    bill_date: str = ''           # YYYYMMDD
    total_amount: float = 0.0
    raw_supplier_text: str = ''

    def is_complete(self) -> bool:
        return bool(self.supplier and self.bill_date)

    def to_dict(self) -> dict:
        return asdict(self)


# 排除关键词：包含这些词的行绝对不是供应商抬头
EXCLUDE_KEYWORDS = (
    "购货单位", "客户名称", "收货地址", "联系电话", "经手人", 
    "地址", "电话", "传真", "邮编", "网址", "开户行", "账号", 
    "税号", "业务员", "联系人", "单据编号", "流水号", "页码",
    "四川省", "成都市", "武侯区", "双流区", "高新区", "金牛区",
    "成华区", "锦江区", "龙泉驿区", "青羊区", "新都区", "温江区",
    "街道", "社区", "楼", "号", "室", "路", "巷", "段",
)

class OrderMetadataExtractor:
    """单据元信息识别器。"""

    # 标题区域行数（采购单抬头 + 日期 + 供应商一般在前 20 行）
    HEADER_LINE_LIMIT = 20

    # 供应商名最大长度
    MAX_SUPPLIER_LEN = 50

    def extract(self, ocr_text: str, ocr_rows: Optional[List[List[str]]] = None, general_text: Optional[str] = None) -> OrderMetadata:
        """从 OCR 原始文本和/或解析后的二维数组提取三字段。

        Args:
            ocr_text: OCR 原始字符串全文（从表格 OCR 提取）
            ocr_rows: 解析后的二维数组（可选）
            general_text: 从通用票据识别接口 (/v1/general_ocr) 提取的文本，优先级最高

        Returns:
            OrderMetadata
        """
        # 优先使用通用票据识别的文本进行供应商和日期提取
        supplier_source = general_text if general_text else ocr_text
        date_source = general_text if general_text else ocr_text
        
        # 金额通常在表格内，优先使用 ocr_text
        amount_source = ocr_text or general_text or ''

        supplier, raw_supplier = self._extract_supplier(supplier_source, ocr_rows)
        bill_date = self._extract_bill_date(date_source)
        total_amount = self._extract_total_amount(amount_source, ocr_rows)

        # 如果通用票据识别没抓到供应商，尝试用表格 OCR 的文本补位
        if not supplier and general_text and ocr_text:
            supplier, raw_supplier = self._extract_supplier(ocr_text, ocr_rows)
            
        # 如果通用票据识别没抓到日期，尝试用表格 OCR 的文本补位
        if not bill_date and general_text and ocr_text:
            bill_date = self._extract_bill_date(ocr_text)

        return OrderMetadata(
            supplier=supplier,
            bill_date=bill_date,
            total_amount=total_amount,
            raw_supplier_text=raw_supplier,
        )

    # ───────────────────────────────────────────────
    # Supplier
    # ───────────────────────────────────────────────

    def _extract_supplier(self, text: str, rows: Optional[List[List[str]]]) -> Tuple[str, str]:
        """提取供应商名称。返回 (cleaned_name, raw_text)。"""
        # 1) 关键词匹配：取顶部 N 行
        header_lines = self._get_header_lines(text, self.HEADER_LINE_LIMIT)
        for line in header_lines:
            cleaned = self._clean_supplier_line(line)
            if not cleaned:
                continue
            
            # 严格排除：购货单位、地址、电话等干扰项
            if any(k in cleaned for k in EXCLUDE_KEYWORDS):
                continue

            for kw in SUPPLIER_KEYWORDS:
                if kw in cleaned:
                    final_name = self._final_cleanup_supplier(cleaned)
                    return self._truncate(final_name), cleaned

        # 2) 兜底：取顶部第一个"含中文且无数字行号"且长度 ≥ 4 的非空行
        # 但排除"纯日期行"（避免把日期当供应商）
        for line in header_lines:
            cleaned = self._clean_supplier_line(line)
            if not cleaned:
                continue
            
            # 兜底也要严格排除地址和电话行
            if any(k in cleaned for k in EXCLUDE_KEYWORDS):
                continue

            if not (re.search(r'[\u4e00-\u9fa5]', cleaned) and len(cleaned) >= 4):
                continue
            # 排除：纯日期（YYYY-MM-DD / YYYY/MM/DD / YYYY年MM月DD日 / YYYYMMDD）
            date_only = re.match(r'^\s*[\d年月日/\-\.]+\s*$', cleaned)
            if date_only:
                continue
            # 排除：以"日期"开头的行（属于日期标注）
            if re.match(r'^\s*日期[:：]?', cleaned):
                continue
            # 排除：以"单据"开头的行
            if re.match(r'^\s*单据[:：]?', cleaned):
                continue
            
            final_name = self._final_cleanup_supplier(cleaned)
            if final_name:
                return self._truncate(final_name), cleaned

        return '', ''

    @staticmethod
    def _final_cleanup_supplier(s: str) -> str:
        """最后的供应商名称深度清理：去除“标题”、“采购单”等噪声。"""
        if not s:
            return ''
        
        # 1. 统一处理全角和常见符号
        s = s.replace('：', ':').replace('（', '(').replace('）', ')')
        
        # 2. 去除“标题:”或“名称:”这类前缀
        s = re.sub(r'^(标题|名称|供应商|供货方|单位|商户)[:\s]*', '', s)
        
        # 3. 循环去除噪声词
        noise_sorted = sorted(SUPPLIER_NOISE_WORDS, key=len, reverse=True)
        
        changed = True
        while changed:
            original = s
            for noise in noise_sorted:
                # 简单替换所有匹配的噪声词
                s = s.replace(noise, '')
            
            # 去除括号及其中间的噪声词（如 (采购单) ）
            s = re.sub(r'\(\s*\)', '', s)
            
            # 去除首尾残留标点和空白
            s = re.sub(r'^[:：\s\-_\|\.\(\)]+', '', s)
            s = re.sub(r'[:：\s\-_\|\.\(\)]+$', '', s)
            
            changed = (s != original)
            
        return s.strip()

    @staticmethod
    def _clean_supplier_line(line: str) -> str:
        """清理一行文本：去前后空白、去首尾日期/编号/电话/标点。"""
        s = line.strip()
        if not s:
            return ''
        
        # 统一全角冒号
        s = s.replace('：', ':')
        
        # 如果是“标题: 新双利采购单”，保留冒号后面的部分进行初步处理
        if ':' in s:
            parts = s.split(':', 1)
            # 如果冒号前面是“标题”、“名称”等词，则取后面
            if any(k in parts[0] for k in ("标题", "名称", "供应商", "商户")):
                s = parts[1].strip()

        # 去行首日期/编号前缀
        s = re.sub(r'^[\s\d\-\.\/年月日:：]+', '', s)
        # 去行尾标点
        s = re.sub(r'[\s\.,;:：\-_/\\|]+$', '', s)
        return s.strip()

    def _truncate(self, s: str) -> str:
        return s[:self.MAX_SUPPLIER_LEN] if len(s) > self.MAX_SUPPLIER_LEN else s

    # ───────────────────────────────────────────────
    # BillDate
    # ───────────────────────────────────────────────

    def _extract_bill_date(self, text: str) -> str:
        """提取单据日期，标准化为 YYYYMMDD。"""
        if not text:
            return ''
            
        lines = text.splitlines()
        
        # 1. 优先尝试关键词定位逻辑（如用户建议：搜索“时间”或“日期”）
        for i, line in enumerate(lines):
            line_clean = line.strip().replace('：', ':')
            
            # 检查是否包含日期相关关键词
            for kw in DATE_KEYWORDS:
                if kw in line_clean:
                    # a) 尝试在当前行找日期
                    for pat in DATE_PATTERNS:
                        m = pat.search(line_clean)
                        if m:
                            y, mo, d = m.group(1), m.group(2), m.group(3)
                            if self._is_valid_date(y, mo, d):
                                return f"{y}{int(mo):02d}{int(d):02d}"
                    
                    # b) 如果当前行没找到，尝试在下一行找（针对表格 OCR 错位情况）
                    if i + 1 < len(lines):
                        next_line = lines[i+1].strip()
                        for pat in DATE_PATTERNS:
                            m = pat.search(next_line)
                            if m:
                                y, mo, d = m.group(1), m.group(2), m.group(3)
                                if self._is_valid_date(y, mo, d):
                                    return f"{y}{int(mo):02d}{int(d):02d}"
        
        # 2. 兜底策略：全文正则匹配
        for pat in DATE_PATTERNS:
            # 优先找包含 202 开头的年份（更像当前日期）
            matches = pat.finditer(text)
            for m in matches:
                y, mo, d = m.group(1), m.group(2), m.group(3)
                if self._is_valid_date(y, mo, d):
                    # 如果年份以 202 开头，优先返回
                    if y.startswith('202'):
                        return f"{y}{int(mo):02d}{int(d):02d}"
                    # 否则记录下来作为候选
                    last_valid = f"{y}{int(mo):02d}{int(d):02d}"
            
        # 如果没有 202 开头的，返回最后一个有效的
        try:
            return last_valid
        except NameError:
            return ''

    @staticmethod
    def _is_valid_date(y: str, m: str, d: str) -> bool:
        try:
            yi, mi, di = int(y), int(m), int(d)
        except (ValueError, TypeError):
            return False
        if not (1900 <= yi <= 2100):
            return False
        if not (1 <= mi <= 12):
            return False
        if not (1 <= di <= 31):
            return False
        return True

    # ───────────────────────────────────────────────
    # TotalAmount
    # ───────────────────────────────────────────────

    def _extract_total_amount(self, text: str, rows: Optional[List[List[str]]]) -> float:
        """提取总金额（应付/实付/合计/总计）。"""
        # 1) 关键词行：取该行最大数字
        for kw in AMOUNT_KEYWORDS:
            for line in text.splitlines():
                if kw in line:
                    nums = AMOUNT_PATTERN.findall(line)
                    if nums:
                        # 取最大的（应付通常在末尾）
                        try:
                            vals = [float(n.replace(',', '')) for n in nums]
                            return max(vals)
                        except ValueError:
                            continue

        # 2) 兜底：取全文最大金额数字（>= 1.0 且含小数点，像金额）
        all_nums = AMOUNT_PATTERN.findall(text)
        candidates = []
        for n in all_nums:
            try:
                v = float(n.replace(',', ''))
                if v >= 1.0 and '.' in n:
                    candidates.append(v)
            except ValueError:
                continue
        return max(candidates) if candidates else 0.0

    # ───────────────────────────────────────────────
    # Helpers
    # ───────────────────────────────────────────────

    @staticmethod
    def _get_header_lines(text: str, n: int) -> List[str]:
        lines = [l for l in text.splitlines() if l.strip()]
        return lines[:n]


def sanitize_for_filename(s: str) -> str:
    """清理字符串为合法文件名片段：替换 Windows 非法字符为下划线。"""
    if not s:
        return ''
    # Windows 非法字符
    s = re.sub(r'[\\/:*?"<>|]', '_', s)
    # 多余空白
    s = re.sub(r'\s+', ' ', s).strip()
    return s