#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""销售日报 AI 重点提炼（本地 Ollama qwen3:8b，数据不出本机）

对某一天、某个权限范围的活动原文做四类提炼：
  1 optimization   优化项目进度
  2 key_tests      大客户/重点客户测试进度及反馈
  3 complaints     客诉进度
  4 opportunities  新商机提炼

输出：数据/daily_review_digests/<date>_<scope>.json
"""
import os, re, json, time, urllib.request, hashlib
from datetime import datetime, timedelta

import daily_review_api as dr

ROOT = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DIGEST_DIR = os.path.join(ROOT, '数据', 'daily_review_digests')
CUSTOMER360 = os.path.join(ROOT, '数据', 'customer_360.json')
OLLAMA = 'http://127.0.0.1:11434/api/chat'
MODEL = 'qwen3:8b'
VERSION = 'daily-review-digest-v1'

CATS = [
    ('optimization', '优化项目进度'),
    ('key_tests', '大客户 / 重点客户测试进度及反馈'),
    ('complaints', '客诉进度'),
    ('opportunities', '新商机提炼'),
]


def _scope_key(au):
    role = (au.get('role') or '').lower()
    name = au.get('name') or ''
    if role in ('admin', 'gm', 'director', 'readonly'):
        return 'all'
    if role == 'manager':
        return 'dept_' + re.sub(r'[^0-9A-Za-z\u4e00-\u9fa5]+', '_', au.get('dept') or 'unknown')
    return 'sales_' + re.sub(r'[^0-9A-Za-z\u4e00-\u9fa5]+', '_', name or 'unknown')


def _scope_label(au):
    role = (au.get('role') or '').lower()
    if role in ('admin', 'gm', 'director', 'readonly'):
        return '全量'
    if role == 'manager':
        return (au.get('dept') or '') + '（本部门）'
    return (au.get('name') or '') + '（本人）'


def _can_generate(au):
    return (au.get('role') or '').lower() in ('admin', 'gm', 'director', 'manager', 'sales')


def _path(au, day):
    os.makedirs(DIGEST_DIR, exist_ok=True)
    return os.path.join(DIGEST_DIR, '%s_%s.json' % (day, _scope_key(au)))


def _key_customer_names():
    try:
        d = json.load(open(CUSTOMER360, encoding='utf-8'))
        out = set()
        for c in (d.get('customers') or []):
            if c.get('level') in ('大客户', '重点客户') and c.get('name'):
                out.add(c['name'])
        for g in (d.get('groups') or []):
            if g.get('level') in ('大客户', '重点客户') and g.get('name'):
                out.add(g['name'])
        return out
    except Exception:
        return set()


def _activity_rows(au, day):
    payload = dr.list_payload(au, {'date': day})
    rows = []
    for g in (payload.get('groups') or []):
        rows.extend(g.get('activities') or [])
    return payload, rows


def _prompt(au, day, rows):
    key_names = _key_customer_names()
    key_in_day = sorted({r.get('customer') for r in rows if r.get('customer') in key_names})
    lines = []
    for r in rows:
        txt = (r.get('text') or '').strip().replace('\r', '')
        if len(txt) > 240:
            txt = txt[:240] + '…'
        lines.append('[%s] 销售：%s｜客户：%s｜类型：%s\n%s' % (
            r.get('id'), r.get('sales'), r.get('customer'), r.get('type'), txt))
    body = '\n\n'.join(lines)
    if len(body) > 20000:
        body = body[:20000] + '\n…（后面内容已截断，仅用于本期提炼）'
    system = """你是宝锐生物销售管理分析员。你只依据用户提供的活动原文做提炼，不补充原文没有的信息，不编造客户、金额、日期、结论。
把同一天的活动按四类整理，每条必须带 evidence_ids（对应输入里的活动 ID）。没有证据就不要输出该条。
四类定义：
1 optimization：配方/体系/工艺/性能优化、迭代、改善、稳定性、灵敏度、LOD、检出限等进展，写清客户、项目、当前进展、下一步。
2 key_tests：大客户/重点客户的测试、验证、试用、对比、反馈；写清测试对象、结果、客户反馈、下一步。
3 complaints：客诉、投诉、异常、退换、批退、污染、起线、翘尾、非特异、不合格等处理进度；写清问题、当前状态、责任动作。
4 opportunities：新出现的项目需求、采购意向、立项、替换、扩展、合作机会；写清客户、需求、阶段、下一步。常规寒暄和重复跟进不要算新商机。
输出严格 JSON，不要 markdown，不要解释。结构：
{"optimization":[{"title":"","customer":"","project":"","progress":"","next_action":"","sales":"","evidence_ids":[""],"confidence":"high|medium|low"}],
 "key_tests":[...], "complaints":[...], "opportunities":[...]}
每类最多 4 条，只保留最重要的；合并同一客户同一项目的重复信息；confidence 只能是 high/medium/low。"""
    user = ("日期：%s\n\n属于大客户/重点客户的当日客户（供判断，不要仅凭此归类）：%s\n\n活动原文：\n%s" %
            (day, '、'.join(key_in_day) or '无', body))
    return system, user


def _call_ollama(system, user):
    body = {
        'model': MODEL,
        'messages': [{'role': 'system', 'content': system}, {'role': 'user', 'content': user}],
        'stream': False,
        'think': False,
        'options': {'temperature': 0.1, 'num_predict': 1800, 'num_ctx': 24576},
        'format': 'json',
    }
    req = urllib.request.Request(OLLAMA, data=json.dumps(body).encode('utf-8'),
                                 headers={'Content-Type': 'application/json'}, method='POST')
    with urllib.request.urlopen(req, timeout=240) as r:
        d = json.loads(r.read())
    msg = (d.get('message') or {})
    return (msg.get('content') or '').strip()


def _parse_json(s):
    if not s:
        return None
    s = s.strip()
    s = re.sub(r'^```(?:json)?\s*', '', s, flags=re.I)
    s = re.sub(r'\s*```$', '', s)
    a, b = s.find('{'), s.rfind('}')
    if a < 0 or b <= a:
        return None
    try:
        return json.loads(s[a:b + 1])
    except Exception:
        return None


def _evidence_map(rows):
    return {r.get('id'): r for r in rows if r.get('id')}


def _normalize(raw, rows):
    emap = _evidence_map(rows)
    out = {}
    for key, _label in CATS:
        items = raw.get(key) if isinstance(raw, dict) else None
        if not isinstance(items, list):
            items = []
        clean = []
        for it in items:
            if not isinstance(it, dict):
                continue
            ids = it.get('evidence_ids') if isinstance(it.get('evidence_ids'), list) else []
            ids = [str(x) for x in ids if str(x) in emap]
            # 去重、限制证据数量
            ids = list(dict.fromkeys(ids))[:5]
            if not ids:
                continue
            ev = []
            for i in ids:
                r = emap[i]
                ev.append({'id': i, 'sales': r.get('sales') or '', 'customer': r.get('customer') or '',
                           'snippet': (r.get('text') or '')[:180]})
            rec = {
                'title': str(it.get('title') or '').strip()[:80],
                'customer': str(it.get('customer') or '').strip()[:80],
                'project': str(it.get('project') or '').strip()[:100],
                'progress': str(it.get('progress') or '').strip()[:400],
                'next_action': str(it.get('next_action') or '').strip()[:240],
                'sales': str(it.get('sales') or '').strip()[:40],
                'confidence': it.get('confidence') if it.get('confidence') in ('high', 'medium', 'low') else 'medium',
                'evidence': ev,
                'evidence_ids': ids,
            }
            if not rec['title']:
                rec['title'] = rec['customer'] or (ev[0]['snippet'][:36] if ev else '未命名')
            clean.append(rec)
        out[key] = clean[:4]
    return out


def _rule_fallback(rows):
    """本地模型不可用时的保守抽取：只摘原文，不生成结论。"""
    def has(t, words):
        return any(w in t for w in words)
    rule = {
        'optimization': ['优化', '迭代', '改善', '调整', '提升', '灵敏度', 'LOD', '检出限', '配方', '体系', '稳定性', '性能'],
        'key_tests': ['测试', '验证', '试用', '对比', '反馈', '结果', '上机', '评估'],
        'complaints': ['客诉', '投诉', '异常', '退换', '退货', '批间差', '污染', '起线', '翘尾', '非特异', '不合格', '漏检', '召回'],
        'opportunities': ['新项目', '需求', '商机', '机会', '采购', '立项', '计划', '准备', '合作', '替换', '扩展', '导入'],
    }
    out = {k: [] for k, _ in CATS}
    for key, words in rule.items():
        for r in rows:
            t = r.get('text') or ''
            if not has(t, words):
                continue
            # 只做保守摘录：最多 120 字，不推断
            title = (r.get('customer') or '') + ' · ' + (r.get('type') or '活动')
            out[key].append({
                'title': title[:80], 'customer': r.get('customer') or '',
                'project': '', 'progress': t[:120], 'next_action': '',
                'sales': r.get('sales') or '', 'confidence': 'low',
                'evidence': [{'id': r.get('id'), 'sales': r.get('sales') or '', 'customer': r.get('customer') or '', 'snippet': t[:180]}],
                'evidence_ids': [r.get('id')],
            })
            if len(out[key]) >= 8:
                break
    return out


def get_digest(au, day):
    path = _path(au, day)
    if not os.path.exists(path):
        return {'ok': True, 'exists': False, 'date': day, 'scope_label': _scope_label(au),
                'can_generate': _can_generate(au)}
    try:
        d = json.load(open(path, encoding='utf-8'))
        d['ok'] = True
        d['exists'] = True
        d['can_generate'] = _can_generate(au)
        return d
    except Exception:
        return {'ok': True, 'exists': False, 'date': day, 'scope_label': _scope_label(au),
                'can_generate': _can_generate(au)}


def generate_digest(au, day):
    if not _can_generate(au):
        return None, '无生成权限（仅主管/经理/总监/管理员）'
    payload, rows = _activity_rows(au, day)
    if not rows:
        return None, '该日期没有活动原文'
    system, user = _prompt(au, day, rows)
    raw, err = None, None
    try:
        content = _call_ollama(system, user)
        raw = _parse_json(content)
        if not raw:
            err = '本地模型返回不是合法 JSON'
    except Exception as e:
        err = '本地模型调用失败：' + str(e)[:160]
    if raw:
        cats = _normalize(raw, rows)
        engine = 'ollama:' + MODEL
    else:
        cats = _rule_fallback(rows)
        engine = 'rule-fallback'
    d = {
        'version': VERSION,
        'date': day,
        'scope': _scope_key(au),
        'scope_label': _scope_label(au),
        'generated_at': datetime.now().isoformat(timespec='seconds'),
        'engine': engine,
        'model': MODEL if engine.startswith('ollama:') else '',
        'source_count': len(rows),
        'source_hash': hashlib.md5(('%s|%s' % (day, len(rows))).encode()).hexdigest()[:12],
        'error': err or '',
        'categories': cats,
        'note': 'AI 提炼仅作导航，事实以活动原文为准；每条均附 evidence_ids。',
    }
    path = _path(au, day)
    tmp = path + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(d, f, ensure_ascii=False, indent=1)
    os.replace(tmp, path)
    return d, None
