#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文书归档员 —— 把工作台文档按销售员自动归类到 归档/<销售员>/<类型>/

归属识别：
  1. AI 生成文件（生成文件/index.json）→ owner 字段直接给
  2. 客户管理/ 历史文档 → 文件名提取客户名 → 客户→销售员映射反查
  3. 识别不到 → 归档/未归类/

归档方式：软链接（symbolic link），不移动原文件，不破坏 file_manifest.json / 前端引用。

用法：
  python3 脚本/文书归档员.py            # 全量归档（清空重建软链接）
  python3 脚本/文书归档员.py --check     # 只报告，不落盘
"""
import json, os, re, sys
from collections import defaultdict

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DATA = os.path.join(BASE, '数据')
ARCHIVE = os.path.join(BASE, '归档')
PRIVATE_DIR = os.path.expanduser('~/Library/Application Support/宝锐工作台/生成文件')
PRIVATE_INDEX = os.path.join(PRIVATE_DIR, 'index.json')
PRIVATE_FILES = os.path.join(PRIVATE_DIR, 'files')
CUST_MGMT = os.path.join(BASE, '客户管理')
AI_FILES_DIR = os.path.join(BASE, 'AI生成文件')

CHECK_ONLY = '--check' in sys.argv

# 子目录名 → 文档类型
DIR_TYPE = {
    '拜访卡': 'visit', '市场调研卡': 'market', '来访卡': 'visitor',
    '访前背调': 'prep', '拜访纪要': 'minutes',
}
TYPE_LABEL = {
    'visit': '拜访卡', 'market': '市场调研卡', 'visitor': '来访卡',
    'prep': '访前背调', 'minutes': '拜访纪要', 'report': '调研报告',
    'other': '其他',
}
# 文件名后缀 → 类型（用于散落文件）
SUFFIX_TYPE = [
    ('拜访卡', 'visit'), ('市场调研卡', 'market'), ('调研卡', 'market'),
    ('来访卡', 'visitor'), ('访前背调', 'prep'), ('拜访纪要', 'minutes'),
    ('客户调研', 'report'), ('调研报告', 'report'),
]


def load_personnel():
    """在职人员名单"""
    try:
        p = json.load(open(os.path.join(DATA, 'pricing/_personnel.json'), encoding='utf-8'))
        return {x['name'] for x in p.get('personnel', []) if x.get('status') == '在职'}
    except Exception:
        return set()


def load_cust_sp():
    """客户名 → 销售员 映射（_key_customers + customer_sp_map）"""
    m = {}
    try:
        kc = json.load(open(os.path.join(DATA, 'pricing/_key_customers.json'), encoding='utf-8'))
        for c in kc:
            name = (c.get('name') or '').strip()
            sp = (c.get('salesperson') or c.get('sales') or '').strip()
            if name and sp:
                m[name] = sp
    except Exception:
        pass
    spm = os.path.join(DATA, 'customer_sp_map.json')
    if os.path.exists(spm):
        try:
            d = json.load(open(spm, encoding='utf-8')).get('map', {})
            for k, v in d.items():
                m.setdefault(str(k).strip(), str(v).strip())
        except Exception:
            pass
    return m


def extract_customer(fname):
    """从文件名提取客户名/销售员名（去扩展名、类型后缀、日期、AI回答前缀）"""
    base = os.path.splitext(fname)[0]
    # 1) 去类型/场景后缀（长→短）
    suffixes = ['沟通策略与销售准备报告', '产品开发方向与原料匹配分析',
                '访前背调报告', '拜访作战卡', '市场调研卡', '简介卡片',
                '客户档案', '来访接待', '拜访纪要', '来访卡', '拜访卡',
                '调研报告', '客户调研', '调研卡', '作战卡', '飞书活动归纳',
                '纪要', '报告', '沟通', '样本']
    for suf in suffixes:
        base = base.replace(suf, '')
    # 2) 去日期（全局替换，不限结尾锚定，避免"20260909_样本"残留日期）
    base = re.sub(r'_\d{6,8}', '', base)
    base = re.sub(r'-\d{8}', '', base)
    base = re.sub(r'-\d{4}-\d{2}-\d{2}', '', base)
    # 3) 去 AI回答 前缀 + 纯日期前缀
    base = re.sub(r'^AI回答_?', '', base)
    base = re.sub(r'^\d{8,14}[-_]?', '', base)
    return base.strip('_ -')


def norm_company(name):
    """归一化公司名：去法律后缀，保留行业词（科技/生物/医疗）做区分"""
    for suf in ['股份有限公司', '有限责任公司', '有限公司', '集团']:
        name = name.replace(suf, '')
    return name.strip()


def load_cust_dept():
    """客户名 → 部门 映射（从 mcp_customers.json 的 field_5ed7ab 提取，用于无销售员归属的 CRM 客户）"""
    m = {}
    p = os.path.join(DATA, 'mcp_customers.json')
    if os.path.exists(p):
        try:
            d = json.load(open(p, encoding='utf-8'))
            items = d.get('customers', []) if isinstance(d, dict) else d
            for c in items:
                name = (c.get('name') or '').strip()
                dept = ''
                f = c.get('field_5ed7ab')
                if isinstance(f, dict):
                    dept = (f.get('label') or '').strip()
                elif isinstance(f, str):
                    dept = f.strip()
                if name and dept:
                    m[name] = dept
        except Exception:
            pass
    return m


def identify_owner(cust, cust_sp, personnel):
    """客户名 → 销售员。精确 → 归一化核心名包含 → 销售员本人名 → None"""
    if not cust:
        return None
    if cust in cust_sp:
        return cust_sp[cust]
    if cust in personnel:
        return cust
    # 归一化核心名双向包含匹配（处理"迈克生物" vs "迈克生物科技股份有限公司"）
    norm_cust = norm_company(cust)
    if len(norm_cust) >= 2:
        for cn, sp in cust_sp.items():
            norm_cn = norm_company(cn)
            if not norm_cn:
                continue
            if norm_cust == norm_cn or (len(norm_cust) >= 2 and norm_cust in norm_cn) or (len(norm_cn) >= 2 and norm_cn in norm_cust):
                return sp
    return None


def identify_dept(cust, cust_dept):
    """客户名 → 部门（CRM 客户无销售员时的 fallback），归一化核心名匹配"""
    if not cust:
        return None
    if cust in cust_dept:
        return cust_dept[cust]
    norm_cust = norm_company(cust)
    if len(norm_cust) >= 2:
        for cn, dept in cust_dept.items():
            norm_cn = norm_company(cn)
            if not norm_cn:
                continue
            if norm_cust == norm_cn or (len(norm_cust) >= 2 and norm_cust in norm_cn) or (len(norm_cn) >= 2 and norm_cn in norm_cust):
                return dept
    return None


def guess_type(fname, dirname):
    """文档类型：优先子目录名，其次文件名后缀"""
    if dirname in DIR_TYPE:
        return DIR_TYPE[dirname]
    for suf, t in SUFFIX_TYPE:
        if suf in fname:
            return t
    return 'other'


def collect_docs():
    """收集所有文档，返回 (docs, personnel, cust_sp, cust_dept)"""
    docs = []
    personnel = load_personnel()
    cust_sp = load_cust_sp()
    cust_dept = load_cust_dept()

    def _resolve(cust):
        """客户名 → (owner, dept)：优先销售员，次部门"""
        owner = identify_owner(cust, cust_sp, personnel)
        if owner:
            return owner, None
        return None, identify_dept(cust, cust_dept)

    # 1) AI 生成文件（私有目录 index.json 有 owner）
    if os.path.exists(PRIVATE_INDEX):
        try:
            idx = json.load(open(PRIVATE_INDEX, encoding='utf-8'))
            for f in idx.get('files', []):
                fp = os.path.join(PRIVATE_FILES, f.get('fid', '') + f.get('ext', ''))
                if os.path.exists(fp):
                    docs.append({
                        'src': fp, 'owner': (f.get('owner') or '').strip() or None, 'dept': None,
                        'type': guess_type(f.get('name', ''), ''),
                        'name': f.get('name', ''),
                        'source': 'AI生成',
                    })
        except Exception:
            pass

    # 2) 客户管理/ 各子目录 + 散落文件
    if os.path.isdir(CUST_MGMT):
        for root, dirs, files in os.walk(CUST_MGMT):
            if '/.' in root or '/归档' in root:
                continue
            dirname = os.path.basename(root)
            for fn in files:
                if fn == '.DS_Store' or fn.startswith('~'):
                    continue
                fp = os.path.join(root, fn)
                cust = extract_customer(fn)
                owner, dept = _resolve(cust)
                docs.append({
                    'src': fp, 'owner': owner, 'dept': dept,
                    'type': guess_type(fn, dirname),
                    'name': fn, 'source': '客户管理',
                })

    # 3) AI生成文件/（根目录早期测试文件）
    if os.path.isdir(AI_FILES_DIR):
        for fn in os.listdir(AI_FILES_DIR):
            if fn.startswith('.') or fn.startswith('~'):
                continue
            fp = os.path.join(AI_FILES_DIR, fn)
            cust = extract_customer(fn)
            owner, dept = _resolve(cust)
            docs.append({
                'src': fp, 'owner': owner, 'dept': dept,
                'type': guess_type(fn, ''),
                'name': fn, 'source': 'AI生成文件',
            })
    return docs, personnel, cust_sp, cust_dept


def archive(docs):
    """归档：软链接到 归档/<销售员>/<类型>/，返回 (索引, 未归类列表)"""
    # 清空旧归档（仅删软链接和空目录，保留索引重建）
    if os.path.isdir(ARCHIVE):
        for root, dirs, files in os.walk(ARCHIVE):
            for fn in files:
                fp = os.path.join(root, fn)
                if os.path.islink(fp) or fn != '索引.json':
                    try:
                        os.unlink(fp)
                    except Exception:
                        pass
        # 删空目录
        for root, dirs, files in os.walk(ARCHIVE, topdown=False):
            try:
                if root != ARCHIVE and not os.listdir(root):
                    os.rmdir(root)
            except Exception:
                pass
    os.makedirs(ARCHIVE, exist_ok=True)

    index = defaultdict(list)
    unclassified = []
    for d in docs:
        owner = d['owner']
        dept = d.get('dept')
        typ = d['type']
        label = TYPE_LABEL.get(typ, '其他')
        if not owner:
            if dept:
                owner = '部门/' + dept  # 无销售员归属，归到部门
            else:
                unclassified.append(d)
                owner = '未归类'
        # 归档目录
        dst_dir = os.path.join(ARCHIVE, owner, label)
        os.makedirs(dst_dir, exist_ok=True)
        # 软链接名：原文件名（去重防覆盖）
        base = os.path.basename(d['src'])
        dst = os.path.join(dst_dir, base)
        if os.path.exists(dst) or os.path.islink(dst):
            # 同名冲突：加时间戳后缀
            stem, ext = os.path.splitext(base)
            import time
            dst = os.path.join(dst_dir, f"{stem}_{int(time.time())%100000}{ext}")
        if not CHECK_ONLY:
            try:
                os.symlink(d['src'], dst)
            except Exception:
                pass
        index[owner].append({
            'name': d['name'], 'type': label, 'path': dst.replace(BASE, ''),
            'source': d['source'], 'src': d['src'].replace(BASE, ''),
        })

    if not CHECK_ONLY:
        with open(os.path.join(ARCHIVE, '索引.json'), 'w', encoding='utf-8') as f:
            json.dump({'updated': __import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                       'total': len(docs), 'owners': dict(index)}, f, ensure_ascii=False, indent=1)
    return dict(index), unclassified


def archive_one(src_path, owner, name):
    """归档单个文件到 归档/<owner>/<类型>/（软链接，用原文件名）。供 AI 生成文件后即时归档。
    src_path = 实际文件路径（fid.html），name = 展示用原文件名，owner = 归属销售员。
    返回 True 表示已归档，False 表示跳过（未归类/已存在/失败）。"""
    try:
        target = owner or '未知'
        if target == '未知':
            target = '未归类'
        ftype = guess_type(name or '', '')
        label = TYPE_LABEL.get(ftype, '其他')
        dst_dir = os.path.join(ARCHIVE, target, label)
        os.makedirs(dst_dir, exist_ok=True)
        base = os.path.basename(name or os.path.basename(src_path))
        dst = os.path.join(dst_dir, base)
        if os.path.exists(dst) or os.path.islink(dst):
            return False  # 已归档过，跳过
        os.symlink(src_path, dst)
        return True
    except Exception:
        return False


if __name__ == '__main__':
    docs, personnel, cust_sp, cust_dept = collect_docs()
    print(f'扫描到文档: {len(docs)} 个')
    idx, unclassified = archive(docs)
    print(f'归档目录数: {len([k for k in idx if k != "未归类"])} 个')
    for owner in sorted(idx):
        print(f'  {owner}: {len(idx[owner])} 个文档')
    print()
    print(f'未归类: {len(unclassified)} 个')
    for d in unclassified:
        print(f'  [未归类] {d["name"]} ({d["source"]})')
    if CHECK_ONLY:
        print('\n(--check 模式，未落盘)')
    else:
        print(f'\n✅ 已归档到 {ARCHIVE}')
