# spacy_training/scripts/predict.py
import re
import cn2an
import os.path
from tqdm import tqdm
from pathlib import Path


# 单位与连接符定义
MAX_ITEM_OFFSET = 10
UNIT_PATTERN = "个|张|片|块|条|幅|根"
SIZE_UNITS_PATTERN = r'mm|cm|dm|m'
SIZE_JOIN_PATTERN = "xX×"
NUMS_PATTERN = r'\d+(?:\.\d+)'  # 修复：原缺少 ? 导致整数不匹配
CNT_PATTERN = rf'{NUMS_PATTERN}?[^克+\s\d-]*'
EXCEPT_TEXT = "解析失败"
INDEX_COL = '序号'

base_colume = ['解析备注', '尺寸备注', '描述', '异常信息']
mapping_size = {
        "width_mm": "长",
        "height_mm": "宽",
        # "original_size": "原始尺寸",
        # "total_quantity": "总数量",
        "style_count": "款数",
        "quantity_per_style": "数量",
        "unit": "单位",
        "描述": "描述",
        "解析备注": "解析备注",
        "size_unit": "尺寸单位",
        "exception_msg": '异常信息'
    }

import spacy
import pandas as pd

# 将 doc 转换为字典
def doc_to_dict(doc):
    return {
        "text": doc.text,
        "entities": [
            {
                "text": ent.text,
                "label": ent.label_,
                "start": ent.start_char,
                "end": ent.end_char
            }
            for ent in doc.ents
        ],
    }

def predict_file(df, model_path="./spacy_training/model", key='备注'):
    print(model_path)
    nlp = spacy.load(model_path)
    if "sentencizer" not in nlp.pipe_names:
        nlp.add_pipe("sentencizer")

    # 统一表头空格
    df.columns = df.columns.str.strip()
    key = key.strip()

    for i, row in tqdm(df.iterrows(), total=len(df)):
        order = str(row[key]).strip()
        base_row = {key: val for key, val in row.items()}  # 复制原行数据
        yield order, base_row, doc_to_dict(nlp(order))
        # doc = nlp(text)
        # print(f"\n🔤 文本: {text}")
        # import json; print(json.dumps(doc_to_dict(doc), indent=4, ensure_ascii=False))
        
        # break

from dataclasses import dataclass

@dataclass
class StructuredOrder:
    key: str
    width_mm: float
    height_mm: float
    original_size: str
    style_count: int
    quantity_per_style: float
    unit: str
    total_quantity: float
    original_qty: str
    size_position: list
    qty_position: list
    exception_msg: str
    calc_type: str
    size_unit: str
    meta: dict

def expand_to_structured(raw_pairs, size_unit=''):
    unit_map = {
        '百': 100, '千': 1000, '万': 10000,
        '百万': 1000000, '千万': 10000000, '亿': 100000000,
    }

    UNIT_REGEX = re.compile(rf'({UNIT_PATTERN})$')
    exception_msg = []

    def extract_multiplier(text):
        """
        从字符串中提取数值乘数，支持：
        - 阿拉伯数字 + 中文单位：3.5万 → 35000
        - 纯中文数字 + 单位：三万五千 → 35000
        - 纯中文：两千万 → 20000000
        """
        # 去除末尾单位（保留前面的部分）
        clean = UNIT_REGEX.sub('', text.strip())

        # 如果去单位后为空，原字符串可能是纯单位（如“万”），默认系数为1
        if not clean:
            matched_unit = UNIT_REGEX.search(text)
            if matched_unit:
                unit = matched_unit.group(1)
                return unit_map.get(unit, 1.0)
            return 1.0

        # 尝试直接转阿拉伯数字
        try:
            return float(clean)
        except ValueError:
            pass

        # 尝试转中文数字（如“三万五千”虽然不合理，但“三万”可以）
        try:
            # 注意：cn2an 可以直接处理“三万五千”这种
            num = cn2an.cn2an(clean, "smart")  # smart 模式支持混合写法
            if isinstance(num, (int, float)):
                # 检查原字符串是否有单位后缀（比如“三万”中的“万”已被去除，需补回）
                matched_unit = UNIT_REGEX.search(text)
                if matched_unit:
                    unit = matched_unit.group(1)
                    num *= unit_map[unit]
                return float(num)
        except Exception:
            exception_msg.append(f"尺寸解析异常：{text}")
            pass

        return 1.0

    def parse_size(size_str):
        join_str = ''.join(re.findall(f"[{SIZE_JOIN_PATTERN}]+", size_str))
        if len(join_str) == 0 or re.search('比例|等高|等比|等宽', size_str):
            result = f'异常值：{size_str}'
            return result, result, size_unit
        size_str = size_str.replace(join_str, "x")
        match = re.match(rf'({NUMS_PATTERN}?)([a-z]+)?[{SIZE_JOIN_PATTERN}]({NUMS_PATTERN}?)([a-z]+)?', size_str, re.IGNORECASE)
        if not match:
            return None, None, size_unit
        w_val, w_unit, h_val, h_unit = match.groups()
        unit = (w_unit or h_unit or size_unit).lower()
        conv = {'m': 1000, 'dm': 100, 'cm': 10, 'mm': 1}
        if unit not in conv:
            print(f'未知单位: {unit} size_str:{size_str} size_unit:{size_unit}')
            exception_msg.append(f"尺寸解析异常：{size_str}, 未知单位: {unit}")
            return None, None, unit
        w_mm = conv[unit] * float(w_val)
        h_mm = conv[unit] * float(h_val)
        return round(w_mm, 3), round(h_mm, 3), unit

    def parse_quantity(qty_str):
        qty_str = "".join(qty_str.split())
        multi_match = re.match((
            rf'[共计]?(.+?)款\w*?([共计]?[\d.一二两三四五六七八九十百千万亿]+)({UNIT_PATTERN})'
        ), qty_str, re.IGNORECASE)
        if multi_match:
            style_part, qty_part = multi_match.group(1), multi_match.group(2)
            if qty_part[0] in '共计':
                qty_part = qty_part[1:]
                calc_type = 'total'
            else:
                calc_type = 'single'
            style_count = int(extract_multiplier(style_part))
            quantity_per_style = int(extract_multiplier(qty_part))
            unit = multi_match.group(3)

            return {"style_count": style_count, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
        
        single_match = re.match(rf'(.+?)({UNIT_PATTERN})', qty_str, re.IGNORECASE)
        if single_match:
            qty_part = single_match.group(1)
            if qty_part[0] in '共计':
                qty_part = qty_part[1:]
                calc_type = 'total'
            else:
                calc_type = 'single'
            quantity_per_style = int(extract_multiplier(qty_part))
            unit = single_match.group(2)
            return {"style_count": 1, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}

        return {"style_count": 0, "quantity_per_style": 0, "unit": "个"}

    structured = []

    for size_info, qty_info in raw_pairs:
        s_start, s_end, s_text, *_ = size_info
        q_start, q_end, q_text, *_ = qty_info

        if s_text == EXCEPT_TEXT:
            width_mm, height_mm = EXCEPT_TEXT, EXCEPT_TEXT
        else:
            size_parsed = parse_size(s_text)
            if size_parsed:
                width_mm, height_mm, size_unit = size_parsed
            else:
                width_mm, height_mm, size_unit = None, None, None
        
        if q_text == EXCEPT_TEXT:
            style_count, quantity_per_style, unit = 0, EXCEPT_TEXT, EXCEPT_TEXT
        else:
            qty_parsed = parse_quantity(q_text)
            style_count, quantity_per_style, unit = qty_parsed["style_count"], qty_parsed["quantity_per_style"], qty_parsed["unit"]

        structured.append(StructuredOrder(
            key=f"{s_start}{s_end}{s_text}",
            width_mm=width_mm,
            height_mm=height_mm,
            original_size=s_text,
            style_count=style_count,
            quantity_per_style=quantity_per_style,
            unit=unit,
            total_quantity=style_count * quantity_per_style if style_count and style_count else None,
            original_qty=q_text,
            size_position=[s_start, s_end],
            qty_position=[q_start, q_end],
            exception_msg="; ".join(exception_msg),
            calc_type=qty_parsed.get('calc_type', None),
            size_unit=size_unit,
            meta={
                "size_text_except": s_text if s_text == EXCEPT_TEXT or (not width_mm and not height_mm) else None,
                "qty_text_except": q_text if q_text == EXCEPT_TEXT else None,
            }
        ))

    return structured


def decode(doc_dict_list, is_horizontal=False):
    ent_group = {
        # "单号": ["单号"],
        "刮刮膜尺寸": ["刮刮膜尺寸"],
        # "产品": ["产品"],
        # "材质": ["材质"],
        # "工艺": ["工艺"],
        # "用户": ["用户信息"],
        # "加急": ["加急"],
        "数量": ["尺寸", "数量"],
    }
    columns = list(ent_group.keys())
    columns.remove('数量')
    columns.extend(base_colume)
    group_reflect = {v: k for k, vs in ent_group.items() for v in vs}
    result_items = []
    exception_list = []
    
    for doc_dict in doc_dict_list:
        sentence = doc_dict["text"]
        entities = doc_dict["entities"]

        is_except = False

        # 强关联实体需要进行组合处理
        # ======================================== 实体分组 ========================================
        ent_group_dict = {k: [] for k in ent_group.keys()}
        base_ent_group_dict = {}

        for ent in entities:
            start, end, label, text = ent["start"], ent["end"], ent["label"], ent["text"]
            group_name = group_reflect[label]
            ent_group_dict[group_name].append((start, end, text, label))
            base_ent_group_dict[group_name] = text
    
        result = []

        # 解析出尺寸和数量的组合
        # ======================================== 解析尺寸和数量 ========================================
        group_list = []
        size_and_qty_list = [[], []]

        # 数量需要和尺寸进行组合处理
        base_ent_group_dict.pop("数量", None)
        for cnt_item in ent_group_dict["数量"]:
            size_info, qty_info = size_and_qty_list
            label = cnt_item[-1]
            if label == '数量':
                qty_info.append(cnt_item)
            elif label == '尺寸':
                start, end, text, _label = cnt_item
                if f'-({text[:2]}' in sentence:
                    continue
                if qty_info:
                    group_list.append(size_and_qty_list)
                    size_and_qty_list = [[], []]
                    size_info, qty_info = size_and_qty_list
                size_info.append(cnt_item)
        
        # 存在数量和尺寸
        if size_and_qty_list[0] or size_and_qty_list[1]:
            group_list.append(size_and_qty_list)
        
        # 如果只有一对尺寸，有可能反过来描述
        if len(group_list) == 2:
            _1, qty_info = group_list[0]
            size_info, _2 = group_list[1]

            if not _1 and not _2:
                group_list[:] = [[qty_info, size_info]]
        
        # print(f'订单中尺寸和数量组合：{group_list}')

        # 解析出尺寸和数量的组合
        # ======================================== 解析长、宽、款数、数量 ========================================
        size_unit = ''
        for size_info, qty_info in group_list:
            units = set(''.join(re.findall(SIZE_UNITS_PATTERN, ent[2])) for ent in size_info)
            units = [unit for unit in units if unit]
            if len(units) == 1:
                size_unit = units[0]

        size_and_qty_parsed_result = []
        for size_info, qty_info in group_list:
            size_len, qty_len = len(size_info), len(qty_info)
            if size_len == 0:
                print(f'解析异常，尺寸为空，数量为{qty_info}，原文：{sentence}', entities)
                is_except = True
                # 起始位置、结束位置、尺寸文本、尺寸标签
                qty_info = [(0, 0, EXCEPT_TEXT, '数量')]
            if qty_len == 0:
                print(f'解析异常，数量为空，尺寸为{size_info}，原文：{sentence}', entities)
                is_except = True
                # 起始位置、结束位置、尺寸文本、尺寸标签
                size_info = [(0, 0, EXCEPT_TEXT, '尺寸')]
                # continue

            # print('size_info, qty_info', size_info, qty_info)
            if size_len == qty_len:
                decode_desc = "单尺寸、单款式描述的订单" if size_len == 1 else "多尺寸、多款式描述一一匹配的订单"
                
                for size, qty in zip(size_info, qty_info):
                    expand = expand_to_structured([(size, qty)], size_unit) or []
                    exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
                    if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
                    for item in expand:
                        parsed_item = item.__dict__.copy()
                        parsed_item['描述'] = f"{size[2]}|{qty[2]}"
                        parsed_item['解析备注'] = decode_desc
                        parsed_item['size_len'] = size_len
                        parsed_item['qty_len'] = qty_len
                        size_and_qty_parsed_result.append(parsed_item)
            elif size_len>1 and qty_len>1:
                
                if size_len > qty_len:
                    total_qty = []
                    for size, qty in zip(size_info, qty_info):
                        expand = expand_to_structured([(size, qty)], size_unit) or []
                        for item in expand:
                            total_qty.append(item.style_count)

                    if size_len == sum(total_qty):
                        decode_desc = "多尺寸、多款式描述:多款式描述和尺寸相等的订单"
                        item = []
                        for idx, qty in enumerate(total_qty):
                            item.extend(qty_info[idx] for _ in range(qty))
                        print('解析：', size_info, qty_info, item)
                        # breakpoint()
                        qty_info = item
                        qty_len = len(qty_info)
                    else:
                        # qty_info = []
                        decode_desc = f"匹配异常：多尺寸、多款式描述的订单: {size_info} {qty_info}"
                else:
                    # qty_info = []
                    decode_desc = f"匹配异常：多尺寸、多款式描述的订单: {size_info} {qty_info}"

                for size, qty in zip(size_info, qty_info):
                    expand = expand_to_structured([(size, qty)], size_unit) or []
                    exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
                    if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
                    for item in expand:
                        parsed_item = item.__dict__.copy()
                        parsed_item['style_count'] = 1
                        parsed_item['描述'] = f"{size[2]}|{qty[2]}"
                        parsed_item['解析备注'] = decode_desc
                        parsed_item['size_len'] = size_len
                        parsed_item['qty_len'] = qty_len
                        size_and_qty_parsed_result.append(parsed_item)
            else:
                # 多尺寸-单数量、单尺寸-多数量 告警多尺寸-多数量
                iter_item = ((size, qty) for size in size_info for qty in qty_info)
                if size_len > 1 and qty_len == 1:
                    decode_desc = "多尺寸、单款式描述的订单"
                elif size_len == 1 and qty_len > 1:
                    decode_desc = "单尺寸、多款式描述的订单"
                else:
                    decode_desc = f"异常：多尺寸、多款式描述的订单: {size_info} {qty_info}"

                for item in iter_item:
                    size, qty = item
                    expand = expand_to_structured([(size, qty)], size_unit) or []
                    exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
                    if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
                    for item in expand:
                        parsed_item = item.__dict__.copy()
                        parsed_item['描述'] = f"{size[2]}|{qty[2]}"
                        parsed_item['解析备注'] = decode_desc
                        parsed_item['size_len'] = size_len
                        parsed_item['qty_len'] = qty_len
                        size_and_qty_parsed_result.append(parsed_item)

        result.extend(size_and_qty_parsed_result)
    
        for item in result:
            # print('item', item)
            new_item = base_ent_group_dict.copy()
            for key in mapping_size:
                col_name = mapping_size[key]
                new_item[col_name] = item[key]
                columns.append(col_name)
            size_len = item.pop('size_len', None)
            qty_len = item.pop('qty_len', None)
            calc_type = item.pop('calc_type', None)

            if size_len > 1 and calc_type == 'total':
                new_item[f'数量'] = f"警告：异常值(总计值:{new_item[f'数量']})"

            if size_len == 1 or size_len == qty_len:
                pass
                # new_item[f'解析备注'] = '单尺寸的订单'
            else:
                if size_len == new_item['款数']:
                    new_item[f'款数'] = 1
                    # new_item[f'解析备注'] = '尺寸数量和款数相同的订单'
                elif new_item['款数'] == 1:
                    new_item[f'款数'] = 1
                    # new_item[f'解析备注'] = '单款的订单'
                else:
                    new_item[f'款数'] = "异常值：款数和尺寸数不一致"
                    new_item[f'数量'] = "异常值：款数和尺寸数不一致"
                    # new_item[f'解析备注'] = '多尺寸并且和款数不同的订单'
                    
            result_items.append(new_item)
            # print(json.dumps(new_item, indent=4, ensure_ascii=False))

        if not result:
            if base_ent_group_dict:
                base_ent_group_dict['是否异常'] = is_except
                base_ent_group_dict[f'解析备注'] = '未解析到实际尺寸的订单1'
                result_items.append(base_ent_group_dict.copy())
            else:
                result_items.append({
                    "是否异常": is_except,
                    "解析备注": "未解析到任何实体"
                })

    return exception_list, columns, result_items


def parse_finance_data(file_path, target_index, is_horizontal, sheet_name="Sheet1"):
    
    df = pd.read_excel(file_path, dtype=str, sheet_name=sheet_name)
    total_amount = len(df)

    items = predict_file(df, './app/resources/models/订单尺寸识别/best_model', target_index)
    # items = predict_file(file_path, './resources/models/lintao/best_model', target_index)
    exception_list, results, outputs_columns = [], [], []
    origin_item = None

    parsed_dict = {}
    mapping_size_values = mapping_size.values()

    for text, origin_item, item in items:

        decode_result = []

        base_info = {}
        result_index = parsed_dict.get(f"{text}_index")
        if result_index:
            exceptions, columns = [], []
            try:
                decode_item = parsed_dict[text][result_index]
                for key in origin_item:
                    if key in mapping_size_values:
                        continue
                    decode_item[key] = origin_item[key]
                    decode_item['尺寸备注'] = '多组尺寸解析'
                parsed_dict[f"{text}_index"] += 1
                continue
            except IndexError:
                print(f'解析到的尺寸不足：{text}，{result_index}， {parsed_dict[text]}')
                base_item = parsed_dict[text][0]
                for col in base_colume:
                    base_info[col] = base_item.get(col, '')
                base_info["长"] = '异常值：没有解析到那么多尺寸'
                
                decode_items = []
                parsed_dict[f"{text}_index"] += 1
        else:
            exceptions, columns, decode_items = decode([item], is_horizontal) or []
        
        # if '(250731202428388557)' in text:
        #     print(result_index, text, exceptions, columns, decode_items)
        #     breakpoint()

        if len(columns) > len(outputs_columns):
            outputs_columns = columns

        if len(decode_items) > 1:
            for item in decode_items:
                item['尺寸备注'] = '多组尺寸解析'
        
        for idx, decode_item in enumerate(decode_items):
            base_row = origin_item.copy()
            if idx and INDEX_COL in base_row: base_row[INDEX_COL] = ''
            # base_row = origin_item.copy() if idx == 0 else {}
            base_row.update(decode_item)
            is_except = base_row.pop('是否异常', False)
            if is_except:
                for col in base_colume:
                    origin_item[col] = base_row.get(col, '')
                decode_result.append(origin_item)
                exception_list.append(base_row)
            else:
                decode_result.append(base_row)
        if not decode_items:
            if base_info:
                origin_item.update(base_info)

            decode_result.append(origin_item)
        else:
            if is_horizontal:
                new_decode_result = decode_result[0].copy()
                for idx, item in enumerate(decode_result[1:], 1):
                    for key in mapping_size_values:
                        new_key = f"{key}{idx}"
                        new_decode_result[new_key] = item[key]
                        if new_key not in outputs_columns:
                            outputs_columns.append(new_key)
                decode_result = [new_decode_result]

            if text not in parsed_dict:
                parsed_dict[text] = decode_result
                parsed_dict[f"{text}_index"] = 0
            parsed_dict[f"{text}_index"] += 1
        
        # if decode_result: 
        #     results.append(decode_result[0])
        if len(decode_items) > 1:
            for item in decode_result[1:]:
                item['尺寸备注'] = '复制新增：多组尺寸解析'
        results.extend(decode_result)

        # break
    
    print('横向解析', outputs_columns)
    if origin_item:
        # outputs_columns.
        _outputs_columns = list(origin_item.keys()) + outputs_columns
        col_set = set()
        outputs_columns = []

        for col in _outputs_columns:
            if col not in col_set:
                col_set.add(col)
                outputs_columns.append(col)
    
    df = pd.DataFrame(results, columns=outputs_columns)

    file_name = os.path.basename(file_path).replace(".xlsx", '')
    filename = file_name + '_解析' + ('_横向排列' if is_horizontal else '_纵向排列')

    upload_dir = Path("outputs")
    upload_dir.mkdir(parents=True, exist_ok=True)
    save_file_path = upload_dir / f'{filename}_1.xlsx'

    # 使用 ExcelWriter 同时写入多个 sheet
    with pd.ExcelWriter(save_file_path, engine='openpyxl') as writer:
        df.to_excel(writer, sheet_name='正常解析', index=False)
        if exception_list:
            except_df = pd.DataFrame(exception_list, columns=outputs_columns)
            except_df.to_excel(writer, sheet_name='异常解析', index=False)

    print(f'解析结果保存在：{save_file_path}')
    return save_file_path, total_amount


if __name__ == "__main__":

    # print(expand_to_structured([[(0,1,'600x50cm','r'), (0,1,'3款各1张','e')]]))
    # exit()
    # predict("Apple is opening a new office in Tokyo.")
    # predict("Google hired Sarah Connor from Berlin last year.")
    file_info = ['d:/会计组/新领图8月.xlsx', '备注']
    # file_info = ['d:/会计组/即客2025年8月尺寸整理.xlsx', '系统文件名']
    file_info = ['d:/会计组/8月订单明细9.6.xlsx', '文件名']
    file_info = ['d:/会计组/ZHX-8月订单明细.xlsx', '文件名']
    file_info = ['d:/会计组/国税数据源/智韬2025年8月尺寸整理(1).xlsx', '系统文件名']
    file_info = ['d:/会计组/彩印通8月数码9.23.xlsx', 'ERP系统文件名']
    file_info = ['d:/会计组/CYT8月明细9.24.xlsx', 'ERP系统文件名']
    file_info = ['d:/会计组/9.1-9.26.xlsx', '备注']
    file_info = ['d:/会计组/JD.xlsx', '文件名']
    file_info = ['d:/会计组/泰州即客2025年9月尺寸整理.xlsx', '系统文件名']
    file_info = ['d:/会计组/ZHX需拆明细9月.xlsx', '文件名', False]
    file_info = ['d:/会计组/七彩2024年9月账单尺寸整理.xlsx', '系统文件名', True]
    file_info = ['d:/会计组/艾印图文2024年9月账单尺寸整理.xlsx', '系统文件名', True]
    file_info = ['d:/会计组/9月转印.xlsx', 'erp', True]
    file_info = ['d:/会计组/9月CYT.xlsx', '文件名', False]
    file_info = ['d:/会计组/智韬2025年9月尺寸整理.xlsx', '系统文件名', True]
    file_info = ['d:/会计组/9月UV转印贴.xlsx', '文件名', True]
    file_info = ['d:/会计组/彩印通2025年9月（数码）.xlsx', '文件名', True]
    file_info = ['d:/会计组/9月名片.xlsx', '文件名', True]
    file_info = ['d:/会计组/9月不干胶.xlsx', '文件名', True]
    file_path, target_index, is_horizontal = file_info
    parse_finance_data(file_path, target_index, is_horizontal)

    