#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成客户首页 V1 数据：客户目标/达成、商机目标/达成、工单计数、档案摘要、活动时间线。"""
import json, os, re, html
from datetime import datetime

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
DATA = os.path.join(ROOT, '数据')

def load(name, default=None):
    p = os.path.join(DATA, name)
    try:
        with open(p, encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return default if default is not None else {}

def norm(s):
    s = str(s or '').strip().lower()
    s = re.sub(r'-(浙江|广东|江苏|北京|上海|华北|华南|华中|华东|华西|山东|四川|福建|河南|河北|湖南|湖北|安徽|重庆|天津|陕西)$', '', s)
    return re.sub(r'\s+', '', s)

def strip_html(s):
    s = str(s or '')
    s = re.sub(r'<[^>]+>', ' ', s, flags=re.S)
    s = html.unescape(s)
    return re.sub(r'\s+', ' ', s).strip()

def target_wan(raw):
    """目标单位归一：>=1000 视为元，<1000 视为万元；返回 (万, 原值口径)。"""
    try:
        v = float(raw or 0)
    except Exception:
        return None, ''
    if v <= 0:
        return None, ''
    if v >= 1000:
        return round(v / 10000.0, 2), '元'
    return round(v, 2), '万元'

def main():
    c360 = load('customer_360.json', {'customers': []})
    sandbox = load('opportunity_sandbox.json', {'opportunities': []})
    node = load('opportunity_node_status.json', {'items': {}})
    status = load('opportunity_status.json', {'items': {}})
    acts_raw = load('daily_activities_mcp.json', {'items': []})
    overrides = load('opportunity_target_overrides.json', {'items': {}})
    customers = c360.get('customers') or []

    # 客户名索引：全称、短名、归一
    by_name, by_short, by_norm = {}, {}, {}
    for c in customers:
        n = c.get('name') or ''
        if n: by_name[n] = c
        if c.get('short'): by_short[norm(c['short'])] = c
        if n: by_norm[norm(n)] = c

    def resolve_customer(raw, raw_sales=''):
        if not raw: return None
        s = str(raw).strip()
        if s in by_name: return by_name[s]
        n = norm(s)
        if n in by_norm: return by_norm[n]
        if n in by_short: return by_short[n]
        hits = [c for c in customers if n and (n in norm(c.get('name')) or norm(c.get('name')) in n)]
        if len(hits) == 1: return hits[0]
        # 简称歧义（如「圣湘」同时匹配上海主体）：用商机沙盘里的销售员再收窄
        if len(hits) > 1:
            sales = str(raw_sales or '').strip()
            same = [c for c in hits if sales and (c.get('sales') or '') == sales]
            if len(same) == 1: return same[0]
            if len(same) > 1: return max(same, key=lambda c: float(c.get('total') or 0))
            return max(hits, key=lambda c: float(c.get('total') or 0))
        return None

    # 商机目标索引：客户规范名 + 商机名规范名
    target_idx = {}
    for o in sandbox.get('opportunities') or []:
        c = resolve_customer(o.get('customer'), o.get('sales'))
        if not c: continue
        key = (c.get('name'), norm(o.get('name')))
        target_idx.setdefault(key, []).append(o)

    def find_target(c, opp_name):
        k = norm(opp_name)
        vals = target_idx.get((c.get('name'), k)) or []
        if vals:
            v = vals[0]
            return v, 'opportunity_sandbox'
        # 同名不同简称/后缀：唯一包含匹配
        hits = []
        for (cn, on), arr in target_idx.items():
            if cn != c.get('name'): continue
            if k and (k in on or on in k):
                hits.extend(arr)
        if len(hits) == 1:
            return hits[0], 'opportunity_sandbox~fuzzy'
        # 客户卡片自带的商机沙盘（同名/包含匹配）
        for o in c.get('opp_sandbox') or []:
            if norm(o.get('name')) == k or (k and (k in norm(o.get('name')) or norm(o.get('name')) in k)):
                return o, 'customer_360.opp_sandbox'
        return None, ''

    # 技术测试单：来自销售活动里的「技术支持测试单」动作，不是 ERP 测试销售订单。
    # 活动录入 V2 在 activity_v2.build_activity 写入 action_* 字段；老记录回落 plan.item。
    work_orders_by_cust = {}
    act_dir = os.path.join(DATA, 'activities')
    try:
        act_files = [f for f in os.listdir(act_dir) if f.endswith('.json') and not f.startswith('_')]
    except Exception:
        act_files = []
    for fn in act_files:
        try:
            with open(os.path.join(act_dir, fn), encoding='utf-8') as f:
                rec = json.load(f)
        except Exception:
            continue
        if not isinstance(rec, dict):
            continue
        c = resolve_customer(rec.get('customer'), rec.get('sales'))
        if not c:
            continue
        plan = rec.get('plan') or []
        plan_item = next((x for x in plan if '技术支持测试单' in str((x or {}).get('item') or '')), None)
        atype = str(rec.get('action_type') or '').strip()
        if atype != '技术支持测试单' and not plan_item:
            continue
        req = str(rec.get('action_requirement') or '').strip()
        if not req and plan_item:
            item = str(plan_item.get('item') or '')
            req = item.split('：', 1)[1] if '：' in item else item
        if not req:
            req = str(rec.get('visit_content') or rec.get('need') or '').strip()
        opp_names = rec.get('opp_names') or []
        item = {
            'id': rec.get('id') or fn[:-5],
            'no': rec.get('id') or fn[:-5],
            'date': (rec.get('date') or (rec.get('created') or '')[:10] or '')[:10],
            'type': '技术测试单',
            'opp': opp_names[0] if len(opp_names) == 1 else '',
            'name': req[:100] or '技术支持测试单',
            'requirement': req,
            'owner': rec.get('sales') or rec.get('submitted_by') or '',
            'assignee': str(rec.get('action_assignee') or rec.get('action_owner') or (plan_item or {}).get('owner') or '').strip(),
            'status': str(rec.get('action_status') or '待承接').strip() or '待承接',
            'due': str(rec.get('action_due') or (plan_item or {}).get('due') or '').strip(),
            'activity_id': rec.get('id') or fn[:-5],
            'source': '活动录入',
        }
        bucket = work_orders_by_cust.setdefault(c.get('name'), [])
        if not any(x.get('id') == item['id'] for x in bucket):
            bucket.append(item)
    for k in work_orders_by_cust:
        work_orders_by_cust[k].sort(key=lambda x: (x.get('date') or ''), reverse=True)

    # 活动：只给有商机的客户生成（避免全量 4.6 万条塞进静态 JSON）
    cust_with_opps = {c.get('name') for c in customers if c.get('opps')}
    acts_by_cust = {}
    for it in acts_raw.get('items') or []:
        label = (it.get('field_5f20fc') or {}).get('label') if isinstance(it.get('field_5f20fc'), dict) else ''
        c = resolve_customer(label)
        if not c or c.get('name') not in cust_with_opps:
            continue
        text = strip_html(it.get('field_b99055') or '')
        if not text: continue
        date = (it.get('start_time') or '')[:10]
        if not date: continue
        acts_by_cust.setdefault(c.get('name'), []).append({
            'id': it.get('work_item_id') or '',
            'date': date,
            'type': (it.get('field_76654e') or {}).get('label') or '',
            'owner': ((it.get('owner') or {}).get('name_cn') or (it.get('owner') or {}).get('name_en') or ''),
            'opp_name': it.get('opportunity_name') or '',
            'contact': it.get('contact') or '',
            'next': it.get('follow_up_plan') or '',
            'summary': text[:180],
            'full': text[:1800],
        })
    for k in acts_by_cust:
        acts_by_cust[k].sort(key=lambda x: x['date'], reverse=True)
        acts_by_cust[k] = acts_by_cust[k][:120]

    out = {'updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'source': 'customer_360 + opportunity_sandbox + daily_activities_mcp + activities(工作台活动录入)', 'customers': {}}
    for c in customers:
        name = c.get('name') or ''
        if not name: continue
        actual_wan = round(float(c.get('y2026') or 0) / 10000.0, 2)
        opps = []
        for o in c.get('opps') or []:
            t, src = find_target(c, o.get('name') or '')
            raw = t.get('target') if t else None
            tw, unit = target_wan(raw)
            opps.append({
                'name': o.get('name') or '', 'status': o.get('status') or '',
                'creator': o.get('creator') or '', 'url': o.get('url') or '',
                'target_raw': raw, 'target_wan': tw, 'target_unit': unit,
                'target_source': src, 'actual_wan': None, 'attainment': None,
                'manual': False,
            })
        matched = [o for o in opps if o.get('target_wan')]
        target_sum = round(sum(o['target_wan'] for o in matched), 2) if matched else None
        coverage = '%d/%d' % (len(matched), len(opps)) if opps else '0/0'
        # 商机达成：按目标比例分摊客户 2026 采购（用户确认 C 口径）
        if target_sum and target_sum > 0:
            for o in matched:
                o['actual_wan'] = round(actual_wan * o['target_wan'] / target_sum, 2)
                o['attainment'] = round(o['actual_wan'] / o['target_wan'] * 100, 1)
        # 手工覆盖（预留）
        for o in opps:
            key = name + '||' + o['name']
            ov = (overrides.get('items') or {}).get(key)
            if not isinstance(ov, dict): continue
            if ov.get('target_wan') not in (None, ''):
                try:
                    o['target_wan'] = round(float(ov['target_wan']), 2); o['manual'] = True
                    o['target_unit'] = '手工'
                except Exception: pass
            if ov.get('actual_wan') not in (None, ''):
                try:
                    o['actual_wan'] = round(float(ov['actual_wan']), 2); o['manual'] = True
                except Exception: pass
            if o.get('target_wan') and o.get('actual_wan') is not None:
                o['attainment'] = round(o['actual_wan'] / o['target_wan'] * 100, 1)
        if matched:
            target_sum = round(sum(o.get('target_wan') or 0 for o in opps if o.get('target_wan')), 2)
        cust_attr = round(actual_wan / target_sum * 100, 1) if target_sum else None

        tos = work_orders_by_cust.get(name, [])
        wo = {
            'technical_test': {'count': len(tos), 'items': tos[:40], 'source': '活动录入 · 技术支持测试单'},
            'rnd_project': {'count': 0, 'items': [], 'source': 'V2待接入'},
            'doc_prep': {'count': 0, 'items': [], 'source': 'V2待接入'},
            'after_sales': {'count': len(c.get('complaints') or []), 'items': c.get('complaints') or [], 'source': '客诉记录'},
        }
        # 商机节点：节点覆盖层 + 状态层
        for o in opps:
            key = name + '||' + o['name']
            st = (status.get('items') or {}).get(key) or {}
            o['stage'] = st.get('stage') or ''
            o['system_status'] = st.get('status') or o.get('status') or ''
            ns = (node.get('items') or {}).get(key) or {}
            o['node_state'] = ns.get('state') or ''
        out['customers'][name] = {
            'name': name, 'short': c.get('short') or '', 'group': c.get('group') or '',
            'sales': c.get('sales') or '', 'level': c.get('level') or '', 'tier': c.get('tier') or '',
            'total': c.get('total') or 0, 'orders': c.get('orders') or 0,
            'y2024': c.get('y2024') or 0, 'y2025': c.get('y2025') or 0, 'y2026': c.get('y2026') or 0,
            'h2_2026': c.get('h2_2026') or 0, 'latest_date': c.get('latest_date') or '',
            'actual_wan': actual_wan, 'target_wan': target_sum, 'attainment': cust_attr,
            'target_coverage': coverage, 'opps': opps,
            'work_orders': wo,
            'profile': {
                'contacts': c.get('contacts') or 0, 'top_products': c.get('top_products') or [],
                'complaints': c.get('complaints') or [], 'risks': c.get('risks') or [],
                'recent_orders': c.get('recent_orders') or [], 'decision_url': '',
                'level': c.get('level') or '', 'tier': c.get('tier') or '',
            },
            'activities': acts_by_cust.get(name, []),
        }
    p = os.path.join(DATA, 'customer_home_v1.json')
    tmp = p + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(out, f, ensure_ascii=False, separators=(',', ':'))
    os.replace(tmp, p)
    print('written', p, os.path.getsize(p), 'bytes', len(out['customers']), 'customers',
          'with_opps', sum(1 for x in out['customers'].values() if x['opps']),
          'with_activities', sum(1 for x in out['customers'].values() if x['activities']))

if __name__ == '__main__':
    main()
