#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把近半年飞书活动回溯成 V2 结构，并按客户/产品线回填项目场景。

只生成数据文件，不改线上数据。
输出：
  数据/customer_rollup_v2.json
  资料/客户拉通大表_V2_场景回填报告_20261101.csv
"""
import csv
import html
import json
import os
import re
from collections import Counter, defaultdict
from datetime import datetime

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
DATA = os.path.join(ROOT, '数据')
OUT = os.path.join(DATA, 'customer_rollup_v2.json')
REPORT = os.path.join(ROOT, '资料', '客户拉通大表_V2_场景回填报告_20261101.csv')

SCENES = ['三类证注册', '动保', '科研客户', '其他']
SINCE = '2026-05-01'

ACADEMIC = re.compile(r'(大学|学院|研究院|研究所|学校|实验室|课题组|研究中心|科学院)')
ANIMAL = re.compile(r'(动保|动物|宠物|农牧|畜牧|非洲猪瘟|水产|禽|猪|牛|羊|兽药|兽医)')
GMP = re.compile(r'(兽药|GMP|兽用|动物疫苗|疫苗生产|生物制品)')
DIAG = re.compile(r'(诊断|基因|检验|医学|生物|PCR|核酸|分子)')
NON_DIAG_DEPT = re.compile(r'(生命科学|生物医药|国际)')


def read_json(name):
    with open(os.path.join(DATA, name), encoding='utf-8') as f:
        return json.load(f)


def slug(s):
    return re.sub(r'\s+', '', str(s or ''))


def plain_text(s):
    s = str(s or '')
    s = re.sub(r'<br\s*/?>', '\n', s, flags=re.I)
    s = re.sub(r'</p\s*>', '\n', s, flags=re.I)
    s = re.sub(r'<[^>]+>', '', s)
    s = html.unescape(s)
    s = re.sub(r'[ \t]+', ' ', s)
    s = re.sub(r'\n{3,}', '\n\n', s)
    return s.strip()


def customer_meta():
    d = read_json('mcp_customers.json')
    m = {}
    for c in (d.get('customers') or []):
        n = (c.get('name') or '').strip()
        if not n:
            continue
        m[n] = c
    return m


def product_line_map():
    """客户 → 主要产品线（忽略基础物料/空值）。"""
    d = read_json('orders_detail_2026.json')
    cnt = defaultdict(Counter)
    for o in (d.get('orders') or {}).values():
        cust = (o.get('customer') or '').strip()
        if not cust:
            continue
        for ln in (o.get('lines') or []):
            p = (ln.get('pgroup') or ln.get('ptype') or '').strip()
            if not p or p.startswith('基础物料'):
                continue
            cnt[cust][p] += 1
    out = {}
    for c, cc in cnt.items():
        out[c] = cc.most_common(1)[0][0]
    return out


def customer_scene(name, meta, product_line, labels):
    text = ' '.join([name or '', ' '.join(labels or []), product_line or ''])
    dept = (meta or {}).get('dept') or ''
    # 1) 科研客户：高校/研究院所优先
    if ACADEMIC.search(name or ''):
        return '科研客户', '客户名称命中高校/研究院所'
    # 兽药/GMP/疫苗项目并入动保
    if GMP.search(text):
        return '动保', '兽药/GMP/疫苗项目并入动保'
    # 2) 动保：动物/宠物/农牧/水产等
    if ANIMAL.search(text):
        return '动保', '项目/客户命中动保关键词'
    # 4) 三类证注册：诊断原料产品线或诊断客户
    if (product_line or '').startswith('诊断原料') or (DIAG.search(name or '') and not NON_DIAG_DEPT.search(dept)):
        return '三类证注册', '诊断原料产品线/诊断客户'
    # 5) 其他
    return '其他', '未命中前三类规则'


def project_scene(cust, opp, meta, product_line, labels):
    text = ' '.join([opp or '', ' '.join(labels or [])])
    # 项目名优先：兽药/GMP/疫苗并入动保
    if GMP.search(text):
        return '动保', '商机/项目名称命中兽药/疫苗，并入动保'
    if ANIMAL.search(text):
        return '动保', '商机/项目名称命中动保关键词'
    # 学术客户
    if ACADEMIC.search(cust or ''):
        return '科研客户', '客户名称命中高校/研究院所'
    # 诊断原料部门/产品线兜底（核酸扩增原料等子类也归诊断原料）
    _dept = (meta or {}).get('dept') or ''
    if '诊断原料' in _dept:
        return '三类证注册', '客户归属诊断原料部门'
    if (product_line or '').startswith('诊断原料'):
        return '三类证注册', '客户产品线为诊断原料'
    return '其他', '未命中项目/客户/产品线规则'


def label_list(meta):
    v = (meta or {}).get('field_79aa0b')
    out = []
    if isinstance(v, list):
        for x in v:
            if isinstance(x, dict) and x.get('label'):
                out.append(x['label'])
    elif isinstance(v, str) and v:
        out.append(v)
    return out


def normalize_channel(atype, channel=''):
    a = atype or ''
    if a.startswith('陌拜'):
        return '陌拜'
    if a.startswith('线上'):
        return '线上跟进'
    if a.startswith('线下'):
        return '线下拜访'
    return {'online': '线上跟进', 'offline': '线下拜访'}.get(channel or '', '线下拜访')


def infer_content(text):
    t = text or ''
    if re.search(r'(测试结果|实测|Ct|通过|超阈|结果图|检测结果)', t):
        return '测试结果跟进'
    if re.search(r'(订单|采购|报价|合同|下单|首单|到货|发货|付款)', t):
        return '订单跟进'
    if re.search(r'(决策链|组织架构|联系人|客户档案|关键人|决策人)', t):
        return '客户档案完善'
    if re.search(r'(其他|会议|拜访目的|寒暄)', t) and len(t) < 120:
        return '其他'
    return '项目需求挖掘'


def pick_opp(text, cust, opp_map):
    names = opp_map.get(cust) or []
    if not names:
        return '', '客户无商机'
    # 最长匹配优先，避免短名误命中
    hits = [n for n in names if n and n in text]
    if hits:
        return sorted(hits, key=len, reverse=True)[0], '正文命中商机名'
    if len(names) == 1:
        return names[0], '客户唯一商机'
    return '', '客户多商机且正文未命中'


def extract_test(text):
    m = re.search(r'((?:ZHBR|BTE|SO|TS|FMD|MD)[A-Za-z0-9\-]{6,})', text or '')
    if m:
        return m.group(1)
    for line in (text or '').splitlines():
        if re.search(r'(测试|结果|实测)', line):
            return line.strip()[:180]
    return '—'


def extract_next(text):
    t = text or ''
    m = re.search(r'(?:【?下一步[^】\n]*】?|待办事项)[：:\s]*\n?([\s\S]{0,240})', t)
    if m:
        return re.sub(r'\n+', ' ', m.group(1)).strip()[:220]
    lines = [x.strip() for x in t.splitlines() if x.strip()]
    return (lines[-1][:220] if lines else '—')


def main():
    meta = customer_meta()
    prod = product_line_map()
    opps = read_json('opps_by_customer.json')
    opp_map = {c: [x.get('name') for x in (ls or []) if x.get('name')] for c, ls in opps.items()}
    acts = read_json('activities_feishu.json').get('items') or []

    # 客户场景映射
    scene_of, basis_of, product_line_of = {}, {}, {}
    for c in set(list(opp_map.keys()) + [a.get('customer') for a in acts if a.get('customer')]):
        c = (c or '').strip()
        if not c:
            continue
        m = meta.get(c)
        pl = prod.get(c, '')
        labels = label_list(m)
        sc, basis = customer_scene(c, m, pl, labels)
        scene_of[c] = sc
        basis_of[c] = basis
        product_line_of[c] = pl

    # 第一层项目：以 opps_by_customer 为真实商机底座
    projects = []
    for c, ls in opps.items():
        m = meta.get(c) or {}
        for o in (ls or []):
            name = (o.get('name') or '').strip()
            if not name:
                continue
            if c.strip().lower() == 'test' or name.lower().startswith('test'):
                continue
            psc, pbasis = project_scene(c, name, m, product_line_of.get(c, ''), label_list(m))
            projects.append({
                'cust': c,
                'cid': str(m.get('work_item_id') or ''),
                'opp': name,
                'dept': m.get('dept') or '',
                'sales': (o.get('creator') or (m.get('owner') or {}).get('name_cn') or ''),
                'group': m.get('group') or '',
                'level': m.get('level') or '',
                'product_line': product_line_of.get(c, ''),
                'scene': psc,
                'scene_basis': pbasis,
                'raw_status': o.get('status') or '',
                'url': o.get('url') or '',
            })

    # 近半年飞书活动 → V2
    activities = []
    skipped = Counter()
    for a in acts:
        date = (a.get('date') or '').strip()
        if not date or date < SINCE:
            skipped['date'] += 1
            continue
        raw = plain_text(a.get('text') or a.get('raw_desc') or '')
        cust = (a.get('customer') or '').strip()
        if not cust:
            skipped['no_customer'] += 1
            continue
        ch = normalize_channel(a.get('atype'), a.get('channel'))
        content = infer_content(raw)
        opp, opp_basis = pick_opp(raw, cust, opp_map)
        if opp:
            scene, scene_basis = project_scene(cust, opp, meta.get(cust) or {}, product_line_of.get(cust, ''), label_list(meta.get(cust) or {}))
        else:
            scene, scene_basis = scene_of.get(cust, '其他'), basis_of.get(cust, '未命中规则')
        # 正文没有商机名时，项目场景仍按客户规则
        act = {
            'id': 'BAK-' + str(a.get('id') or ''),
            'source': 'feishu_backfill',
            'backfill': True,
            'v2': True,
            'date': date,
            'created': date + ' 00:00:00',
            'submitted_by': a.get('submitted_by') or '',
            'sales': a.get('sales') or a.get('submitted_by') or '',
            'dept': a.get('dept_raw') or '',
            'customer': cust,
            'contact': '',
            'activity_type': ch,
            'content_type': content,
            'project_scene': scene,
            'scene_basis': scene_basis,
            'product_line': product_line_of.get(cust, ''),
            'opp_names': [opp] if opp else [],
            'opp_basis': opp_basis,
            'approval_status': '历史回填',
            'approval': '历史回填 · 无审批',
            'test': extract_test(raw),
            'next': extract_next(raw),
            'desc': raw[:600],
            'feishu_id': a.get('id') or '',
            'feishu_atype': a.get('atype') or '',
            'feishu_type': a.get('type') or '',
        }
        activities.append(act)

    scene_counts = Counter(p['scene'] for p in projects)
    activity_scene = Counter(x['project_scene'] for x in activities)
    content_counts = Counter(x['content_type'] for x in activities)
    unmatched_cust = Counter(x['customer'] for x in activities if x['customer'] not in meta)
    no_opp = sum(1 for x in activities if not x.get('opp_names'))

    out = {
        'meta': {
            'version': 'v2-backfill-20261101',
            'generated': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'since': SINCE,
            'source_activities': '数据/activities_feishu.json',
            'source_projects': '数据/opps_by_customer.json',
            'source_products': '数据/orders_detail_2026.json',
            'scene_rules': '科研客户 > 动保（含兽药/疫苗） > 三类证注册 > 其他',
            'activity_count': len(activities),
            'project_count': len(projects),
            'activity_skipped': dict(skipped),
            'activity_no_opp': no_opp,
            'customer_unmatched': len(unmatched_cust),
            'scene_counts': dict(scene_counts),
            'activity_scene_counts': dict(activity_scene),
            'content_counts': dict(content_counts),
        },
        'projects': projects,
        'activities': activities,
    }
    os.makedirs(DATA, exist_ok=True)
    with open(OUT, 'w', encoding='utf-8') as f:
        json.dump(out, f, ensure_ascii=False, separators=(',', ':'))
    os.makedirs(os.path.dirname(REPORT), exist_ok=True)
    with open(REPORT, 'w', encoding='utf-8-sig', newline='') as f:
        w = csv.writer(f)
        w.writerow(['类型', '客户', '商机/活动ID', '名称', '项目场景', '产品线', '场景依据', '部门', '销售', '备注'])
        for p in projects:
            w.writerow(['项目', p['cust'], p['cid'], p['opp'], p['scene'], p['product_line'], p['scene_basis'], p['dept'], p['sales'], p['raw_status']])
        for a in activities:
            w.writerow(['活动', a['customer'], a['id'], (a.get('opp_names') or [''])[0], a['project_scene'], a['product_line'], a['scene_basis'], a['dept'], a['sales'], a['opp_basis']])
    print('WROTE', OUT, os.path.getsize(OUT))
    print('REPORT', REPORT, os.path.getsize(REPORT))
    print('projects', len(projects), dict(scene_counts))
    print('activities', len(activities), dict(activity_scene))
    print('content', dict(content_counts))
    print('no_opp', no_opp, 'unmatched_customers', len(unmatched_cust), 'skipped', dict(skipped))


if __name__ == '__main__':
    main()
