#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把「结构化活动录入」相关接口注入 脚本/销售工作台服务.py（幂等，可重复执行）"""
import io, os, sys

P = os.path.expanduser('~/Desktop/Hermes输出-工作类/脚本/销售工作台服务.py')
s = io.open(P, encoding='utf-8').read()

if '_act_feishu_text' in s:
    print('  已注入过，跳过'); sys.exit(0)

# ── ① import re ──
a = 'import os\n'
assert s.count(a) == 1
s = s.replace(a, 'import os\nimport re\n', 1)

# ── ② 模块级：活动数据/附件/飞书文本/CloudBase ──
MOD = '''# ══════════════════ 结构化活动录入（项目跟进 / 采购跟进）══════════════════
ACT_DIR = os.path.join(DATA_DIR, 'activities')
ACT_ATTACH_DIR = os.path.join(DATA_DIR, '活动附件')
ACT_ORDERS_INDEX = os.path.join(DATA_DIR, 'orders_2026_index.json')
ACT_CB_COLLECTION = 'activities'
ACT_LOCK = threading.Lock()
_ACT_CACHE = {'orders': None, 'mtime': 0}


def _act_orders():
    """2026 订单索引（懒加载；文件变更自动失效）"""
    try:
        mt = os.path.getmtime(ACT_ORDERS_INDEX)
    except Exception:
        return []
    if _ACT_CACHE['orders'] is None or _ACT_CACHE['mtime'] != mt:
        try:
            with open(ACT_ORDERS_INDEX, encoding='utf-8') as f:
                _ACT_CACHE['orders'] = (json.load(f) or {}).get('orders') or []
            _ACT_CACHE['mtime'] = mt
        except Exception:
            _ACT_CACHE['orders'] = []
    return _ACT_CACHE['orders']


def _act_visible_sales(user):
    """可查看的销售员姓名集合；None = 不限（admin/gm）"""
    role = (user or {}).get('role') or 'sales'
    name = (user or {}).get('name') or ''
    if role in ('admin', 'gm'):
        return None
    if role == 'manager':
        mem = _dept_members((user or {}).get('dept') or '')
        return (mem | {name}) if mem else {name}
    return {name}


def _act_can_see(user, rec):
    vis = _act_visible_sales(user)
    if vis is None:
        return True
    return (rec.get('submitted_by') in vis) or (rec.get('sales') in vis)


def _mp_parse(raw, boundary):
    """极简 multipart 解析 -> (files[(filename, bytes)], fields{name: str})"""
    files, fields = [], {}
    try:
        parts = raw.split(('--' + boundary).encode())
    except Exception:
        return files, fields
    for part in parts[1:]:
        if part.startswith(b'--'):
            continue
        if part.startswith(b'\\r\\n'):
            part = part[2:]
        if part.endswith(b'\\r\\n'):
            part = part[:-2]
        h = part.find(b'\\r\\n\\r\\n')
        if h < 0:
            continue
        head = part[:h].decode('utf-8', errors='replace')
        body = part[h + 4:]
        mf = re.search(r'filename="([^"]*)"', head)
        mn = re.search(r'name="([^"]*)"', head)
        if mf and mf.group(1):
            files.append((mf.group(1), body))
        elif mn:
            fields[mn.group(1)] = body.decode('utf-8', errors='replace').strip()
    return files, fields


def _tcb_storage_upload(local_path, cloud_path):
    """经 CLI 上传到 CloudBase 存储桶 -> (ok, msg)"""
    try:
        r = subprocess.run(['node', TCB_CLI, 'storage', 'upload', local_path, cloud_path, '-e', CB_ENV],
                           capture_output=True, text=True, timeout=300)
        ok = (r.returncode == 0) and ('成功' in (r.stdout or ''))
        return ok, ((r.stdout or '') + (r.stderr or ''))[-300:]
    except Exception as e:
        return False, str(e)


def _tcb_storage_url(cloud_path):
    """签发 CloudBase 存储临时 URL（约 1 小时）"""
    try:
        r = subprocess.run(['node', TCB_CLI, 'storage', 'url', cloud_path, '-e', CB_ENV, '--json'],
                           capture_output=True, text=True, timeout=90)
        m = re.search(r'\\{[\\s\\S]*\\}', r.stdout or '')
        if m:
            return ((json.loads(m.group(0)).get('data') or {}).get('url') or '')
    except Exception:
        pass
    return ''


def _act_feishu_text(rec):
    """生成可粘贴到飞书的结构化描述（唯一来源：本地记录与前端共用）"""
    L = []
    if rec.get('type'):
        L.append('【活动类型】%s%s' % (rec['type'], ('｜' + rec['subtype']) if rec.get('subtype') else ''))
    for k, lbl in (('customer', '客户'), ('contact', '联系人'), ('date', '活动日期')):
        if rec.get(k):
            L.append('【%s】%s' % (lbl, rec[k]))
    if rec.get('type') == '采购跟进':
        if rec.get('visit_goal'):
            L.append('【拜访目标】%s' % rec['visit_goal'])
        if rec.get('visit_content'):
            L.append('【拜访内容】\\n%s' % rec['visit_content'])
    else:
        if rec.get('need'):
            L.append('【客户需求】\\n%s' % rec['need'])
        if rec.get('params'):
            L.append('【技术参数 / 测试数据】')
            for x in rec['params']:
                L.append('- %s：%s' % (x.get('sku') or '', x.get('desc') or ''))
        t = rec.get('test_order') or {}
        if t.get('no'):
            L.append('【测试单结果跟进】订单 %s%s' % (t['no'], ('（%s）' % t['customer']) if t.get('customer') else ''))
            for x in (t.get('items') or []):
                att = x.get('attachments') or []
                L.append('- %s%s：%s%s' % (x.get('sku') or '', (' ' + x['name']) if x.get('name') else '',
                                          x.get('result') or '',
                                          ('｜附件 %d 个：%s' % (len(att), '、'.join(a.get('name') or '' for a in att))) if att else ''))
        if rec.get('issue'):
            L.append('【问题现象】%s' % rec['issue'])
        if rec.get('quote'):
            L.append('【客户原话】%s' % rec['quote'])
        if rec.get('feedback'):
            L.append('【客户反馈】%s' % rec['feedback'])
        comp = rec.get('competitors') or []
        if comp:
            L.append('【竞对信息】')
            for c in comp:
                L.append('- %s%s：%s%s' % (c.get('brand') or '', ('（' + c['sku'] + '）') if c.get('sku') else '',
                                          c.get('perf') or '',
                                          ('｜客户评价：' + c['feedback']) if c.get('feedback') else ''))
    plan = rec.get('plan') or []
    if plan:
        L.append('【下一步计划】')
        for i, p in enumerate(plan, 1):
            L.append('%d. %s（%s / %s）' % (i, p.get('item') or '', p.get('owner') or '未定', p.get('due') or '未定'))
    return '\\n'.join([x for x in L if x])


def _act_save(rec):
    os.makedirs(ACT_DIR, exist_ok=True)
    with ACT_LOCK:
        with open(os.path.join(ACT_DIR, rec['id'] + '.json'), 'w', encoding='utf-8') as f:
            json.dump(rec, f, ensure_ascii=False, indent=1)
        idx_f = os.path.join(ACT_DIR, '_index.json')
        try:
            with open(idx_f, encoding='utf-8') as f:
                idx = json.load(f)
            if not isinstance(idx, list):
                idx = []
        except Exception:
            idx = []
        idx = [x for x in idx if x.get('id') != rec['id']]
        idx.append({k: rec.get(k) for k in ('id', 'created', 'submitted_by', 'sales', 'type', 'subtype',
                                            'customer', 'contact', 'date', 'order_no', 'sku_count', 'att_count')})
        idx.sort(key=lambda x: x.get('created') or '', reverse=True)
        with open(idx_f, 'w', encoding='utf-8') as f:
            json.dump(idx, f, ensure_ascii=False, indent=1)


def _act_cb_insert(rec):
    """写入 CloudBase activities 集合（失败不阻断本地落盘）"""
    try:
        cmd_obj = {'TableName': ACT_CB_COLLECTION, 'CommandType': 'ADD',
                   'Command': json.dumps({'insert': ACT_CB_COLLECTION, 'documents': [rec]}, ensure_ascii=False)}
        cmd = json.dumps([cmd_obj], ensure_ascii=False)
        r = subprocess.run(['node', TCB_CLI, 'db', 'nosql', 'execute', '--command', cmd, '--json', '-e', CB_ENV],
                           capture_output=True, text=True, timeout=120)
        out = ((r.stdout or '') + (r.stderr or '')).strip()
        ok = (r.returncode == 0) and ('ok' in out.lower()) and ('false' not in out.lower()[:200]) and ('error' not in out.lower()[:200])
        return ok, out[-300:]
    except Exception as e:
        return False, str(e)


def _act_load_index():
    try:
        with open(os.path.join(ACT_DIR, '_index.json'), encoding='utf-8') as f:
            idx = json.load(f)
        return idx if isinstance(idx, list) else []
    except Exception:
        return []


def _price_scope_of(user):'''

anchor = 'def _price_scope_of(user):'
assert s.count(anchor) == 1
s = s.replace(anchor, MOD, 1)

# ── ③ do_GET 路由 ──
GET_ROUTES = '''        # ══════════ 活动录入：订单 / SKU / 列表 / 详情 / 附件地址 ══════════
        if path.startswith('/api/activity/'):
            _au = self._auth_user()
            if not _au:
                self._send_json({'ok': False, 'error': '登录已过期，请重新登录'}, 401)
                return
            from urllib.parse import parse_qs
            _qs = {k: v[0] for k, v in parse_qs(self.path.split('?', 1)[1]).items()} if '?' in self.path else {}

            if path == '/api/activity/orders':
                # 订单下拉：按登录人权限过滤（销售只看到自己名下订单）
                q = (_qs.get('q') or '').strip().upper()
                lim = max(1, min(500, int(_qs.get('limit') or 200)))
                vis = _act_visible_sales(_au)
                out = []
                for o in _act_orders():
                    if vis is not None and (o.get('sales') not in vis):
                        continue
                    if q and q not in ((o.get('no') or '') + ' ' + (o.get('customer') or '')).upper():
                        continue
                    out.append({'no': o['no'], 'customer': o.get('customer', ''), 'sales': o.get('sales', ''),
                                'date': o.get('date', ''), 'is_test': o.get('is_test', False),
                                'sku_count': len(o.get('skus') or [])})
                    if len(out) >= lim:
                        break
                self._send_json({'ok': True, 'total': len(out), 'orders': out})
                return

            if path == '/api/activity/order_skus':
                no = (_qs.get('no') or '').strip()
                vis = _act_visible_sales(_au)
                for o in _act_orders():
                    if o.get('no') == no:
                        if vis is not None and o.get('sales') not in vis:
                            self._send_json({'ok': False, 'error': '无权查看该订单'}, 403)
                            return
                        self._send_json({'ok': True, 'order': {
                            'no': o.get('no'), 'customer': o.get('customer', ''), 'sales': o.get('sales', ''),
                            'date': o.get('date', ''), 'is_test': o.get('is_test', False),
                            'skus': o.get('skus') or []}})
                        return
                self._send_json({'ok': False, 'error': '未找到该订单'}, 404)
                return

            if path == '/api/activity/list':
                vis = _act_visible_sales(_au)
                rows = [x for x in _act_load_index()
                        if vis is None or (x.get('submitted_by') in vis) or (x.get('sales') in vis)]
                self._send_json({'ok': True, 'total': len(rows), 'activities': rows[:500]})
                return

            if path == '/api/activity/detail':
                rid = os.path.basename((_qs.get('id') or '').strip())
                try:
                    with open(os.path.join(ACT_DIR, rid + '.json'), encoding='utf-8') as f:
                        rec = json.load(f)
                except Exception:
                    self._send_json({'ok': False, 'error': '未找到该活动'}, 404)
                    return
                if not _act_can_see(_au, rec):
                    self._send_json({'ok': False, 'error': '无权查看该活动'}, 403)
                    return
                self._send_json({'ok': True, 'activity': rec})
                return

            if path == '/api/activity/attachment_url':
                cp = (_qs.get('path') or '').strip()
                if not cp.startswith('activity-attachments/'):
                    self._send_json({'ok': False, 'error': '非法附件路径'}, 400)
                    return
                aid = cp.split('/')[1] if len(cp.split('/')) > 1 else ''
                allowed = _au.get('role') in ('admin', 'gm')
                if not allowed and aid:
                    try:
                        with open(os.path.join(ACT_DIR, os.path.basename(aid) + '.json'), encoding='utf-8') as f:
                            allowed = _act_can_see(_au, json.load(f))
                    except Exception:
                        allowed = False
                if not allowed:
                    self._send_json({'ok': False, 'error': '无权访问该附件'}, 403)
                    return
                u = _tcb_storage_url(cp)
                self._send_json({'ok': bool(u), 'url': u, 'expires': 3600,
                                 'error': '' if u else '签发临时地址失败'})
                return

            self._send_json({'ok': False, 'error': '未知活动接口'}, 404)
            return

        # ── 用户表（服务端；客户端不再直连 CloudBase user_roles）──'''

anchor2 = '        # ── 用户表（服务端；客户端不再直连 CloudBase user_roles）──'
assert s.count(anchor2) == 1
s = s.replace(anchor2, GET_ROUTES, 1)

# ── ④ do_POST 路由 ──
POST_ROUTES = '''        # ══════════ 活动录入：提交 / 附件上传 ══════════
        if path in ('/api/activity/submit', '/api/activity/attachment'):
            _au = self._auth_user()
            if not _au:
                self._send_json({'ok': False, 'error': '登录已过期，请重新登录'}, 401)
                return

            if path == '/api/activity/attachment':
                ct = self.headers.get('Content-Type', '') or ''
                if 'multipart/form-data' not in ct or 'boundary=' not in ct:
                    self._send_json({'ok': False, 'error': '需要 multipart/form-data'}, 400)
                    return
                boundary = ct.split('boundary=')[1].strip()
                if boundary.startswith('"') and boundary.endswith('"'):
                    boundary = boundary[1:-1]
                length = int(self.headers.get('Content-Length') or 0)
                files, fields = _mp_parse(self.rfile.read(length), boundary)
                if not files:
                    self._send_json({'ok': False, 'error': '未收到文件'}, 400)
                    return
                aid = re.sub(r'[^\\w\\-]', '', (fields.get('activityId') or '').strip()) or ('TMP' + datetime.now().strftime('%Y%m%d%H%M%S'))
                sku = (fields.get('sku') or '').strip()
                d = os.path.join(ACT_ATTACH_DIR, aid)
                os.makedirs(d, exist_ok=True)
                out = []
                for fname, body in files:
                    if len(body) > 30 * 1024 * 1024:
                        out.append({'name': fname, 'error': '超过 30MB 上限'})
                        continue
                    safe = re.sub(r'[^\\w\\u4e00-\\u9fa5.\\-]+', '_', fname)[-90:] or 'file'
                    local = os.path.join(d, datetime.now().strftime('%H%M%S_') + safe)
                    with open(local, 'wb') as f:
                        f.write(body)
                    cloud = 'activity-attachments/%s/%s' % (aid, os.path.basename(local))
                    ok, err = _tcb_storage_upload(local, cloud)
                    out.append({'name': fname, 'saved': os.path.basename(local), 'size': len(body), 'sku': sku,
                                'cloud_ok': ok, 'cloud_path': (cloud if ok else ''), 'err': ('' if ok else err),
                                'local': os.path.relpath(local, ROOT)})
                self._send_json({'ok': True, 'activityId': aid, 'files': out})
                return

            # ── 提交活动 ──
            length = int(self.headers.get('Content-Length') or 0)
            try:
                payload = json.loads(self.rfile.read(length).decode('utf-8') or '{}')
            except Exception as e:
                self._send_json({'ok': False, 'error': '参数解析失败: %s' % e}, 400)
                return

            typ = (payload.get('type') or '').strip()
            if typ not in ('项目跟进', '采购跟进'):
                self._send_json({'ok': False, 'error': '活动类型必须是「项目跟进」或「采购跟进」'}, 400)
                return
            plan = [p for p in (payload.get('plan') or []) if (p.get('item') or '').strip()]
            params = [p for p in (payload.get('params') or []) if (p.get('sku') or '').strip() or (p.get('desc') or '').strip()]
            t = payload.get('test_order') or {}
            items = [x for x in (t.get('items') or []) if (x.get('sku') or '').strip()]
            comps = [c for c in (payload.get('competitors') or []) if (c.get('brand') or '').strip()]
            att_all = [a for x in items for a in (x.get('attachments') or [])]
            rec = {
                'id': 'ACT' + datetime.now().strftime('%Y%m%d%H%M%S'),
                'created': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'submitted_by': _au.get('name') or '',
                'sales': (payload.get('sales') or '').strip() or (_au.get('name') or ''),
                'type': typ,
                'subtype': (payload.get('subtype') or '').strip(),
                'customer': (payload.get('customer') or '').strip(),
                'contact': (payload.get('contact') or '').strip(),
                'date': (payload.get('date') or '').strip() or datetime.now().strftime('%Y-%m-%d'),
                'order_no': (t.get('no') or '').strip(),
                'need': (payload.get('need') or '').strip(),
                'params': params,
                'test_order': ({'no': (t.get('no') or '').strip(), 'customer': (t.get('customer') or '').strip(),
                                'sales': (t.get('sales') or '').strip(), 'items': items} if (t.get('no') or '').strip() else {}),
                'issue': (payload.get('issue') or '').strip(),
                'quote': (payload.get('quote') or '').strip(),
                'feedback': (payload.get('feedback') or '').strip(),
                'competitors': comps,
                'visit_goal': (payload.get('visit_goal') or '').strip(),
                'visit_content': (payload.get('visit_content') or '').strip(),
                'plan': plan,
                'sku_count': len(items) or len(params),
                'att_count': len(att_all),
                'attachments': att_all,
            }
            rec['feishu_text'] = _act_feishu_text(rec)

            errs = []
            if not rec['customer'] and not rec['order_no']:
                errs.append('客户 或 订单编号 至少填一项')
            if not rec['plan']:
                errs.append('下一步计划至少 1 条')
            if typ == '项目跟进' and not (rec['need'] or params or rec['issue']):
                errs.append('项目跟进至少填「客户需求 / 技术参数 / 问题现象」之一')
            if typ == '采购跟进' and not (rec['visit_goal'] or rec['visit_content']):
                errs.append('采购跟进至少填「拜访目标 / 拜访内容」之一')
            if errs:
                self._send_json({'ok': False, 'error': '；'.join(errs)}, 400)
                return

            _act_save(rec)
            cb_ok, cb_err = _act_cb_insert(rec)
            self._send_json({'ok': True, 'id': rec['id'], 'feishu_text': rec['feishu_text'],
                             'cb': cb_ok, 'cb_err': ('' if cb_ok else cb_err)})
            return

        # ── 用户表维护（仅 admin/gm；写本地快照，登录以此为准）──'''

anchor3 = '        # ── 用户表维护（仅 admin/gm；写本地快照，登录以此为准）──'
assert s.count(anchor3) == 1
s = s.replace(anchor3, POST_ROUTES, 1)

io.open(P, 'w', encoding='utf-8').write(s)
print('  ✅ 已注入：模块级函数 9 个 + GET 路由 5 个 + POST 路由 2 个')
