#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""飞书 MCP → 拜访与活动基础数据（带上真正的「活动类型」字段）。

背景：之前 `daily_activities_mcp.json` 只有 名称/描述/客户/创建者/创建时间 —— **没有活动类型**，
所以线上下只能靠文本猜（只有 22.5% 可判定）。用户指出「活动类型 = 活动形式」，
飞书里确实有 `field_76654e 活动类型`（线上跟进 / 线下拜访 / 陌拜 / 协同拜访）。

本次直接按字段拉，不再猜。

用法：python3 脚本/pull_activities_mcp.py [2026-07 2026-08 2026-09]
输出：数据/activities_feishu.json
"""
import io
import json
import math
import os
import re
import subprocess
import sys
import time
from collections import Counter

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
OUT = os.path.join(ROOT, '数据', 'activities_feishu.json')
PIPELINE = os.path.join(ROOT, '数据', 'mcp_activities_pipeline.py')
PK = '6593cd71471290e3cc6be6e6'
START_MONTH = '2026-07'   # 固定起点：Q3 起（重跑季度报告必须含 7 月）


def _default_months():
    """默认窗口 = START_MONTH ~ 当前月（含当月）。
    旧实现写死 ['2026-07','2026-08','2026-09']，跨到 10 月后当月活动拉不到，
    拜访视图/待办生成会缺当月数据；这里改成动态补到当月，且不动 Q3 起点。"""
    y, m = int(START_MONTH[:4]), int(START_MONTH[5:7])
    ny, nm = time.localtime().tm_year, time.localtime().tm_mon
    out = []
    while (y, m) <= (ny, nm):
        out.append('%04d-%02d' % (y, m))
        m += 1
        if m > 12:
            y, m = y + 1, 1
    return out


MONTHS = [a for a in sys.argv[1:] if re.match(r'^\d{4}-\d{2}$', a)] or _default_months()
OWNER_MERGE = {'张立娅': '刘子研', '黄明月': '刘子研'}   # 离职归并，同 cron 刷新任务 Step 1 口径

# 活动类型（field_76654e）→ 归一渠道。陌拜=上门初访（线下），协同拜访=与同事同去（线下）。
TYPE_MAP = {
    '线下拜访': 'offline',
    '线上跟进': 'online',
    '协同拜访': 'joint',
    '陌拜(指线索及客户板块均无客户信息，属于初次开发)': 'cold',
    '陌拜': 'cold',
}


def token():
    """从既有管道脚本取 MCP token，避免把凭据复制进新文件。"""
    src = io.open(PIPELINE, encoding='utf-8').read()
    m = re.search(r'TK\s*=\s*"([^"]+)"', src)
    if not m:
        raise SystemExit('❌ 未能从 %s 取到 MCP token' % PIPELINE)
    return m.group(1)


TK = token()


def mcp(method, args, timeout=60):
    """调 MCP。⚠️ 不要用 `--resolve` 钉 IP：2026-10-22 实测旧 IP 120.233.177.47
    已失效，curl 会静默挂到超时（45s、空输出），整条活动拉取管道崩在第一个月。
    走系统 DNS 正常（同一查询 1.2s 返回）。"""
    cmd = ['curl', '-s', '--max-time', '55', '-X', 'POST',
           'https://project.feishu.cn/mcp_server/v1',
           '-H', 'X-Mcp-Token: %s' % TK, '-H', 'Content-Type: application/json',
           '-d', json.dumps({"jsonrpc": "2.0", "method": "tools/call",
                             "params": {"name": method, "arguments": args}, "id": 1})]
    r = None
    for attempt in range(3):
        try:
            r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
            if r.stdout.strip():
                break
        except subprocess.TimeoutExpired:
            print('     ⏱ 超时重试 %d/3' % (attempt + 1))
            time.sleep(2)
    if r is None:
        return None
    try:
        d = json.loads(r.stdout)
    except Exception:
        return None
    for c in (d.get('result') or {}).get('content') or []:
        t = c.get('text', '')
        if 'log_id' in t and not t.strip().startswith('{'):
            continue
        if t.strip().startswith('{'):
            try:
                return json.loads(t)
            except Exception:
                continue
    return None


def scalar(f):
    """按 value_type 取值，兼容各种包装。"""
    vt = f.get('value_type')
    v = f.get('value') or {}
    if vt == 'key_label_value':
        return ((v.get('key_label_value') or {}).get('label') or '').strip()
    if vt == 'user_value':
        u = v.get('user_value') or {}
        return (u.get('name_cn') or u.get('name_en') or '').strip()
    if vt == 'long_value':
        return str(v.get('long_value', ''))
    if vt == 'string_value':
        return (v.get('string_value') or '').strip()
    for k in v:                                   # 兜底：取第一个标量
        x = v[k]
        if isinstance(x, dict):
            for kk in ('label', 'name_cn', 'string_value', 'long_value'):
                if x.get(kk) not in (None, ''):
                    return str(x[kk]).strip()
        elif x not in (None, ''):
            return str(x).strip()
    return ''


def rows_of(result):
    """从返回体取出 moql_field_list 行，并压成 {字段名: 值}。"""
    out = []
    data = (result or {}).get('data')
    if isinstance(data, dict):
        groups = list(data.values())
    elif isinstance(data, list):
        groups = data
    else:
        return out
    for g in groups:
        if isinstance(g, dict):
            g = g.get('moql_field_list') or []
        if not isinstance(g, list):
            continue
        for item in g:
            if not isinstance(item, dict):
                continue
            fld = {}
            for f in item.get('moql_field_list') or []:
                fld[f.get('name') or f.get('key')] = scalar(f)
            if fld:
                out.append(fld)
    return out


def pull_month(month):
    """拉一个月（活动类型/客户/跟进部门/创建者/创建时间/描述/名称）。"""
    lo = '%s-01' % month
    import calendar
    hi = '%s-%02d' % (month, calendar.monthrange(int(month[:4]), int(month[5:7]))[1])
    mql = ("SELECT `名称`, `活动类型`, `活动关联客户`, `跟进部门`, `创建者`, `创建时间`, "
           "`活动描述`, work_item_id FROM `销售管理`.`活动` "
           "WHERE `创建时间` between '%s' and '%s' ORDER BY `创建时间` DESC" % (lo, hi))
    print('  ── %s（%s ~ %s）' % (month, lo, hi))
    res = mcp('search_by_mql', {'project_key': PK, 'mql': mql})
    if not res:
        print('     ❌ 首页无返回')
        return []
    rows = rows_of(res)
    sid = res.get('session_id')
    lst = (res.get('list') or [{}])[0]
    count = lst.get('count') or len(rows)
    gid = ((lst.get('group_infos') or [{}])[0].get('group_id')) or '1'
    pages = max(1, int(math.ceil(count / 50.0)))
    print('     总 %d 条 / %d 页' % (count, pages))
    for p in range(2, pages + 1):
        r2 = mcp('search_by_mql', {'project_key': PK, 'session_id': sid,
                                   'group_pagination_list': [{'group_id': gid, 'page_num': p}]})
        got = rows_of(r2) if r2 else []
        if not got:
            print('     第 %d 页空，提前收尾' % p)
            break
        rows.extend(got)
        if p % 10 == 0 or p == pages:
            print('     第 %d/%d 页 ｜ 累计 %d' % (p, pages, len(rows)))
        time.sleep(0.15)
    # 去重（同一 work_item_id 可能跨页重复）
    seen, uniq = set(), []
    for r in rows:
        wid = r.get('工作项id') or ''
        if wid and wid in seen:
            continue
        if wid:
            seen.add(wid)
        uniq.append(r)
    print('     去重后 %d 条' % len(uniq))
    return uniq


def main():
    print('飞书「活动」全字段拉取 ｜ 月份 %s\n' % MONTHS)
    items, cnt = [], Counter()
    for m in MONTHS:
        for r in pull_month(m):
            atype = (r.get('活动类型') or '').strip()
            ch = TYPE_MAP.get(atype, 'none')
            who = (r.get('创建者') or '').strip()
            who = OWNER_MERGE.get(who, who)
            items.append({
                'id': 'FS%s' % (r.get('工作项id') or ''),
                'date': (r.get('创建时间') or '')[:10],
                'submitted_by': who,
                'sales': who,
                'customer': (r.get('活动关联客户') or '').strip(),
                'channel': ch,
                'atype': atype,
                'dept_raw': (r.get('跟进部门') or '').strip(),
                'type': (r.get('名称') or '').strip(),
                'src': 'feishu',
                'text': (r.get('活动描述') or '')[:1500],
            })
            cnt[ch] += 1
    items.sort(key=lambda r: (r['date'], r['id']), reverse=True)

    tot = len(items)
    payload = {
        'source': 'feishu_mcp_activity_type', 'generated': time.strftime('%Y-%m-%d %H:%M:%S'),
        'months': MONTHS, 'total': tot,
        'channel_breakdown': {k: cnt[k] for k in ('offline', 'online', 'cold', 'joint', 'none')},
        'type_raw': dict(Counter(r['atype'] or '(未填)' for r in items)),
        'owner_merge': OWNER_MERGE,
        'note': '活动形式取自飞书「活动类型」字段（field_76654e），非文本推断；陌拜/协同拜访归线下',
        'items': items,
    }
    io.open(OUT, 'w', encoding='utf-8').write(json.dumps(payload, ensure_ascii=False, indent=0))
    print('\n输出 %s（%.1f KB）' % (OUT, os.path.getsize(OUT) / 1024.0))
    print('  月份 %s ｜ 条数 %d' % (MONTHS, tot))
    print('  渠道：线下拜访 %d ｜ 线上跟进 %d ｜ 陌拜 %d ｜ 协同拜访 %d ｜ 未填 %d'
          % (cnt['offline'], cnt['online'], cnt['cold'], cnt['joint'], cnt['none']))
    print('  活动类型原始分布：')
    for k, v in Counter(r['atype'] or '(未填)' for r in items).most_common():
        print('     %-46s %d' % (k[:44], v))
    print('  归属人 %d 位 ｜ 客户 %d 家 ｜ 无客户 %d 条'
          % (len({r['submitted_by'] for r in items}), len({r['customer'] for r in items if r['customer']}),
             sum(1 for r in items if not r['customer'])))
    print('  飞书跟进部门分布：', dict(Counter(r['dept_raw'] or '(未填)' for r in items).most_common(8)))


main()
