# -*- coding: utf-8 -*-
"""纪念币解析器 (board 10) - 复用 parser 项目的核心解析逻辑"""

from __future__ import annotations

import re

from .base import PriceRecord


# ============================================================
# 0. 噪声过滤辅助
# ============================================================
# 论坛模板/联系方式/银行行名等噪声名称（出现即丢弃记录）
NOISE_NAMES = {
    '要', '楼主', '查看', '评分', '亮照亮证', '认证员注', '认证员', '营销员',
    '营销员第', '业余爱好者', '业余爱好者第', '认证会员', '高级认证员',
    '电话', '手机', '电联', '电联确认', '微信', 'QQ', 'qq',
    '农行', '交行', '工行', '工行商友', '工行商友卡', '中国银行', '招行',
    '中行', '华夏', '华夏银行', '浦发', '浦发银行', '民生', '民生银行',
    '姓名', '招商', '邮政', '支付宝', '淘宝', '来自', '所在地', '地址', '交易等级',
    '加微信', '也可加微信', '也可以加微信',
    # 噪声短语
    '先款', '现款', '高价', '低价', '面值', '面额', '以上', '代友', '出售',
    '包邮', '卖家包邮', '亮证链接', '亮证', '高价先款', '先款回收', '先款收',
    '先款求购', '现款回收', '以上各', '以上全部', '以上收好', '以上暂定',
    '以上每个', '以上需', '如题', '代友求', '出售香港', '出售香港回归',
    '卷起步', '单价', '起步', '价格',
}

# 价格上限（纪念币单价不会超过 10 万）
PRICE_MAX = 100000.0

# 纯包装/单位词（作为 name 出现时无意义，多为 extract_name 误提取）
PACKAGE_WORDS = {
    '原', '原卷', '原盒', '原包', '原桶', '原封', '原箱',
    '盒', '卷', '散', '散新', '散币', '散卡', '卡', '卡册', '件',
    '枚', '套', '对', '个', '箱', '元', '单价', '起步', '价格',
    '先款', '现款', '高价', '低价', '面值', '面额',
}

# 品相描述词（从 name 中剥离，不作为过滤条件）
# 注意：只用词组，不用单字（"好"/"原"/"裂"会误伤"爱好者"/"原创"等）
QUALITY_WORDS = [
    '美品', '好品', '原封', '原盒', '原包', '原桶', '原箱', '原卷', '原件',
    '全品', '上品', '差品', '裂卷', '氧化', '生锈', '流通好品',
    '卷拆品', '卷拆', '好卷', '裂卷', '远光', '流通品', '带光',
    '原合', '一合',
]


def _looks_like_price(s: str) -> bool:
    """判断字符串是否像真实价格（排除电话号/银行卡号/科学计数法）。"""
    if not s:
        return False
    s = s.strip()
    if 'e' in s.lower():
        return False
    if not re.fullmatch(r'\d+\.?\d*', s):
        return False
    # 纯数字长度 >=8 视为电话号/银行卡号
    digits = s.replace('.', '')
    if len(digits) >= 8:
        return False
    try:
        v = float(s)
    except ValueError:
        return False
    return 0 < v <= PRICE_MAX


def _is_valid_price_value(s: str) -> bool:
    """对外接口：判断价格字段值是否有效。"""
    return _looks_like_price(s)


def _strip_name(name: str) -> str:
    """去除名称末尾的冒号/空白，便于噪声匹配。"""
    return (name or '').rstrip(':： 　').strip()


def _clean_name(name: str) -> str:
    """清理品种名称：去品相词、标点、多余空白，保留纯名称。"""
    if not name:
        return ''
    n = name
    # 去除品相词（先长后短，避免"原封"被"原"提前吃掉）
    for q in QUALITY_WORDS:
        n = n.replace(q, ' ')
    # 去除"纪念币"后缀（如"一轮龙 纪念币"→"一轮龙"）
    n = n.replace('纪念币', ' ')
    # 去除全角不间断空格和普通空白
    n = n.replace('\xa0', ' ')
    n = re.sub(r'\s+', ' ', n).strip()
    # 去除首尾标点
    n = n.strip(':：，,。.；;、· \xa0')
    # 去除尾部残留单字品相词（如"一兔 原"→"一兔"、"一轮兔好"→"一轮兔"，循环处理）
    changed = True
    while changed:
        changed = False
        for q in ('原', '好', '裂'):
            if n.endswith(' ' + q) or n.endswith('\xa0' + q):
                n = n[:-2].rstrip()
                changed = True
            elif n.endswith(q) and len(n) > 1:
                n = n[:-1].rstrip()
                changed = True
            elif n == q:
                return ''
    # 去除内部多余空格
    n = re.sub(r'\s+', ' ', n).strip()
    return n


def _is_noise_name(name: str) -> bool:
    """名称是否为已知噪声词。"""
    core = _strip_name(name)
    if not core:
        return True
    if core in NOISE_NAMES:
        return True
    # 纯数字名称（如 "1", "2", "1600"）
    if re.fullmatch(r'\d+\.?\d*', core):
        return True
    # 以非汉字开头（标点/数字/符号，如 "(在中介存有"、"----原"、"--原"）
    if re.match(r'^[^\u4e00-\u9fff]', core):
        return True
    # 纯包装/单位词（如 "原"、"原卷"、"单价"、"起步"）
    if core in PACKAGE_WORDS:
        return True
    # 单字名称（纪念币品种名通常 >=2 字，单字多为噪声如"一""要"）
    if len(core) == 1:
        return True
    # 长句噪声（>12字符且含说明性词汇）
    if len(core) > 12 and re.search(r'(包邮|所售|三包|偏远地区|一切货品|视 频|拆包验货|电联确认|录像|评分|闲鱼|极兔|百世|当天有效|第三方|异地发货|长期收|邮寄地址|签名档|本人拒收|默认顺丰|全程录像|货到付款|跟帖确认|发货请|劳务费|市场忙|每个品种|每人每单|容易 品种|更新求购|精制系列|精致币)', core):
        return True
    # 噪声前缀变体（如 "认证员注:交易级别...", "营销员第13年", "业余爱好者第4年"）
    for prefix in NOISE_NAMES:
        if prefix and core.startswith(prefix):
            return True
    return False


def _is_bank_account_row(name: str, raw_text: str) -> bool:
    """判断是否为银行账户行（区分"建行:6227..."银行账户 vs "建行1250/枚"纪念币）。
    银行账户特征：名称后紧跟冒号 + 银行卡号（连续数字>=4位）。
    """
    if not name or not raw_text:
        return False
    # 检查 raw_text 开头是否为"名称+冒号+数字"模式
    for sep in (':', '：', ' '):
        idx = raw_text.find(name + sep)
        if idx == 0:
            after = raw_text[len(name) + 1:].lstrip()
            # 紧跟连续数字（卡号）→ 银行账户
            if re.match(r'^\d{4,}', after):
                return True
    return False


def _is_valid_record(r: dict) -> bool:
    """校验单条解析记录是否有效，无效则丢弃。"""
    name = (r.get('品种名称') or '').strip()
    if _is_noise_name(name):
        return False

    # 银行账户行过滤（如"建行:6227..."是账户而非纪念币）
    raw_text = r.get('原始文本') or r.get('raw_source') or ''
    if _is_bank_account_row(name, raw_text):
        return False

    has_price = any(
        _is_valid_price_value(r.get(k, ''))
        for k in ('盒价(元)', '卷价(元)', '散新价(元)', '卡册价(元)')
    )
    has_qty = bool((r.get('需求数量') or '').strip())

    # 既无有效价格又无数量 → 无意义
    if not has_price and not has_qty:
        return False

    # 参考价若为电话号/卡号则清空
    ref = r.get('参考价') or ''
    if ref and not _is_valid_price_value(ref):
        r['参考价'] = ''
        if not has_price and not has_qty:
            return False
    return True


def _mask_commas_in_parens(line: str) -> str:
    """保护 ()[]{} 内的逗号，避免 split_entries 误拆。"""
    out = []
    depth = 0
    for ch in line:
        if ch in '([{':
            depth += 1
            out.append(ch)
        elif ch in ')]}':
            depth = max(0, depth - 1)
            out.append(ch)
        elif ch in ',，' and depth > 0:
            out.append('\x00')
        else:
            out.append(ch)
    return ''.join(out)


def _unmask(s: str) -> str:
    return s.replace('\x00', ',')


# ============================================================
# 1. 文本预处理
# ============================================================
def preprocess(text):
    """清洗原始文本：统一全角标点、修正小数点、多余空格等"""
    text = text.replace('，', ',').replace('。', '.').replace('；', ';').replace('：', ':')
    text = text.replace('【', '[').replace('】', ']').replace('（', '(').replace('）', ')')
    text = re.sub(r'(\d+)\.{2,}(\d+)', r'\1.\2', text)
    text = re.sub(r'(\d+)\s+\.(\d+)', r'\1.\2', text)
    text = re.sub(r'(\d+)\.(?=\s|盒|卷|散|卡|要|箱|个|对|枚|套|北|开|原|[，,\[\]])', r'\1', text)
    text = re.sub(r'[ \t]+', ' ', text)
    return text.strip()


# ============================================================
# 2. 拆分行内多条目
# ============================================================
def split_entries(raw_line):
    """将一行内的多个独立条目拆分为多行。"""
    line = raw_line.strip()
    if not line:
        return []

    entries = []
    notes_in_brackets = re.findall(r'\[[^\]]*\]', line)
    line_no_notes = re.sub(r'\[[^\]]*\]', '', line)

    # 保护 ()[]{} 内的逗号，避免备注/说明被误拆成独立条目
    masked = _mask_commas_in_parens(line_no_notes)
    parts = re.split(r'[,，]', masked)
    parts = [_unmask(p) for p in parts]

    base_entry = None
    for i, p in enumerate(parts):
        p = p.strip()
        if not p:
            continue
        if i == 0:
            base_entry = p
            if notes_in_brackets:
                base_entry = base_entry + ' ' + ' '.join(notes_in_brackets)
            entries.append(base_entry)
        else:
            has_independent_qty = bool(re.search(r'要\s*\d+', p))
            has_independent_price = bool(re.search(r'\d+\.?\d*\s*/\s*(枚|套|对)', p))
            is_price_clause_start = bool(re.match(r'^(散币|真正卷拆品|单边|任意单边)\d*', p))
            is_numeric_price_clause = bool(re.match(r'^\d+\.?\d*', p)) and (has_independent_qty or has_independent_price)

            if (has_independent_qty or (has_independent_price and is_price_clause_start) or is_numeric_price_clause):
                if re.match(r'^(散币|真正卷拆品|单边|任意单边)', p):
                    m_name = re.match(r'^(散币|真正卷拆品|单边\S*|任意单边\S*)', p)
                    prefix = ''
                    if m_name and base_entry:
                        main_name = extract_name_simple(base_entry)
                        prefix = main_name + m_name.group(1)
                    entries.append(prefix + ' ' + p if prefix else p)
                elif is_numeric_price_clause:
                    pkg_tag = ''
                    if re.search(r'散币|散新', p):
                        pkg_tag = '散币'
                    elif re.search(r'原卷|原桶', p):
                        pkg_tag = ''
                    prefix = ''
                    if base_entry:
                        main_name = extract_name_simple(base_entry)
                        prefix = main_name + pkg_tag if main_name else ''
                    entries.append(prefix + ' ' + p if prefix else p)
                else:
                    entries.append(p)
            else:
                if entries:
                    entries[-1] = entries[-1] + '，' + p
    return entries


def extract_name_simple(text):
    """简易提取名称：取开头非数字非单位的文字部分"""
    m = re.match(r'^([^\d盒卷散新卡要枚对套个箱元/.]+)', text)
    if m:
        return m.group(1).strip()
    return ''


# ============================================================
# 3. 提取名称
# ============================================================
def extract_name(text):
    """从解析文本中提取品种名称。"""
    t = text.strip()

    # 处理"动词:品种"开头：如"求:遗产"、"收:二虎"、"求购:泰山"
    m_verb_colon = re.match(r'^(收购|求购|出售|收|求|出|售)\s*[:：]\s*', t)
    if m_verb_colon:
        t = t[m_verb_colon.end():]

    # 处理"价格+动词"开头：如"14.5收二虎"、"5.8求泰山"、"10.1收购建军"
    # 剥离前导价格与动词，名称从动词之后提取（价格仍由 extract_prices 从原文提取）
    m_prefix = re.match(r'^\d+\.?\d*\s*(收购|求购|出售|收|求|出|售)', t)
    if m_prefix:
        t = t[m_prefix.end():].strip()

    # 处理"动词+品种"开头（无价格）：如"收二虎"、"求泰山"、"收购建军"
    m_verb_only = re.match(r'^(收购|求购|出售|收|求|出|售)(?![\d:])', t)
    if m_verb_only:
        t = t[m_verb_only.end():].strip()

    m1 = re.match(r'^(.+?)(?=(?:盒|卷|散新|散币|卡|件|\d|要|/))', t)
    if m1:
        name = m1.group(1).strip()
        name = _clean_name(name)
        if name:
            return name

    m2 = re.match(r'^(.+?)(?=(?:\d|/))', t)
    if m2:
        name = m2.group(1).strip()
        name = _clean_name(name)
        if name:
            return name

    fallback = re.sub(r'[\d\.]', '', t)
    fallback = re.sub(r'(盒|卷|散新|散币|卡|件|要|枚|套|对|个|箱|元|/|,|，|\[.*?\])', '', fallback)
    return _clean_name(fallback) or t


# ============================================================
# 4. 提取价格（盒/卷/散新/卡册）
# ============================================================
def normalize_price(s):
    """清理价格字符串"""
    s = s.strip()
    s = re.sub(r'\s+', '', s)
    s = re.sub(r'\.{2,}', '.', s)
    if s.endswith('.'):
        s = s[:-1]
    return s


def extract_prices(text, name):
    """提取各包装形式价格。返回 dict: {'he','juan','san','ka','unit'}"""
    result = {'he': '', 'juan': '', 'san': '', 'ka': '', 'unit': ''}
    t = text

    if name and t.startswith(name):
        body = t[len(name):]
    else:
        body = t

    # A. X/单位 显式价格模式
    pkg_unit_price = re.compile(r'(盒|卷|散新|散币|卡|卡册|件)\s*(\d+\.?\d*)\s*/\s*(枚|套|对|个)')
    for m in pkg_unit_price.finditer(body):
        pkg, price, unit = m.group(1), m.group(2), m.group(3)
        result['unit'] = result['unit'] or unit
        price = normalize_price(price)
        if pkg in ('盒',):
            result['he'] = result['he'] or price
        elif pkg in ('卷',):
            result['juan'] = result['juan'] or price
        elif pkg in ('散新', '散币'):
            result['san'] = result['san'] or price
        elif pkg in ('卡', '卡册'):
            result['ka'] = result['ka'] or price

    pkg_unit_price2 = re.compile(r'(盒|卷|散新|散币|卡|卡册|件)(\d+\.?\d*)\s*/\s*(枚|套|对|个)')
    for m in pkg_unit_price2.finditer(body):
        pkg, price, unit = m.group(1), m.group(2), m.group(3)
        result['unit'] = result['unit'] or unit
        price = normalize_price(price)
        if pkg in ('盒',) and not result['he']:
            result['he'] = price
        elif pkg in ('卷',) and not result['juan']:
            result['juan'] = price

    rev_pkg = re.compile(r'(?:^|[^盒卷散新卡件])(\d+\.?\d*)\s*/\s*(枚|套|对|个)\s*(盒|卷)')
    for m in rev_pkg.finditer(body):
        price, unit, pkg = m.group(1), m.group(2), m.group(3)
        result['unit'] = result['unit'] or unit
        price = normalize_price(price)
        if pkg == '盒' and not result['he']:
            result['he'] = price
        elif pkg == '卷' and not result['juan']:
            result['juan'] = price

    # B. 传统模式：盒X 卷Y 散新Z
    yao_idx = body.find('要')
    price_body = body[:yao_idx] if yao_idx >= 0 else body

    combo_pat = re.compile(r'(?:盒卷|卷盒)\.?\s*(\d+\.?\d*)')
    m_combo = combo_pat.search(price_body)
    if m_combo:
        combo_price = normalize_price(m_combo.group(1))
        result['he'] = result['he'] or combo_price
        result['juan'] = result['juan'] or combo_price

    he_pat = re.compile(r'盒(?!卷)\.?\s*(\d+\.?\d*)')
    m_he = he_pat.search(price_body)
    if m_he and not result['he']:
        result['he'] = normalize_price(m_he.group(1))

    he_pat2 = re.compile(r'(\d+\.?\d*)\s*盒(?!卷)')
    m_he2 = he_pat2.search(price_body)
    if m_he2 and not result['he']:
        idx = m_he2.start(1)
        pre = price_body[max(0, idx - 2):idx]
        if not re.search(r'(卷|散新|散币|卡|件)$', pre):
            result['he'] = normalize_price(m_he2.group(1))

    juan_pat = re.compile(r'卷(?!盒|拆)\.?\s*(\d+\.?\d*)')
    m_juan = juan_pat.search(price_body)
    if m_juan and not result['juan']:
        result['juan'] = normalize_price(m_juan.group(1))
    juan_pat2 = re.compile(r'(\d+\.?\d*)\s*卷(?!拆|盒)')
    m_juan2 = juan_pat2.search(price_body)
    if m_juan2 and not result['juan']:
        idx = m_juan2.start(1)
        pre = price_body[max(0, idx - 3):idx]
        if not re.search(r'(盒|散新|散币|卡|件)$', pre):
            result['juan'] = normalize_price(m_juan2.group(1))

    san_pat = re.compile(r'(散新|散币)\.?\s*(\d+\.?\d*)')
    m_san = san_pat.search(body)
    if m_san and not result['san']:
        result['san'] = normalize_price(m_san.group(2))
    san_pat2 = re.compile(r'(\d+\.?\d*)\s*(散新|散币)')
    m_san2 = san_pat2.search(body)
    if m_san2 and not result['san']:
        result['san'] = normalize_price(m_san2.group(1))

    ka_pat = re.compile(r'(?:^|[^二])卡\.?\s*(\d+\.?\d*)')
    m_ka = ka_pat.search(price_body)
    if m_ka and not result['ka']:
        result['ka'] = normalize_price(m_ka.group(1))
    ka_pat2 = re.compile(r'卡(\d+\.?\d*)')
    m_ka2 = ka_pat2.search(price_body)
    if m_ka2 and not result['ka']:
        result['ka'] = normalize_price(m_ka2.group(1))

    # C. 无包装关键词：名称后直接跟数字
    if not any([result['he'], result['juan'], result['san'], result['ka']]):
        bare_pat = re.match(r'\D*?(\d+\.?\d*)', body)
        if bare_pat:
            price = normalize_price(bare_pat.group(1))
            if _looks_like_price(price):
                result['san'] = price

    # D. 确定计价单位
    if not result['unit']:
        if re.search(r'/枚', body):
            result['unit'] = '枚'
        elif re.search(r'/套', body):
            result['unit'] = '套'
        elif re.search(r'/对', body):
            result['unit'] = '对'

    # E. 语序倒装修复
    yao = body.find('要')
    if yao >= 0:
        after_yao = body[yao:]
        if re.search(r'要[盒卷散新卡件原]*盒卷', after_yao) or re.search(r'要盒卷', after_yao):
            if result['san'] and not result['he']:
                result['he'] = result['san']
            if result['san'] and not result['juan']:
                result['juan'] = result['san']

    return result


# ============================================================
# 5. 提取数量与数量单位
# ============================================================
def extract_quantity(text):
    """返回 (qty, qty_unit)"""
    t = text
    qty_pat = re.compile(r'要\s*(?:盒|卷|散新|散币|卡|件|原卷|原桶|盒卷|卷盒)?\s*(\d+\.?\d*)\s*(套|枚|对|个|箱|盒)?')
    matches = list(qty_pat.finditer(t))
    if matches:
        last = matches[-1]
        qty = last.group(1)
        unit = last.group(2) or ''
        qty = normalize_price(qty)
        return qty, unit

    qty_pat2 = re.compile(r'要\s*(\d+\.?\d*)\s*(箱)')
    m2 = qty_pat2.search(t)
    if m2:
        return normalize_price(m2.group(1)), m2.group(2) or '箱'

    return '', ''


# ============================================================
# 6. 提取备注
# ============================================================
def extract_notes(text, name, prices, qty, qty_unit):
    """返回 (pkg_req, quality_req, other)"""
    t = text

    cleaned = t
    if name:
        cleaned = cleaned.replace(name, ' ', 1)
    cleaned = re.sub(r'\d+\.?\d*\s*/\s*(枚|套|对|个)', ' ', cleaned)
    cleaned = re.sub(r'\d+\.?\d*', ' ', cleaned)
    cleaned = re.sub(r'\b(盒|卷|散新|散币|卡|卡册|件|元)\b', ' ', cleaned)
    cleaned = re.sub(r'要\s*(套|枚|对|个|箱|盒)?', ' ', cleaned)
    cleaned = re.sub(r'\s+/\s*', ' ', cleaned)

    pkg_keywords = [
        '原卷', '原桶未开', '原桶', '开盒不开袋', '北方版',
        '单边卷', '任意单边卷', '单边朝天宫', '单边', '不要卷币',
    ]
    quality_keywords = [
        '真正卷拆品', '卷拆品', '流通好品', '9品', '散新', '差品',
        '达不到卷拆不要', '达不到真正卷拆的不要', '差品不要',
        '币里面无油或微油', '币里面可微油', '无油或微油', '可微油', '油大不要',
    ]
    other_known_notes = ['差品看货议价']

    pkg_req_parts = []
    quality_req_parts = []
    other_parts = []

    brackets = re.findall(r'\[([^\]]*)\]', t)
    all_kw_sorted = sorted(pkg_keywords + quality_keywords + other_known_notes, key=len, reverse=True)

    def classify_fragment(frag):
        frag = frag.strip()
        if not frag:
            return
        for kw in all_kw_sorted:
            if kw in frag:
                if kw in pkg_keywords:
                    pkg_req_parts.append(kw)
                elif kw in quality_keywords:
                    quality_req_parts.append(kw)
                else:
                    other_parts.append(kw)
                frag = frag.replace(kw, ' ')
        remain = re.sub(r'[\s,，。.；;：:、/\\\[\]\(\)（）]+', '', frag)
        if len(remain) >= 2:
            other_parts.append(remain)

    for b in brackets:
        for frag in re.split(r'[,，;；]', b):
            classify_fragment(frag)

    remaining = cleaned
    found_kws = set()
    for kw in all_kw_sorted:
        if kw in remaining and kw not in found_kws:
            already = (kw in pkg_req_parts) or (kw in quality_req_parts) or (kw in other_parts)
            if not already:
                found_kws.add(kw)
                if kw in pkg_keywords:
                    pkg_req_parts.append(kw)
                elif kw in quality_keywords:
                    quality_req_parts.append(kw)
                else:
                    other_parts.append(kw)
            remaining = remaining.replace(kw, ' ')

    for seg in re.split(r'[,，;；]', remaining):
        classify_fragment(seg)

    remaining_clean = re.sub(r'[\s,，。.；;：:、/\\\[\]\(\)（）]+', ' ', remaining).strip()
    if remaining_clean:
        frags = [f.strip() for f in remaining_clean.split() if len(f.strip()) >= 2]
        for frag in frags:
            if frag not in other_parts:
                other_parts.append(frag)

    def dedup(seq):
        seen = set()
        out = []
        for x in seq:
            if x and x not in seen:
                seen.add(x)
                out.append(x)
        return out

    all_kw_flat = set(pkg_keywords + quality_keywords + other_known_notes)

    def is_junk_frag(f):
        if len(f) <= 1:
            return True
        if re.fullmatch(r'[盒卷散卡件枚套对个箱原桶不开]+', f):
            return True
        if f not in all_kw_flat:
            for kw in all_kw_flat:
                if len(f) >= 2 and (f in kw and f != kw):
                    return True
        return False

    other_parts = [x for x in other_parts if not is_junk_frag(x)]

    def clean_joined(s):
        s = re.sub(r'[，,、；;]+$', '', s.strip())
        s = re.sub(r'^[，,、；;]+', '', s)
        s = re.sub(r'[，,]+', '、', s)
        return s.strip('、').strip()

    pkg_req = clean_joined('、'.join(dedup(pkg_req_parts)))
    quality_req = clean_joined('、'.join(dedup(quality_req_parts)))
    other = clean_joined('、'.join(dedup(other_parts)))

    return pkg_req, quality_req, other


# ============================================================
# 7. 单条解析
# ============================================================
def parse_entry(entry_text, raw_source=''):
    """解析单行/单条目，返回 dict"""
    record = {
        '品种名称': '',
        '参考价': '',
        '盒价(元)': '',
        '卷价(元)': '',
        '散新价(元)': '',
        '卡册价(元)': '',
        '单位': '',
        '需求数量': '',
        '数量单位': '',
        '包装要求': '',
        '品相要求': '',
        '其他备注': '',
        '原始文本': raw_source or entry_text,
    }
    try:
        name = extract_name(entry_text)
        record['品种名称'] = name

        prices = extract_prices(entry_text, name)
        record['盒价(元)'] = prices['he']
        record['卷价(元)'] = prices['juan']
        record['散新价(元)'] = prices['san']
        record['卡册价(元)'] = prices['ka']
        record['单位'] = prices['unit']

        all_prices = []
        for key in ('he', 'juan', 'san', 'ka'):
            v = prices[key]
            if v:
                try:
                    all_prices.append(float(v))
                except ValueError:
                    pass
        if all_prices:
            record['参考价'] = str(min(all_prices))
        else:
            record['参考价'] = ''

        qty, qty_unit = extract_quantity(entry_text)
        record['需求数量'] = qty
        record['数量单位'] = qty_unit
        if not record['单位'] and qty_unit in ('枚', '套', '对', '个'):
            record['单位'] = qty_unit

        pkg_req, quality_req, other = extract_notes(
            entry_text, name, prices, qty, qty_unit
        )
        record['包装要求'] = pkg_req
        record['品相要求'] = quality_req
        record['其他备注'] = other

    except Exception as e:
        record['其他备注'] = (record['其他备注'] + '；' if record['其他备注'] else '') + f'解析异常:{e}'
    return record


# ============================================================
# 8. 对外接口
# ============================================================
def parse(text: str) -> list[PriceRecord]:
    """纪念币解析入口：预处理 → 按行拆分 → 逐条解析 → 转 PriceRecord"""
    cleaned = preprocess(text)
    records: list[PriceRecord] = []
    for line in cleaned.split('\n'):
        line = line.strip()
        if not line:
            continue
        for ent in split_entries(line):
            ent = ent.strip()
            if not ent:
                continue
            r = parse_entry(ent, raw_source=line)
            if not _is_valid_record(r):
                continue
            records.append(PriceRecord(
                name=r['品种名称'],
                ref_price=r['参考价'],
                price_details={
                    '盒价': r['盒价(元)'],
                    '卷价': r['卷价(元)'],
                    '散新价': r['散新价(元)'],
                    '卡册价': r['卡册价(元)'],
                },
                unit=r['单位'],
                quantity=r['需求数量'],
                quantity_unit=r['数量单位'],
                package_req=r['包装要求'],
                quality_req=r['品相要求'],
                other_notes=r['其他备注'],
                raw_text=r['原始文本'],
            ))
    return records
