"""
Excel处理核心模块
--------------
提供Excel文件处理功能，包括表格解析、数据提取和处理。
"""

import os
import re
import pandas as pd
import numpy as np
import xlrd
import xlwt
from xlutils.copy import copy as xlcopy
from typing import Dict, List, Optional, Tuple, Union, Any, Callable
from datetime import datetime

from app.config.settings import ConfigManager
from app.core.utils.log_utils import get_logger
from app.core.utils.file_utils import (
    ensure_dir,
    get_file_extension,
    get_latest_file,
    load_json,
    save_json
)
from app.core.utils.string_utils import (
    clean_string,
    extract_number,
    format_barcode,
    parse_monetary_string
)
from app.core.excel.converter import UnitConverter
from app.core.handlers.column_mapper import ColumnMapper

logger = get_logger(__name__)

class ExcelProcessor:
    """
    Excel处理器：处理OCR识别后的Excel文件，
    提取条码、单价和数量，并按照采购单模板的格式填充
    """
    
    def __init__(self, config, product_db=None, missing_barcodes_cb: Optional[Callable[[List[str]], None]] = None):
        """
        初始化Excel处理器

        Args:
            config: 配置信息
            product_db: 商品数据库实例（可选，由外部传入以共享）
            missing_barcodes_cb: 缺失条码的回调函数，接收条码列表
        """
        self.config = config
        self.missing_barcodes_cb = missing_barcodes_cb
        self.current_missing_barcodes = []  # 记录当前文件处理中缺失的条码
        self.current_file_path = ""        # 记录当前处理的文件路径
        
        # 修复ConfigParser对象没有get_path方法的问题
        try:
            # 获取输入和输出目录
            self.output_dir = config.get_path('Paths', 'output_folder', fallback='data/output', create=True) if hasattr(config, 'get_path') else os.path.abspath('data/output')
            self.temp_dir = config.get('Paths', 'temp_folder', fallback='data/temp')
            
            # 获取模板文件路径
            self.template_path = config.get('Paths', 'template_file', fallback='templates/银豹-采购单模板.xls')
            if not os.path.exists(self.template_path):
                logger.warning(f"模板文件不存在: {self.template_path}")
            
            # 设置缓存文件路径
            self.cache_file = os.path.join(self.output_dir, "excel_process_records.json")
            self.processed_files = self._load_processed_files()
            
            # 确保目录存在
            os.makedirs(self.output_dir, exist_ok=True)
            os.makedirs(self.temp_dir, exist_ok=True)
            
            # 记录实际路径
            logger.info(f"使用输出目录: {os.path.abspath(self.output_dir)}")
            logger.info(f"使用临时目录: {os.path.abspath(self.temp_dir)}")
            
            # 加载单位转换器和配置
            self.unit_converter = UnitConverter()

            # 商品记忆库
            if product_db is not None:
                self.product_db = product_db
            else:
                from app.core.db.product_db import ProductDatabase
                db_path = config.get_path('Paths', 'product_db', fallback='data/product_cache.db') if hasattr(config, 'get_path') else 'data/product_cache.db'
                tpl_folder = config.get('Paths', 'template_folder', fallback='templates')
                item_data = config.get('Templates', 'item_data', fallback='商品资料.xlsx')
                tpl_path = os.path.join(tpl_folder, item_data)
                self.product_db = ProductDatabase(db_path, tpl_path)

            logger.info(f"初始化ExcelProcessor完成，模板文件: {self.template_path}")
        except Exception as e:
            logger.error(f"初始化ExcelProcessor失败: {e}")
            raise
    
    def _load_processed_files(self) -> Dict[str, str]:
        """
        加载已处理文件的缓存
        
        Returns:
            处理记录字典
        """
        return load_json(self.cache_file, {})
        
    def _save_processed_files(self) -> None:
        """保存已处理文件的缓存"""
        save_json(self.processed_files, self.cache_file)
    
    def get_latest_excel(self) -> Optional[str]:
        """
        获取output目录下最新的Excel文件（排除采购单文件）
        
        Returns:
            最新Excel文件的路径，如果未找到则返回None
        """
        logger.info(f"搜索目录 {self.output_dir} 中的Excel文件")
        
        # 使用文件工具获取最新文件
        latest_file = get_latest_file(
            self.output_dir,
            pattern="",  # 不限制文件名
            extensions=['.xlsx', '.xls']  # 限制为Excel文件
        )
        
        # 如果没有找到文件
        if not latest_file:
            logger.warning(f"未在 {self.output_dir} 目录下找到未处理的Excel文件")
            return None
        
        # 检查是否是采购单（以"采购单_"开头的文件）
        file_name = os.path.basename(latest_file)
        if file_name.startswith('采购单_'):
            logger.warning(f"找到的最新文件是采购单，不作处理: {latest_file}")
            return None
        
        logger.info(f"找到最新的Excel文件: {latest_file}")
        return latest_file
    
    def extract_barcode(self, df: pd.DataFrame) -> List[str]:
        """
        从数据帧中提取条码列名
        
        Args:
            df: 数据帧
            
        Returns:
            可能的条码列名列表
        """
        possible_barcode_columns = ColumnMapper.STANDARD_COLUMNS['barcode']
        
        found_columns = []
        
        # 检查精确匹配
        for col in df.columns:
            col_str = str(col).strip()
            if col_str in possible_barcode_columns:
                found_columns.append(col)
                logger.info(f"找到精确匹配的条码列: {col_str}")
        
        # 如果找不到精确匹配，尝试部分匹配
        if not found_columns:
            for col in df.columns:
                col_str = str(col).strip().lower()
                for keyword in ['条码', '条形码', 'barcode', '编码']:
                    if keyword.lower() in col_str:
                        found_columns.append(col)
                        logger.info(f"找到部分匹配的条码列: {col} (包含关键词: {keyword})")
                        break
        
        # 如果仍然找不到，尝试使用数据特征识别
        if not found_columns and len(df) > 0:
            for col in df.columns:
                # 检查此列数据是否符合条码特征
                sample_values = df[col].dropna().astype(str).tolist()[:10]  # 取前10个非空值
                
                if sample_values and all(len(val) >= 8 and len(val) <= 14 for val in sample_values):
                    # 大多数条码长度在8-14之间
                    if all(val.isdigit() for val in sample_values):
                        found_columns.append(col)
                        logger.info(f"基于数据特征识别的可能条码列: {col}")
        
        return found_columns
    
    def extract_product_info(self, df: pd.DataFrame) -> List[Dict]:
        """
        从数据帧中提取商品信息
        
        Args:
            df: 数据帧
            
        Returns:
            商品信息列表
        """
        products = []
        
        # 检测列映射
        column_mapping = self._detect_column_mapping(df)
        logger.info(f"检测到列映射: {column_mapping}")
        
        # 处理每一行
        for idx, row in df.iterrows():
            try:
                # 初始化商品信息
                product = {
                    'barcode': '',     # 条码
                    'name': '',        # 商品名称
                    'specification': '',  # 规格
                    'quantity': 0,     # 数量
                    'unit': '',        # 单位
                    'price': 0,        # 单价
                    'amount': 0,       # 金额
                    'is_gift': False   # 是否为赠品
                }
                
                # 提取条码
                if '条码' in df.columns and not pd.isna(row['条码']):
                    product['barcode'] = str(row['条码']).strip()
                elif column_mapping.get('barcode') and not pd.isna(row[column_mapping['barcode']]):
                    product['barcode'] = str(row[column_mapping['barcode']]).strip()
                
                # 跳过空条码行
                if not product['barcode']:
                    continue

                # 检查条码是否存在于数据库（商品资料）
                bc = product['barcode']
                mem = self.product_db.get_memory(bc)
                if not mem or mem.get('confidence', 0) < 50:
                    # 如果不存在，或者置信度低（说明不是来自商品资料模板），记录为缺失
                    if bc not in self.current_missing_barcodes:
                        self.current_missing_barcodes.append(bc)
                        # 记录到数据库
                        self.product_db.record_missing_barcode(
                            bc, 
                            product.get('name', ''), 
                            self.current_file_path
                        )
                        logger.warning(f"条码缺失: {bc} ({product.get('name', '')})")
                
                # 检查备注列，过滤换货、退货、作废等非采购行
                skip_row = False
                for col in df.columns:
                    col_str = str(col)
                    if any(k in col_str for k in ['备注', '说明', '类型', '备注1']):
                        val = str(row[col]).strip()
                        # 过滤常见的非采购关键字
                        if any(k in val for k in ['换货', '退货', '作废', '减钱', '冲减', '赠品单', '补货']):
                            logger.info(f"过滤非采购行: {product['barcode']} - {product.get('name', '')}, 原因: {col_str}包含 '{val}'")
                            skip_row = True
                            break
                if skip_row:
                    continue
                    
                # 提取商品名称
                if '商品名称' in df.columns and not pd.isna(row['商品名称']):
                    product['name'] = str(row['商品名称']).strip()
                elif '名称' in df.columns and not pd.isna(row['名称']):
                    product['name'] = str(row['名称']).strip()
                elif column_mapping.get('name') and not pd.isna(row[column_mapping['name']]):
                    product['name'] = str(row[column_mapping['name']]).strip()
                    
                # 提取单位
                if '单位' in df.columns and not pd.isna(row['单位']):
                    product['unit'] = str(row['单位']).strip()
                elif column_mapping.get('unit') and not pd.isna(row[column_mapping['unit']]):
                    product['unit'] = str(row[column_mapping['unit']]).strip()
                
                # 提取单价
                if '单价' in df.columns and not pd.isna(row['单价']):
                    product['price'] = row['单价']
                elif column_mapping.get('price') and not pd.isna(row[column_mapping['price']]):
                    product['price'] = row[column_mapping['price']]
                
                # 提取金额
                if '金额' in df.columns and not pd.isna(row['金额']):
                    product['amount'] = row['金额']
                elif '小计' in df.columns and not pd.isna(row['小计']):
                    product['amount'] = row['小计']
                elif column_mapping.get('amount') and not pd.isna(row[column_mapping['amount']]):
                    product['amount'] = row[column_mapping['amount']]
                # 根据金额判断赠品：金额为0、为空、或为o/O
                amt = product.get('amount', None)
                try:
                    is_amt_gift = False
                    if amt is None:
                        is_amt_gift = True
                    elif isinstance(amt, str):
                        parsed = parse_monetary_string(amt)
                        is_amt_gift = (parsed is None or parsed == 0.0)
                    else:
                        parsed = parse_monetary_string(amt)
                        is_amt_gift = (parsed is not None and parsed == 0.0)
                    if is_amt_gift:
                        product['is_gift'] = True
                except Exception:
                    pass
                
                # 提取数量
                if '数量' in df.columns and not pd.isna(row['数量']):
                    product['quantity'] = row['数量']
                elif column_mapping.get('quantity') and not pd.isna(row[column_mapping['quantity']]):
                    product['quantity'] = row[column_mapping['quantity']]
                
                # 处理可能的复合数量字段，例如"2箱"、"3件"
                if isinstance(product['quantity'], str) and product['quantity']:
                    num, unit = self.unit_converter.extract_unit_from_quantity(product['quantity'])
                    if unit:
                        product['unit'] = unit
                        if num is not None:
                            product['quantity'] = num
                
                # 提取规格并解析包装数量
                if '规格' in df.columns and not pd.isna(row['规格']):
                    product['specification'] = str(row['规格'])
                    # 修正OCR误识别的4.51*4为4.5L*4
                    product['specification'] = re.sub(r'(\d+\.\d+)1\*(\d+)', r'\1L*\2', product['specification'])
                    package_quantity = self.parse_specification(product['specification'])
                    if package_quantity:
                        product['package_quantity'] = package_quantity
                        logger.info(f"解析规格: {product['specification']} -> 包装数量={package_quantity}")
                elif column_mapping.get('specification') and not pd.isna(row[column_mapping['specification']]):
                    product['specification'] = str(row[column_mapping['specification']])
                    # 修正OCR误识别的4.51*4为4.5L*4
                    product['specification'] = re.sub(r'(\d+\.\d+)1\*(\d+)', r'\1L*\2', product['specification'])
                    package_quantity = self.parse_specification(product['specification'])
                    if package_quantity:
                        product['package_quantity'] = package_quantity
                        logger.info(f"从映射列解析规格: {product['specification']} -> 包装数量={package_quantity}")
                else:
                    # 只有在无法从Excel获取规格时，才尝试从商品名称推断规格
                    if product['name']:
                        # 特殊处理：优先检查名称中是否包含"容量*数量"格式
                        container_pattern = r'.*?(\d+(?:\.\d+)?)\s*(?:ml|[mM][lL]|[lL]|升|毫升)[*×xX](\d+).*'
                        match = re.search(container_pattern, product['name'])
                        if match:
                            # 容量单位*数量格式，如"1.8L*8瓶"，取数量部分作为包装数量
                            volume = match.group(1)
                            count = match.group(2)
                            inferred_spec = f"{volume}L*{count}"
                            inferred_qty = int(count)
                            product['specification'] = inferred_spec
                            product['package_quantity'] = inferred_qty
                            logger.info(f"从商品名称提取容量*数量格式: {product['name']} -> {inferred_spec}, 包装数量={inferred_qty}")
                        # 原来的重量/容量*数字格式处理逻辑
                        else:
                            weight_volume_pattern = r'.*?\d+(?:g|ml|毫升|克)[*xX×](\d+)'
                            match = re.search(weight_volume_pattern, product['name'])
                            if match:
                                inferred_spec = f"1*{match.group(1)}"
                                inferred_qty = int(match.group(1))
                                product['specification'] = inferred_spec
                                product['package_quantity'] = inferred_qty
                                logger.info(f"从商品名称提取重量/容量规格: {product['name']} -> {inferred_spec}, 包装数量={inferred_qty}")
                            else:
                                # 一般情况的规格推断
                                inferred_spec = self.unit_converter.infer_specification_from_name(product['name'])
                                if inferred_spec:
                                    product['specification'] = inferred_spec
                                    package_quantity = self.parse_specification(inferred_spec)
                                    if package_quantity:
                                        product['package_quantity'] = package_quantity
                                    logger.info(f"从商品名称推断规格: {product['name']} -> {inferred_spec}, 包装数量={package_quantity}")
                
                # 检查已设置的规格但未设置包装数量的情况
                if product.get('specification') and not product.get('package_quantity'):
                    package_quantity = self.parse_specification(product['specification'])
                    if package_quantity:
                        product['package_quantity'] = package_quantity
                        logger.info(f"解析已设置的规格: {product['specification']} -> 包装数量={package_quantity}")
                
                # 如果数量为0但单价和金额都存在，先算数量（必须在 unit 推断之前，否则 unit 推断条件因 quantity=0 被跳过）
                if (product['quantity'] == 0 or product['quantity'] is None) and product['price'] and product['amount']:
                    try:
                        amount = parse_monetary_string(product['amount'])
                        if amount is not None and amount > 0:
                            quantity = amount / product['price']
                            logger.info(f"数量为空或为0，通过金额({amount})和单价({product['price']})计算得出数量: {quantity}")
                            product['quantity'] = quantity
                    except Exception as e:
                        logger.warning(f"通过金额和单价计算数量失败: {e}")

                # 推断单位为"件"：仅依赖规格文本（spec 已经是 "1*N"），不再要求 quantity 非零
                if not product['unit'] and product.get('specification'):
                    volume_pattern = r'(\d+(?:\.\d+)?)\s*(?:ml|[mL]L|l|L|升|毫升)[*×xX](\d+)'
                    simple_pattern = r'(\d+)[*×xX](\d+)'
                    if re.search(volume_pattern, product['specification']) or re.search(simple_pattern, product['specification']):
                        product['unit'] = '件'
                        logger.info(f"根据规格推断单位: {product['specification']} -> 单位=件")

                # 应用单位转换规则
                product = self.unit_converter.process_unit_conversion(product)
                
                # 应用记忆库补全
                product = self._apply_memory(product)

                products.append(product)
            except Exception as e:
                logger.error(f"提取第{idx+1}行商品信息时出错: {e}", exc_info=True)
                continue

        logger.info(f"提取到 {len(products)} 个商品信息")
        return products

    def _apply_memory(self, product: Dict) -> Dict:
        """查记忆库，补全 OCR 缺失/错误的字段"""
        barcode = product.get('barcode', '')
        if not barcode:
            return product

        try:
            memory = self.product_db.get_memory(barcode)
        except Exception:
            return product

        if memory is None or memory.get('confidence', 0) < 80:
            return product

        # 补全规格
        ocr_spec = product.get('specification', '')
        mem_spec = memory.get('specification', '') or ''
        if mem_spec and (not ocr_spec or self._is_spec_suspicious(ocr_spec)):
            product['specification'] = mem_spec
            logger.info(f"记忆修正规格: {barcode} '{ocr_spec}' -> '{mem_spec}'")

        # 补全名称
        ocr_name = product.get('name', '')
        mem_name = memory.get('name', '') or ''
        if mem_name and not ocr_name:
            product['name'] = mem_name
            logger.info(f"记忆修正名称: {barcode} -> '{mem_name}'")

        # 补全单位
        ocr_unit = product.get('unit', '')
        mem_unit = memory.get('unit', '') or ''
        if mem_unit and not ocr_unit:
            product['unit'] = mem_unit
            logger.info(f"记忆修正单位: {barcode} -> '{mem_unit}'")

        # 不改数量和单价（每单不同）
        return product

    def _is_spec_suspicious(self, spec: str) -> bool:
        """检测规格是否像 OCR 垃圾"""
        if not spec:
            return True
        # IL*12（I 和 1 混淆）
        if re.search(r'^[Ii][Ll*]', spec):
            return True
        # 4.51*4（L 被识别为 1）
        if re.search(r'\d+\.\d+1\*\d+', spec):
            return True
        # 包含非常规字符（排除常见规格字符）
        if re.search(r'[^\d.*xX\-LlKkGgMm升毫瓶桶盒箱件提\s]', spec):
            return True
        return False

    def fill_template(self, products: List[Dict], output_file_path: str) -> bool:
        """
        填充采购单模板
        
        Args:
            products: 商品信息列表
            output_file_path: 输出文件路径
            
        Returns:
            是否成功填充
        """
        try:
            # 打开模板文件
            template_workbook = xlrd.open_workbook(self.template_path, formatting_info=True)
            template_sheet = template_workbook.sheet_by_index(0)
            
            # 创建可写的副本
            output_workbook = xlcopy(template_workbook)
            output_sheet = output_workbook.get_sheet(0)
            
            # 先对产品按条码分组，区分正常商品和赠品
            barcode_groups = {}
            
            # 遍历所有产品，按条码分组
            logger.info(f"开始处理{len(products)} 个产品信息")
            for product in products:
                barcode = product.get('barcode', '')
                # 确保条码是整数字符串
                barcode = format_barcode(barcode)
                
                if not barcode:
                    logger.warning(f"跳过无条码商品")
                    continue
                
                # 获取数量和单价
                quantity = product.get('quantity', 0)
                price = product.get('price', 0)
                amount = product.get('amount', 0)
                
                # 如果数量为0但单价和金额都存在，计算数量 = 金额/单价
                if (quantity == 0 or quantity is None) and price > 0 and amount:
                    try:
                        amount = parse_monetary_string(amount)
                        if amount is not None and amount > 0:
                            quantity = amount / price
                            logger.info(f"数量为空或为0，通过金额({amount})和单价({price})计算得出数量: {quantity}")
                            product['quantity'] = quantity
                    except Exception as e:
                        logger.warning(f"通过金额和单价计算数量失败: {e}")
                
                # 判断是否为赠品（价格为0）
                is_gift = bool(product.get('is_gift', False)) or (price == 0)
                
                logger.info(f"处理商品: 条码={barcode}, 数量={quantity}, 单价={price}, 是否赠品={is_gift}")
                
                if barcode not in barcode_groups:
                    barcode_groups[barcode] = {
                        'normal': None,  # 正常商品信息
                        'gift_quantity': 0  # 赠品数量
                    }
                
                if is_gift:
                    # 是赠品，累加赠品数量
                    barcode_groups[barcode]['gift_quantity'] += quantity
                    logger.info(f"发现赠品：条码{barcode}, 数量={quantity}")
                else:
                    # 是正常商品
                    if barcode_groups[barcode]['normal'] is None:
                        barcode_groups[barcode]['normal'] = {
                            'product': product,
                            'quantity': quantity,
                            'price': price
                        }
                        logger.info(f"发现正常商品：条码{barcode}, 数量={quantity}, 单价={price}")
                    else:
                        # 如果有多个正常商品记录，累加数量
                        barcode_groups[barcode]['normal']['quantity'] += quantity
                        logger.info(f"累加正常商品数量：条码{barcode}, 新增={quantity}, 累计={barcode_groups[barcode]['normal']['quantity']}")
                        
                        # 如果单价不同，取平均值
                        if price != barcode_groups[barcode]['normal']['price']:
                            avg_price = (barcode_groups[barcode]['normal']['price'] + price) / 2
                            barcode_groups[barcode]['normal']['price'] = avg_price
                            logger.info(f"调整单价(取平均值)：条码{barcode}, 原价={barcode_groups[barcode]['normal']['price']}, 新价={price}, 平均={avg_price}")
            
            # 输出调试信息
            logger.info(f"分组后共{len(barcode_groups)} 个不同条码的商品")
            for barcode, group in barcode_groups.items():
                if group['normal'] is not None:
                    logger.info(f"条码 {barcode} 处理结果：正常商品数量{group['normal']['quantity']}，单价{group['normal']['price']}，赠品数量{group['gift_quantity']}")
                else:
                    logger.info(f"条码 {barcode} 处理结果：只有赠品，数量={group['gift_quantity']}")
            
            # 准备填充数据
            row_index = 1  # 从第2行开始填充（索引从0开始）
            
            for barcode, group in barcode_groups.items():
                # 1. 列B(1): 条码（必填）
                output_sheet.write(row_index, 1, barcode)
                
                if group['normal'] is not None:
                    # 有正常商品
                    product = group['normal']['product']
                    
                    # 2. 列C(2): 采购量（必填） 使用正常商品的采购量
                    normal_quantity = group['normal']['quantity']
                    output_sheet.write(row_index, 2, normal_quantity)
                    
                    # 3. 列D(3): 赠送量 - 添加赠品数量
                    if group['gift_quantity'] > 0:
                        output_sheet.write(row_index, 3, group['gift_quantity'])
                        logger.info(f"条码 {barcode} 填充：采购量={normal_quantity}，赠品数量{group['gift_quantity']}")
                    
                    # 4. 列E(4): 采购单价（必填）
                    purchase_price = group['normal']['price']
                    style = xlwt.XFStyle()
                    style.num_format_str = '0.0000'
                    output_sheet.write(row_index, 4, round(purchase_price, 4), style)
                else:
                    # 只有赠品，没有正常商品
                    # 采购量填0，赠送量填赠品数量
                    output_sheet.write(row_index, 2, 0)  # 采购量为0
                    output_sheet.write(row_index, 3, group['gift_quantity'])  # 赠送量
                    output_sheet.write(row_index, 4, 0)  # 单价为0
                    
                    logger.info(f"条码 {barcode} 填充：仅有赠品，采购量=0，赠品数量={group['gift_quantity']}")
                
                # 移到下一行
                row_index += 1
            
            # 保存文件
            output_workbook.save(output_file_path)
            logger.info(f"采购单已保存到: {output_file_path}")
            return True
            
        except Exception as e:
            logger.error(f"填充模板时出错: {e}")
            return False
    
    def _find_header_row(self, df: pd.DataFrame) -> Optional[int]:
        """自动识别表头行，委托给 ColumnMapper.detect_header_row"""
        result = ColumnMapper.detect_header_row(df, max_rows=30)
        if result >= 0:
            logger.info(f"找到表头行: 第{result+1}行")
            return result
        # 回退：找第一个非空行
        for row in range(len(df)):
            if df.iloc[row].notna().sum() > 3:
                logger.info(f"未找到明确表头，使用第一个有效行: 第{row+1}行")
                return row
        logger.warning("无法识别表头行")
        return None
    
    def process_specific_file(self, file_path: str, progress_cb: Optional[Callable[[int], None]] = None) -> Optional[str]:
        """
        处理指定的Excel文件
        
        Args:
            file_path: Excel文件路径
            
        Returns:
            输出文件路径，如果处理失败则返回None
        """
        logger.info(f"开始处理Excel文件: {file_path}")
        
        if not os.path.exists(file_path):
            logger.error(f"文件不存在: {file_path}")
            return None

        self.current_missing_barcodes = []  # 重置缺失列表
        self.current_file_path = file_path # 设置当前处理文件路径
        
        try:
            # 读取Excel文件时不立即指定表头
            if progress_cb:
                try:
                    progress_cb(92)
                except Exception:
                    pass
            df = pd.read_excel(file_path, header=None)
            logger.info(f"成功读取Excel文件: {file_path}, 共 {len(df)} 行")
            
            # 自动识别表头行
            header_row = self._find_header_row(df)
            if header_row is None:
                logger.error("无法识别表头行")
                return None
                
            logger.info(f"识别到表头在第 {header_row+1} 行")
            
            # 重新设置表头，避免二次读取
            if progress_cb:
                try:
                    progress_cb(94)
                except Exception:
                    pass
            
            # 使用识别到的表头行设置列名，并过滤掉表头之前的行
            df.columns = df.iloc[header_row]
            df = df.iloc[header_row + 1:].reset_index(drop=True)
            
            logger.info(f"重新整理数据结构，共 {len(df)} 行有效数据")
            
            # 提取商品信息
            if progress_cb:
                try:
                    progress_cb(96)
                except Exception:
                    pass
            products = self.extract_product_info(df)
            
            if not products:
                logger.warning("未提取到有效商品信息")
                return None
            
            # 生成输出文件名，保存到result目录
            file_name = os.path.splitext(os.path.basename(file_path))[0]
            result_dir = self.config.get_path('Paths', 'result_folder', fallback='data/result', create=True) if hasattr(self.config, 'get_path') else os.path.abspath('data/result')
            os.makedirs(result_dir, exist_ok=True)
            output_file = os.path.join(result_dir, f"采购单_{file_name}.xls")
            
            # 填充模板并保存
            if self.fill_template(products, output_file):
                # 从处理结果中学习商品记忆
                try:
                    self.product_db.learn_from_products(products, source='ocr')
                    self.product_db._export_memory_json()
                    logger.info(f"已从处理结果学习 {len(products)} 条商品记忆")
                except Exception as e:
                    logger.warning(f"学习商品记忆失败: {e}")

                # 记录已处理文件
                self.processed_files[file_path] = output_file
                self._save_processed_files()
                
                # 不再自动打开输出目录
                logger.info(f"采购单已保存到: {output_file}")

                # 处理完成，如果有缺失条码，触发回调
                if self.current_missing_barcodes and self.missing_barcodes_cb:
                    try:
                        self.missing_barcodes_cb(self.current_missing_barcodes)
                    except Exception as e:
                        logger.error(f"触发缺失条码回调失败: {e}")

                if progress_cb:
                    try:
                        progress_cb(100)
                    except Exception:
                        pass
                
                return output_file
            
            return None
            
        except Exception as e:
            logger.error(f"处理Excel文件时出错: {file_path}, 错误: {e}")
            return None
    
    def process_latest_file(self, progress_cb: Optional[Callable[[int], None]] = None) -> Optional[str]:
        """
        处理最新的Excel文件
        
        Returns:
            输出文件路径，如果处理失败则返回None
        """
        # 获取最新的Excel文件
        latest_file = self.get_latest_excel()
        if not latest_file:
            logger.warning("未找到可处理的Excel文件")
            return None
        
        # 处理文件
        return self.process_specific_file(latest_file, progress_cb=progress_cb)
    
    def _detect_column_mapping(self, df: pd.DataFrame) -> Dict[str, str]:
        """
        自动检测列名映射
        
        Args:
            df: 数据框
            
        Returns:
            列名映射字典，键为标准列名，值为实际列名
        """
        # 提取有用的列
        barcode_cols = self.extract_barcode(df)

        # 如果没有找到条码列，无法继续处理
        if not barcode_cols:
            logger.error("未找到条码列，无法处理")
            return {}

        # 使用 ColumnMapper 统一查找列名
        mapped_columns = {'barcode': barcode_cols[0]}
        logger.info(f"使用条码列: {mapped_columns['barcode']}")

        # 内部键名 -> 标准列名映射 (processor.py 使用 price/amount 作为内部键名)
        field_map = [
            ('name', 'name'),
            ('specification', 'specification'),
            ('quantity', 'quantity'),
            ('unit', 'unit'),
            ('price', 'unit_price'),
            ('amount', 'total_price'),
        ]

        for internal_key, standard_name in field_map:
            matched = ColumnMapper.find_column(list(df.columns), standard_name)
            if matched:
                mapped_columns[internal_key] = matched
                logger.info(f"找到{internal_key}列: {matched}")

        return mapped_columns
    
    def infer_specification_from_name(self, product_name: str) -> Tuple[Optional[str], Optional[int]]:
        """
        从商品名称推断规格
        根据特定的命名规则匹配规格信息
        
        Args:
            product_name: 商品名称
            
        Returns:
            规格字符串和包装数量的元组
        """
        if not product_name or not isinstance(product_name, str):
            logger.warning(f"无效的商品名: {product_name}")
            return None, None
            
        product_name = product_name.strip()
        
        # 特殊处理：重量/容量*数字格式
        weight_volume_pattern = r'.*?\d+(?:g|ml|毫升|克)[*xX×](\d+)'
        match = re.search(weight_volume_pattern, product_name)
        if match:
            inferred_spec = f"1*{match.group(1)}"
            inferred_qty = int(match.group(1))
            logger.info(f"从商品名称提取重量/容量规格: {product_name} -> {inferred_spec}, 包装数量={inferred_qty}")
            return inferred_spec, inferred_qty
        
        # 使用单位转换器推断规格
        inferred_spec = self.unit_converter.infer_specification_from_name(product_name)
        if inferred_spec:
            # 解析规格中的包装数量
            package_quantity = self.parse_specification(inferred_spec)
            if package_quantity:
                logger.info(f"从商品名称推断规格: {product_name} -> {inferred_spec}, 包装数量={package_quantity}")
                return inferred_spec, package_quantity
        
        # 特定商品规则匹配
        spec_rules = [
            # XX入白膜格式，如"550纯净水24入白膜"
            (r'.*?(\d+)入白膜', lambda m: (f"1*{m.group(1)}", int(m.group(1)))),
            
            # 白膜格式，如"550水24白膜"
            (r'.*?(\d+)白膜', lambda m: (f"1*{m.group(1)}", int(m.group(1)))),
            
            # 445水溶C系列
            (r'445水溶C.*?(\d+)[入个]纸箱', lambda m: (f"1*{m.group(1)}", int(m.group(1)))),
            
            # 东方树叶系列
            (r'东方树叶.*?(\d+\*\d+).*纸箱', lambda m: (m.group(1), int(m.group(1).split('*')[1]))),
            
            # 桶装
            (r'(\d+\.?\d*L)桶装', lambda m: (f"{m.group(1)}*1", 1)),
            
            # 树叶茶系
            (r'树叶.*?(\d+)[入个]纸箱', lambda m: (f"1*{m.group(1)}", int(m.group(1)))),
            
            # 茶π系列
            (r'茶[πΠπ].*?(\d+)纸箱', lambda m: (f"1*{m.group(1)}", int(m.group(1)))),
            
            # 通用入数匹配
            (r'.*?(\d+)[入个](?:纸箱|箱装|白膜)', lambda m: (f"1*{m.group(1)}", int(m.group(1)))),
            
            # 通用数字+纸箱格式
            (r'.*?(\d+)纸箱', lambda m: (f"1*{m.group(1)}", int(m.group(1))))
        ]
        
        # 尝试所有规则
        for pattern, formatter in spec_rules:
            match = re.search(pattern, product_name)
            if match:
                spec, qty = formatter(match)
                logger.info(f"根据特定规则推断规格: {product_name} -> {spec}, 包装数量={qty}")
                return spec, qty
        
        # 尝试直接从名称中提取数字*数字格式
        match = re.search(r'(\d+\*\d+)', product_name)
        if match:
            spec = match.group(1)
            package_quantity = self.parse_specification(spec)
            if package_quantity:
                logger.info(f"从名称中直接提取规格: {spec}, 包装数量={package_quantity}")
                return spec, package_quantity
        
        # 最后尝试提取任何位置的数字，默认典型件装数
        numbers = re.findall(r'\d+', product_name)
        if numbers:
            for num in numbers:
                # 检查是否为典型的件装数(12/15/24/30)
                if num in ['12', '15', '24', '30']:
                    spec = f"1*{num}"
                    logger.info(f"从名称中提取可能的件装数: {spec}, 包装数量={int(num)}")
                    return spec, int(num)
            
        logger.warning(f"无法从商品名'{product_name}' 推断规格")
        return None, None 
    
    def parse_specification(self, spec_str: str) -> Optional[int]:
        """
        解析规格字符串，提取包装数量
        支持格式：1*15, 1x15, 1*5*10, 5kg*6, IL*12等
        
        Args:
            spec_str: 规格字符串
            
        Returns:
            包装数量，如果无法解析则返回None
        """
        if not spec_str or not isinstance(spec_str, str):
            return None
        
        try:
            # 清理规格字符串
            spec_str = clean_string(spec_str)
            
            # 处理可能的OCR误识别，如"IL"应为"1L"，"6oo"应为"600"
            spec_str = re.sub(r'(\b|^)[iIlL](\d+)', r'1\2', spec_str)  # 将"IL"替换为"1L"
            spec_str = re.sub(r'(\d+)[oO0]{2,}', lambda m: m.group(1) + '00', spec_str)  # 将"6oo"替换为"600"
            spec_str = spec_str.replace('×', '*').replace('x', '*').replace('X', '*')  # 统一乘号
            
            logger.debug(f"清理后的规格字符串: {spec_str}")
            
            # 新增：匹配“1件=12桶/袋/盒…”等等式规格，取右侧数量作为包装数量
            eq_match = re.search(r'(\d+(?:\.\d+)?)\s*(?:件|箱|提|盒)\s*[=＝]\s*(\d+)\s*(?:瓶|桶|盒|支|个|袋|罐|包|卷)', spec_str)
            if eq_match:
                return int(eq_match.group(2))

            # 匹配带单位的格式，如"5kg*6"、"450g*15"、"450ml*15"
            weight_pattern = r'(\d+(?:\.\d+)?)\s*(?:kg|KG|千克|公斤)[*×](\d+)'
            match = re.search(weight_pattern, spec_str)
            if match:
                return int(match.group(2))
            
            # 匹配克、毫升等单位格式
            match = re.search(r'\d+(?:\.\d+)?(?:g|G|ml|ML|mL|毫升|克)[*×](\d+)', spec_str)
            if match:
                return int(match.group(1))
            
            # 匹配1*5*10 格式的三级规格
            match = re.search(r'(\d+(?:\.\d+)?)[*×](\d+(?:\.\d+)?)[*×](\d+(?:\.\d+)?)', spec_str)
            if match:
                # 取最后一个数字作为袋数量
                return int(float(match.group(3)))
            
            # 匹配1*15, 1x15 格式
            match = re.search(r'(\d+(?:\.\d+)?)[*×](\d+(?:\.\d+)?)', spec_str)
            if match:
                # 取第二个数字作为包装数量
                return int(float(match.group(2)))
                
            # 匹配24瓶/件等格式
            match = re.search(r'(\d+(?:\.\d+)?)[瓶个支袋][/／](件|箱)', spec_str)
            if match:
                return int(float(match.group(1)))
                
            # 匹配4L格式
            match = re.search(r'(\d+(?:\.\d+)?)\s*[Ll升][*×]?(\d+(?:\.\d+)?)?', spec_str)
            if match:
                # 如果有第二个数字，返回它；否则返回1
                return int(float(match.group(2))) if match.group(2) else 1
            
            # 匹配单独的数字+单位格式，如"12瓶装"
            match = re.search(r'(\d+(?:\.\d+)?)[瓶个支袋包盒罐箱](?:装|\/箱)?', spec_str)
            if match:
                return int(float(match.group(1)))
            
            # 尝试直接匹配任何数字
            numbers = re.findall(r'\d+(?:\.\d+)?', spec_str)
            if numbers and len(numbers) > 0:
                # 如果只有一个数字，通常是包装数量
                if len(numbers) == 1:
                    return int(float(numbers[0]))
                
                # 如果有多个数字，尝试识别可能的包装数量（典型数值如6/12/24/30）
                for num in numbers:
                    if float(num) in [6.0, 12.0, 24.0, 30.0]:
                        return int(float(num))
                
                # 如果没有典型数值，选择最后一个数字（通常是包装数量）
                return int(float(numbers[-1]))
                
        except Exception as e:
            logger.warning(f"解析规格'{spec_str}'时出错: {e}")
            
        return None
