#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""飞书「活动」→ 数据/daily_activities_mcp.json 增量刷新（保留历史，带「活动类型」）。

与 pull_activities_mcp.py 的分工：
  · pull_activities_mcp.py  → activities_feishu.json（拜访视图，映射后字段）
  · 本脚本                  → daily_activities_mcp.json（AI助手/客户360/同比用的原始字段行）

硬规则（血泪教训，勿改）：
  1. 必须 SELECT `活动类型` field_76654e —— 线下/线上次数唯一口径，禁文本猜。
  2. MCP search_by_mql 的 LIMIT..OFFSET 硬上限 1000：必须按 7 天分段拉，段内 OFFSET ≤1000，
     禁止单条 SQL 一次 OFFSET 翻到底（会静默截断成 1050 行零头）。
  3. 该文件是 2024-05 起的全量历史（由 merge_activity_full_export.py 合并导出补齐），
     刷新窗口只覆盖 start_time >= WINDOW_START 的部分 → 必须 **按 work_item_id upsert**，
     禁止整文件重写（否则 3.8 万条历史活动被清空，AI助手/客户360 背景检索直接空）。
  4. 条数低于 MIN_OK → 拒绝写盘（保留旧文件），exit 1。

用法: /usr/bin/python3 数据/pull_daily_activities.py [--window 2026-07-01]
"""
import json
import os
import shutil
import subprocess
import sys
import time
from datetime import datetime, timedelta, date

BASE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(BASE, 'daily_activities_mcp.json')
TK = 'm-abfb29e8-3104-434f-9944-8d0bb592f8cd'
PK = '6593cd71471290e3cc6be6e6'          # 销售管理
COLUMNS = ('name, start_time, field_b99055, field_5f20fc, field_76654e, '
           'field_5ed7ab, owner, work_item_id')
WINDOW_START = '2026-07-01'
MIN_OK = 4000                              # 窗口内正常 ≥7000 条；低于此数视为 MCP 大面积失败
TODAY = date.today()

if '--window' in sys.argv:
    WINDOW_START = sys.argv[sys.argv.index('--window') + 1]


def mcp(method, args, timeout=60):
    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})]
    out = None
    for _ in range(3):
        try:
            r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
            if r.stdout.strip():
                out = r.stdout
                break
        except subprocess.TimeoutExpired:
            print('     ⏱ 超时重试')
            time.sleep(2)
    if not out:
        return None
    try:
        d = json.loads(out)
    except Exception:
        return None
    for c in (d.get('result') or {}).get('content') or []:
        t = (c.get('text') or '').strip()
        if not t.startswith('{'):
            continue
        try:
            return json.loads(t)
        except Exception:
            continue
    return None


def scalar(f):
    """返回值必须是**扁平化对象**，与历史文件一致：
        key_label_value → {"key","label"} ｜ user_value → {"user_key","email","name_cn"...}
        long_value → int ｜ string_value → str
    ⚠️ 直出 label 字符串会破坏下游 `x.get('label')` 取值（2026-10-28 踩过：
       活动类型写成纯字符串 → 类型覆盖自检 0.0%，拜访视图类型全丢）。
    """
    vt = f.get('value_type')
    v = f.get('value') or {}
    if vt == 'key_label_value':
        return v.get('key_label_value') or {}
    if vt == 'user_value':
        return v.get('user_value') or {}
    if vt == 'long_value':
        return v.get('long_value', '')
    if vt == 'string_value':
        return (v.get('string_value') or '').strip()
    for k in ('key_label_value', 'user_value', 'long_value', 'string_value'):
        if k in v:
            return v[k]
    return ''


def parse_rows(result):
    rows = []
    data = (result or {}).get('data')
    groups = list(data.values()) if isinstance(data, dict) else (data or [])
    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:
            fld = {}
            for f in (item.get('moql_field_list') or []):
                fld[f.get('key') or f.get('name')] = scalar(f)
            if fld:
                rows.append(fld)
    return rows


def pull_segment(start, end):
    """拉 [start, end) 段，段内 OFFSET 翻页，硬上限 1000。"""
    where = ["start_time >= '%s'" % start]
    if end:
        where.append("start_time < '%s'" % end)
    mql = ('SELECT %s FROM `销售管理`.`活动` WHERE %s ORDER BY start_time DESC'
           % (COLUMNS, ' AND '.join(where)))
    items, seen, offset = [], set(), 0
    while offset <= 1000:
        res = mcp('search_by_mql', {'project_key': PK,
                                    'mql': '%s LIMIT 50 OFFSET %d' % (mql, offset)})
        rows = parse_rows(res) if res else []
        if not rows:
            break
        for it in rows:
            wid = str(it.get('work_item_id') or '')
            if wid and wid in seen:
                continue
            if wid:
                seen.add(wid)
            items.append(it)
        if len(rows) < 50:
            break
        offset += 50
        time.sleep(0.2)
    return items


def chunks(start):
    out, d = [], datetime.strptime(start, '%Y-%m-%d').date()
    while d < TODAY:
        nxt = min(d + timedelta(days=7), TODAY)
        out.append((d.isoformat(), nxt.isoformat() if nxt < TODAY else None))
        d = nxt
    return out


def main():
    print('=' * 62)
    print('活动增量刷新 daily_activities_mcp.json ｜ 窗口 %s ~ now' % WINDOW_START)
    print('=' * 62)
    fresh, total_seg = [], len(chunks(WINDOW_START))
    for i, (s, e) in enumerate(chunks(WINDOW_START), 1):
        got = pull_segment(s, e)
        fresh.extend(got)
        if i % 5 == 0 or i == total_seg:
            print('  段 %d/%d %s~%s: +%d（累计 %d）' % (i, total_seg, s, e or 'now', len(got), len(fresh)))

    # 全局去重
    seen, win = set(), []
    for it in fresh:
        wid = str(it.get('work_item_id') or '')
        if wid and wid in seen:
            continue
        if wid:
            seen.add(wid)
        win.append(it)
    print('  窗口内 %d 条（work_item_id 去重后）' % len(win))

    if len(win) < MIN_OK:
        print('🔴 仅 %d 条（基准 ≥%d）→ 拒绝写盘，保留旧文件' % (len(win), MIN_OK))
        sys.exit(1)

    old = {"updated": '', "items": []}
    if os.path.exists(OUT):
        old = json.load(open(OUT, encoding='utf-8'))
    old_items = old.get('items') or []
    by_id = {str(x.get('work_item_id')): x for x in old_items if x.get('work_item_id')}
    n_new = n_upd = 0
    for it in win:
        wid = str(it.get('work_item_id') or '')
        if wid in by_id:
            merged = dict(by_id[wid])
            merged.update({k: v for k, v in it.items() if v not in ('', None)})
            by_id[wid] = merged
            n_upd += 1
        else:
            by_id[wid] = it
            n_new += 1
    merged_items = list(by_id.values())
    merged_items.sort(key=lambda x: str(x.get('start_time') or ''), reverse=True)

    # 活动类型覆盖自检
    typed = sum(1 for x in merged_items
                if isinstance(x.get('field_76654e'), dict) and x['field_76654e'].get('label'))
    payload = {'updated': time.strftime('%Y-%m-%d %H:%M:%S'),
               'total': len(merged_items), 'items': merged_items}

    tmp = OUT + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(payload, f, ensure_ascii=False)
    shutil.copy2(OUT, OUT + '.bak-' + time.strftime('%Y%m%d_%H%M%S')) if os.path.exists(OUT) else None
    os.replace(tmp, OUT)
    print('  ✅ 写入 %s' % OUT)
    print('     总 %d 条（原 %d ｜ 新增 %d ｜ 更新 %d）｜ 活动类型已填 %d 条 (%.1f%%)'
          % (len(merged_items), len(old_items), n_new, n_upd, typed,
             100.0 * typed / max(1, len(merged_items))))
    print('     文件 %.1f MB' % (os.path.getsize(OUT) / 1024.0 / 1024.0))


if __name__ == '__main__':
    main()
