#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Hermes输出-工作类 目录整理（B 归类 / C 根文件归位 / D 备份归档 / A 区只清 __pycache__）

用法：
    env -u PYTHONPATH /usr/bin/python3 脚本/organize_workdir.py            # 干跑（默认）
    env -u PYTHONPATH /usr/bin/python3 脚本/organize_workdir.py --apply    # 真动

铁律：
  1. 被代码硬引用的顶层目录/根文件/备份目录 —— 一律原位不动（下方 PROTECT_* 常量）
  2. 只移动不删除；唯一删除项 = __pycache__（用户批准，Python 自动重建）
  3. 每次真动写 归档/_整理清单_<时间>.json，逐条可回滚
"""
import os, sys, json, time, shutil, re

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
APPLY = '--apply' in sys.argv

# ── 保护清单：被代码引用，绝不移动 ────────────────────────────────
PROTECT_DIRS = {
    '数据', '脚本', '工具', '销售分析', '对外物料', '科研2026秋季促销', '营销活动',
    '2026年3季度汇报', '知识库-copilot', '知识沉淀', '客户管理', '资料', '产品说明',
    '产品验证资料', '报销助手', '归档', '备份', 'AI生成文件', '.hermes',
    'cache', '_tmp', '.q3scratch', '_bak_todo_p0_20261007-212235',
}
PROTECT_FILES = {
    # 三层架构必留根
    '宝锐工作台.md', 'PROJECT.md', 'TASK.md', 'RULES.md',
    # CloudBase CLI 按 cwd 自动发现，不能动
    'cloudbaserc.json',
    # 被脚本/页面硬引用
    '2026基础数据核对表_全部部门_可修正.html', 'P0待办类型复核.html', 'QA报告_20260726.md',
    'logo.png', '三部门KPI关键任务积分卡看板.html', '产品说明.md',
    '刘子研_2025vs2026同期对比.xlsx', '客户7月计划_9月计划-可编辑-3.xlsx',
    '客户归属待放权确认_20261006.html', '客户归属待放权确认_20261006.md',
    '工作台改版上手一页图.html', '工作台结构重构方案_v0.1.html',
    '开学季促销方案_spec.json', '活动待办打通构想_v0.2.html',
    '知识库完善方案-AI助手支撑-v1.md',
    # 备份脚本状态标记（无后缀隐藏文件）
    '.liubin_bakts', '.ls600_bakts', '.sandept_bakts', '.DS_Store',
}

# ── B 区：零散主题目录 → 5 个大类 ─────────────────────────────────
B_ZONE = {
    '培训与上手': ['培训', '培训资料', '刘子研上手包', '操作指导', '手册与规范', '人才培养'],
    '经营分析':   ['汇报材料', '月报', '数据分析', '产销', '财务', '回款分析'],
    '产品与市场': ['产品', '市场', '竞争分析', '竞品调研', '组学酶市场扫描', '调研报告',
                   '科研助手', '诊断类非分子产品定制项目'],
    '行政与商务': ['商务', '合同', '出差拜访计划', '团队共享', '会话存档', '配置', '云函数'],
    '工作台工程': ['架构对比', '验证_三部门逐月_方案A', '截图', '.sandept_work'],
}

# ── C 区：根目录散落文件 → 归位 ──────────────────────────────────
# 按扩展名路由（目标目录，可带子目录）
C_ROUTE = [
    ('.html', '工具'),
    ('.md',   '资料'),
    ('.png',  '工作台工程/截图'),
    ('.docx', '经营分析/汇报材料'),
    ('.xlsx', '销售分析'),
    ('.csv',  '产品与市场'),
    ('.pdf',  '培训与上手/操作指导'),
    ('.json', '营销活动'),
]
# 旧版/备份后缀 → 归档
BAK_SUFFIX = re.compile(r'\.bak|_bak_|-OLD|_\d{8}[_-]?\d{0,6}\.bak|副本')


def size_of(p):
    if os.path.isfile(p):
        return os.path.getsize(p)
    n = s = 0
    for dp, _, fs in os.walk(p):
        for f in fs:
            try:
                s += os.path.getsize(os.path.join(dp, f)); n += 1
            except Exception:
                pass
    return s


def count_of(p):
    if os.path.isfile(p):
        return 1
    n = 0
    for _, _, fs in os.walk(p):
        n += len(fs)
    return n


def main():
    os.chdir(ROOT)
    actions = []          # (kind, src, dst)
    warnings = []

    # ── B 区 ──
    b_items = []
    for parent, subs in B_ZONE.items():
        for s in subs:
            if not os.path.exists(s):
                continue
            if s in PROTECT_DIRS:
                warnings.append('B区跳过（保护清单）：%s' % s); continue
            dst = os.path.join(parent, s)
            if os.path.exists(dst):
                warnings.append('目标已存在，跳过：%s → %s' % (s, dst)); continue
            actions.append(('dir', s, dst)); b_items.append((parent, s))
    # 空目录也要搬（团队共享 0 文件）

    # ── C 区 ──
    c_items = []
    roots = [e for e in sorted(os.listdir('.'))
             if os.path.isfile(e) and e != '.DS_Store']
    for f in roots:
        if f in PROTECT_FILES:
            continue
        if BAK_SUFFIX.search(f):
            dst = os.path.join('归档', '_版本备份', '根文件旧版', f)
            actions.append(('file', f, dst)); c_items.append((os.path.dirname(dst), f)); continue
        ext = os.path.splitext(f)[1].lower()
        dst_dir = next((d for e, d in C_ROUTE if e == ext), None)
        if not dst_dir:
            warnings.append('C区无路由，留原位：%s' % f); continue
        actions.append(('file', f, os.path.join(dst_dir, f)))
        c_items.append((dst_dir, f))

    # ── D 区 ──
    d_items = []
    for e in sorted(os.listdir('.')):
        if not os.path.isdir(e):
            continue
        if not re.match(r'^(_bak_|_tmp|\.q3scratch|cache$)', e):
            continue          # __pycache__ 不归档，走 A 区删除
        if e in PROTECT_DIRS:
            warnings.append('D区跳过（保护清单）：%s' % e); continue
        dst = os.path.join('归档', '_版本备份', e)
        if os.path.exists(dst):
            warnings.append('目标已存在，跳过：%s' % e); continue
        actions.append(('dir', e, dst)); d_items.append(e)
    # 知识库-copilot-OLD（用户拍板归档）
    if os.path.isdir('知识库-copilot-OLD'):
        dst = os.path.join('归档', '_版本备份', '知识库-copilot-OLD')
        if os.path.exists(dst):
            warnings.append('目标已存在，跳过：知识库-copilot-OLD')
        else:
            actions.append(('dir', '知识库-copilot-OLD', dst)); d_items.append('知识库-copilot-OLD')

    # ── A 区：__pycache__ 清理（含嵌套）──
    pyc = []
    for dp, ds, _ in os.walk('.'):
        if os.path.basename(dp) == '__pycache__':
            pyc.append(dp); ds[:] = []
    pyc = sorted(set(pyc))

    # ── 守卫 1：没有任何保护项被排进移动列表 ──
    moved = [a[1] for a in actions]
    bad = [m for m in moved if m in PROTECT_DIRS or m in PROTECT_FILES]
    if bad:
        print('❌ 守卫失败：保护项被排入移动列表 → %s' % bad); return 1

    # ── 守卫 2：目标不落在保护目录内部（归档 除外）──
    for _, s, d in actions:
        top = d.split('/')[0]
        if top not in PROTECT_DIRS and top not in B_ZONE and top != '归档':
            print('❌ 守卫失败：目标目录不在白名单 → %s' % d); return 1

    # ── 守卫 3：源不存在 / 源=目标 ──
    for k, s, d in actions:
        if not os.path.exists(s):
            print('❌ 守卫失败：源不存在 %s' % s); return 1
        if os.path.abspath(s) == os.path.abspath(d):
            print('❌ 守卫失败：源与目标相同 %s' % s); return 1

    # ── 输出 ──
    print('═' * 74)
    print('  Hermes输出-工作类 整理计划   %s' % ('【真动模式】' if APPLY else '【干跑 · 未做任何改动】'))
    print('═' * 74)

    print('\n【B 区】零散主题目录 → %d 个大类   共 %d 项' % (len(B_ZONE), len(b_items)))
    for parent in B_ZONE:
        subs = [s for p, s in b_items if p == parent]
        if not subs:
            continue
        n = sum(count_of(s) for s in subs)
        mb = sum(size_of(s) for s in subs) / 1048576
        print('  %-12s ← %s' % (parent + '/', '  '.join('%s(%d)' % (s, count_of(s)) for s in subs)))
        print('  %-12s   %d 个文件 / %.1f MB' % ('', n, mb))

    print('\n【C 区】根目录散落文件 → 归位   共 %d 项' % len(c_items))
    from collections import OrderedDict
    grp = OrderedDict()
    for d, f in c_items:
        grp.setdefault(d, []).append(f)
    for d, fs in grp.items():
        print('  → %-18s %2d 个   %s' % (d + '/', len(fs),
                                         '  '.join(f[:18] for f in fs[:3]) + (' …' if len(fs) > 3 else '')))

    print('\n【D 区】备份/临时目录 → 归档/_版本备份/   共 %d 项' % len(d_items))
    fam = OrderedDict()
    for e in d_items:
        key = re.sub(r'_\d{6,}[-_]?\d*$', '', e).rstrip('_-')
        fam.setdefault(key, []).append(e)
    for k, v in fam.items():
        print('  %-26s ×%-2d  %.1f MB' % (k, len(v), sum(size_of(x) for x in v) / 1048576))

    print('\n【A 区】清理 __pycache__   共 %d 个（Python 自动重建，用户已批准）' % len(pyc))
    for p in pyc:
        print('  %-40s %.1f MB' % (p, size_of(p) / 1048576))
    print('  合计释放 ≈ %.1f MB' % (sum(size_of(p) for p in pyc) / 1048576))

    print('\n【保护清单 · 原位不动】')
    pf_found = [f for f in sorted(PROTECT_FILES) if os.path.exists(f)]
    pf_miss = sorted(set(PROTECT_FILES) - set(pf_found))
    pd_found = [d for d in sorted(PROTECT_DIRS) if os.path.isdir(d)]
    print('  顶层目录 %d 个（实存 %d）：%s …' % (len(PROTECT_DIRS), len(pd_found), ' '.join(pd_found[:10])))
    print('  根目录文件 实存 %d 个：%s …' % (len(pf_found), ' '.join(pf_found[:8])))
    allroots = [e for e in os.listdir('.') if os.path.isfile(e) and e != '.DS_Store']
    movable = [f for f in allroots if f not in PROTECT_FILES]
    pf_in = [f for f in allroots if f in PROTECT_FILES]
    print('  根目录文件 计数勾稽：%d 个 = 归位 %d + 保护 %d  %s'
          % (len(allroots), len(movable), len(pf_in),
             '✅ 平' if len(movable) + len(pf_in) == len(allroots) else '❌ 不平，需查'))
    if pf_miss:
        print('  （清单中 %d 项在根目录已不存在，跳过：%s）' % (len(pf_miss), ' '.join(pf_miss[:4])))
    if warnings:
        print('\n【提示 %d 条】' % len(warnings))
        for w in warnings:
            print('  · %s' % w)

    print('\n' + '─' * 74)
    print('  合计：移动 %d 项（目录 %d / 文件 %d）｜删除 __pycache__ %d 个'
          % (len(actions), sum(1 for a in actions if a[0] == 'dir'),
             sum(1 for a in actions if a[0] == 'file'), len(pyc)))
    print('─' * 74)

    if not APPLY:
        print('\n  ⓘ 干跑结束，未改动任何文件。确认后加 --apply 执行。')
        return 0

    # ── 执行 ──
    manifest = {'ts': time.strftime('%Y-%m-%d %H:%M:%S'), 'moves': [], 'deleted': []}
    os.makedirs(os.path.join('归档', '_版本备份'), exist_ok=True)
    for kind, s, d in actions:
        os.makedirs(os.path.dirname(d), exist_ok=True)
        shutil.move(s, d)
        manifest['moves'].append({'src': s, 'dst': d, 'kind': kind})
    for p in pyc:
        shutil.rmtree(p, ignore_errors=True)
        manifest['deleted'].append(p)
    mf = os.path.join('归档', '_整理清单_%s.json' % time.strftime('%Y%m%d-%H%M%S'))
    with open(mf, 'w', encoding='utf-8') as f:
        json.dump(manifest, f, ensure_ascii=False, indent=2)
    print('\n✅ 执行完成：移动 %d 项，删除 %d 个缓存目录' % (len(manifest['moves']), len(manifest['deleted'])))
    print('   回滚清单：%s' % mf)
    return 0


if __name__ == '__main__':
    sys.exit(main())
