#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把《产品资料（说明书/COA/MSDS）》里的说明书入库，补齐知识库缺口。

来源目录为 iCloud 未物化（blocks=0）→ 先 brctl download 物化，再拷贝到本地资料区并抽表。
产出：
  资料/说明书_产品资料/<文件>.pdf              本地可读副本
  数据/asset_manuals.json                      货号 → {文件/品名(自文件名)/体系配制/反应程序/页数}
选取规则：优先「01 正式版说明书」，次选「02 测试说明书」；同名多版本取版本号/日期最新；
        跳过「测试样品使用说明」（除非该货号没有正式说明书）。
"""
import json
import os
import re
import shutil
import subprocess
import time
from collections import defaultdict

import fitz

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
SRC = ('/Users/liuxinyuan/Desktop/hermas输入-工作台数据库/宝锐产品资料/'
       '宝锐产品资料（说明书、COA、MSDS）/2026_10_12_01 成品COA【QA维护】/02 产品说明书【QA及产品部门维护】')
OUT_PDF = os.path.join(W, '资料/说明书_产品资料')
OUT_JSON = os.path.join(W, '数据/asset_manuals.json')
CORE = os.path.join(W, '数据/diag_knowledge.json')
KW_SYS = ['体系配制', '反应体系', '配制体系', '体系组成', '反应液配制', 'PCR体系', '组分', '用量']
KW_PROG = ['反应程序', '循环条件', '扩增程序', '反应条件', '预变性', '退火', '延伸', '循环数', '采集荧光']


def loose(s):
    return re.sub(r'[^A-Z0-9\u4e00-\u9fff]', '', str(s or '').upper())


def ver_key(name):
    v = re.findall(r'V(\d+)', name.upper())
    d = re.findall(r'(20\d{6})', name)
    return (max([int(x) for x in v], default=0), max([int(x) for x in d], default=0))


def materialize(path):
    """iCloud 物化：blocks=0 先 brctl download，再校验。"""
    def blocks():
        out = subprocess.run(['stat', '-f', '%b', path], capture_output=True, text=True).stdout.strip()
        return int(out or 0)
    if blocks() > 0:
        return True
    subprocess.run(['brctl', 'download', path], capture_output=True, text=True)
    for _ in range(20):
        time.sleep(1.5)
        if blocks() > 0:
            return True
    return blocks() > 0


def parse_pdf(path):
    doc = fitz.open(path)
    res = {'体系配制': [], '反应程序': [], '页数': doc.page_count}
    sys_p, prog_p = [], []
    for i, pg in enumerate(doc, 1):
        t = pg.get_text('text')
        if any(k in t for k in KW_SYS) and len(sys_p) < 3:
            sys_p.append(i)
        if any(k in t for k in KW_PROG) and len(prog_p) < 3:
            prog_p.append(i)
    for tag, pages, kws in (('体系配制', sys_p, KW_SYS), ('反应程序', prog_p, KW_PROG)):
        for pno in pages:
            pg = doc[pno - 1]
            tabs = []
            try:
                for tb in (pg.find_tables() or []):
                    rows = [[re.sub(r'\s+', ' ', str(c or '')).strip() for c in row] for row in tb.extract()]
                    if len(rows) >= 2:
                        tabs.append(rows)
            except Exception:
                pass
            blocks = [re.sub(r'\n{2,}', '\n', p).strip() for p in re.split(r'\n(?=[^\n])', pg.get_text('text'))
                      if any(k in p for k in kws) and len(p) < 600]
            res[tag].append({'页': pno, '表格': tabs, '文本块': blocks[:4]})
    return res


def name_from_file(fn):
    """从说明书文件名中取品名（去掉 说明书(...) 前缀与 -VXX版/日期 后缀）。"""
    s = re.sub(r'^.*?说明书\s*[（(][^）)]*[）)]\s*', '', fn)
    s = re.sub(r'\.pdf$', '', s, flags=re.I)
    s = re.sub(r'[-－]?\s*20\d{2}\s*V\d+.*$', '', s)
    s = re.sub(r'[-－]?\s*V\d+版?.*$', '', s)
    s = re.sub(r'[（(]\s*\d{4,8}\s*[）)].*$', '', s)
    return s.strip(' -_（）()')


def main():
    os.makedirs(OUT_PDF, exist_ok=True)
    core = json.load(open(CORE, encoding='utf-8'))
    have = {loose(r['货号']): r['货号'] for r in core['items']}
    # 收集目录内所有 PDF
    allf = []
    for root, _, files in os.walk(SRC):
        for f in files:
            if f.lower().endswith('.pdf') and not f.startswith('.'):
                allf.append(os.path.join(root, f))
    print('说明书目录文件 %d 个' % len(allf))
    # 建 loose 名字索引
    idx = defaultdict(list)
    for p in allf:
        idx[loose(os.path.basename(p))].append(p)

    targets = {r['货号']: loose(r['货号']) for r in core['items'] if not r.get('说明书本地文件')}
    print('待补货号 %d 个' % len(targets))
    out, todo = {}, []
    for sku, lk in targets.items():
        cands = [p for k, ps in idx.items() if lk and lk in k for p in ps]
        cands = [p for p in cands if '正式版' in p] + [p for p in cands if '正式版' not in p]
        cands = [p for p in cands if '测试样品使用说明' not in os.path.basename(p)] or cands
        cands.sort(key=lambda p: (('01 正式版' in p), ver_key(os.path.basename(p))), reverse=True)
        if cands:
            out[sku] = cands[0]
            todo.append((sku, cands[0]))

    print('匹配到说明书 %d / %d' % (len(todo), len(targets)))
    res, ok, fail = {}, 0, 0
    for i, (sku, path) in enumerate(todo, 1):
        fn = os.path.basename(path)
        try:
            if not materialize(path):
                raise RuntimeError('物化失败 blocks=0')
            dst = os.path.join(OUT_PDF, re.sub(r'[^\w\u4e00-\u9fff.\-（()）]+', '_', fn)[:120])
            if not os.path.exists(dst):
                shutil.copy2(path, dst)
            d = parse_pdf(dst)
            d.update({'货号': sku, '原始文件': fn, '源路径': path, '本地副本': os.path.basename(dst),
                      '品名_自文件名': name_from_file(fn),
                      '体系配制表数': sum(len(x['表格']) for x in d['体系配制'])})
            res[sku] = d
            ok += 1
        except Exception as e:
            fail += 1
            print('  ✗ %s / %s: %s' % (sku, fn[:40], str(e)[:70]))
        if i % 10 == 0:
            print('  进度 %d/%d（成功 %d / 失败 %d）' % (i, len(todo), ok, fail))

    json.dump({'生成时间': time.strftime('%Y-%m-%d %H:%M'), '来源目录': SRC,
               '口径': '说明书取自 QA 正式版目录，iCloud 物化后本地解析；品名取自文件名，保留原始文件名可溯源',
               '条数': len(res), 'items': res},
              open(OUT_JSON, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
    print('\n入库 %d 个货号 ｜ 其中含体系表 %d ｜ 含程序 %d' % (
        len(res), sum(1 for v in res.values() if v['体系配制表数']), sum(1 for v in res.values() if v['反应程序'])))
    print('→ %s ｜ PDF 副本 %s' % (OUT_JSON, OUT_PDF))


if __name__ == '__main__':
    main()
