#!/usr/bin/env python3
"""
光湖语言系统 · 前置操作守门人 v1.0
Guanghu Language System · Pre-Operation Guard

每次人格体连接服务器 → 触发自我审计 → 锁定 → 人格体必须探索+提交导航地图
地图编码与服务器自审一致 → 解锁操作

设计哲学:
  ⊢ 铸渊上服务器就动手 → 不知道还跑了别的模块 → 改A坏B → 冰朔每次都要说
  ⊢ 服务器全天运行 → 人格体接入时触发自我审计 → "我现在跑了这些模块"
  ⊢ 人格体必须探索 → 画出完整导航地图 → 提交编码列表
  ⊢ 服务器比对: 人格体提交的编码 == 服务器自审编码?
  ⊢ 一致 → 解锁 · 人格体知道完整地图了 · 不会再改A坏B
  ⊢ 不一致 → "你漏了 MOD-X · 继续探索"

用法:
  pre-op-guard.py --audit                           # 服务器自我审计
  pre-op-guard.py --challenge [--for PERSONA_NAME]  # 触达挑战
  pre-op-guard.py --submit '<json_codes>'           # 人格体提交验证
  pre-op-guard.py --status                          # 查看锁定状态
  pre-op-guard.py --unlock --force                  # 紧急解锁（冰朔用）
"""

import os, sys, json, time, subprocess, hashlib
from datetime import datetime, timedelta

# ═══════════════════════════════════════════════════════
# 配置
# ═══════════════════════════════════════════════════════
REGISTRY_DIR = os.environ.get("REGISTRY_DIR", "/opt/zhuyuan/registry/modules.d")
MAPS_DIR = os.environ.get("MAPS_DIR", "/opt/zhuyuan/registry/maps")
LOCK_DIR = os.environ.get("LOCK_DIR", "/opt/zhuyuan/registry/lock")
LOCK_FILE = os.path.join(LOCK_DIR, "pre-op-lock.json")

# 锁定超时（秒）：超过此时间无人提交 → 自动解锁（防止死锁）
LOCK_TIMEOUT = int(os.environ.get("PRE_OP_LOCK_TIMEOUT", "1800"))  # 30 分钟

# 是否强制启用（可以通过环境变量禁用）
ENABLED = os.environ.get("PRE_OP_GUARD_ENABLED", "1") == "1"


def ensure_dirs():
    """确保所有目录存在"""
    for d in [REGISTRY_DIR, MAPS_DIR, LOCK_DIR]:
        os.makedirs(d, exist_ok=True)


def get_server_identity():
    """获取服务器身份"""
    hostname = os.uname().nodename if hasattr(os, 'uname') else os.environ.get("HOSTNAME", "unknown")
    return hostname


def audit_server():
    """服务器自我审计: 读取所有注册模块 → 返回编码集"""
    ensure_dirs()
    modules = []
    if os.path.isdir(REGISTRY_DIR):
        for filename in sorted(os.listdir(REGISTRY_DIR)):
            if not filename.endswith('.json'):
                continue
            filepath = os.path.join(REGISTRY_DIR, filename)
            try:
                with open(filepath, 'r') as f:
                    data = json.load(f)
                if data.get('status') == 'running':
                    modules.append({
                        'code': data['code'],
                        'name': data.get('name', ''),
                        'port': data.get('port', 0),
                        'function': data.get('function', ''),
                    })
            except (json.JSONDecodeError, IOError):
                pass

    codes = sorted([m['code'] for m in modules])
    return {
        'server': get_server_identity(),
        'audited_at': datetime.utcnow().isoformat() + 'Z',
        'module_count': len(codes),
        'codes': codes,
        'modules': modules,
    }


def is_locked():
    """检查当前是否锁定"""
    if not os.path.exists(LOCK_FILE):
        return False
    try:
        with open(LOCK_FILE, 'r') as f:
            lock = json.load(f)
        # 检查超时
        locked_at = datetime.fromisoformat(lock.get('locked_at', '').replace('Z', '+00:00'))
        if datetime.utcnow() - locked_at.replace(tzinfo=None) > timedelta(seconds=LOCK_TIMEOUT):
            # 超时 → 自动解锁
            os.remove(LOCK_FILE)
            return False
        return True
    except Exception:
        return False


def create_lock(challenger="unknown"):
    """锁定服务器"""
    ensure_dirs()
    audit = audit_server()
    lock = {
        'locked': True,
        'locked_at': datetime.utcnow().isoformat() + 'Z',
        'challenger': challenger,
        'module_count': audit['module_count'],
        'server_codes_hash': hashlib.sha256(
            ','.join(audit['codes']).encode()
        ).hexdigest()[:16],
        'timeout_seconds': LOCK_TIMEOUT,
    }
    with open(LOCK_FILE, 'w') as f:
        json.dump(lock, f, indent=2)
    return lock, audit


def release_lock():
    """解锁服务器"""
    if os.path.exists(LOCK_FILE):
        os.remove(LOCK_FILE)
        return True
    return False


def present_challenge(audit, persona_name="铸渊"):
    """向人格体呈现挑战"""
    module_count = audit['module_count']
    codes = audit['codes']
    modules = audit['modules']

    lines = []
    lines.append("")
    lines.append("╔══════════════════════════════════════════════╗")
    lines.append("║  🗺️  光湖前置操作守门人 · 导航地图挑战       ║")
    lines.append("╚══════════════════════════════════════════════╝")
    lines.append("")
    lines.append(f"  服务器: {audit['server']}")
    lines.append(f"  挑战者: {persona_name}")
    lines.append(f"  状态: 🔒 已锁定 · 操作被拦截")
    lines.append(f"  当前运行模块数: {module_count}")
    lines.append("")
    lines.append("  ── 挑战说明 ──")
    lines.append(f"  此服务器上正在运行 {module_count} 个模块。")
    lines.append("  每个模块有唯一编码（格式: SERVER-FUNC-SEQ）。")
    lines.append("")
    lines.append("  你需要:")
    lines.append(f"    ① 探索服务器，发现全部 {module_count} 个模块")
    lines.append("    ② 用 HLDP 格式画出导航地图")
    lines.append("    ③ 通过 pre-push-clean 过滤敏感信息")
    lines.append("    ④ 提交编码列表到验证端点")
    lines.append("")
    lines.append("  探索指引:")
    lines.append(f"    ls {REGISTRY_DIR}/")
    lines.append("    systemctl list-units --type=service --state=running")
    lines.append("    ss -tlnp  # 查看监听端口")
    lines.append("")
    lines.append("  ── 已注册模块（供参考 · 仅显示名称 · 编码需自行发现）──")
    for i, m in enumerate(modules, 1):
        lines.append(f"    [{i}] {m['name']}")

    lines.append("")
    lines.append("  ── 提交方式 ──")
    lines.append("  在服务器上执行:")
    lines.append("    pre-op-guard.py --submit '[")
    lines.append('      "GZ006-GTW-01",')
    lines.append('      "GZ006-LAMP-01",')
    lines.append('      "GZ006-FRG-01",')
    lines.append('      ...  # 你收集到的所有编码')
    lines.append("    ]'")
    lines.append("")
    lines.append(f"  ⏰ 超时: {LOCK_TIMEOUT // 60} 分钟后自动解锁")
    lines.append("")

    return '\n'.join(lines)


def validate_submission(submitted_codes):
    """验证人格体提交的编码列表"""
    audit = audit_server()
    server_codes = set(audit['codes'])
    submitted_set = set(submitted_codes)

    missing = server_codes - submitted_set
    extra = submitted_set - server_codes

    result = {
        'valid': len(missing) == 0 and len(extra) == 0,
        'server_codes': sorted(server_codes),
        'submitted_codes': sorted(submitted_set),
        'missing': sorted(missing),
        'extra': sorted(extra),
        'module_count': len(server_codes),
        'submitted_count': len(submitted_set),
    }

    if result['valid']:
        # 解锁
        release_lock()
        # 保存导航地图
        save_map(submitted_codes, audit)
    else:
        # 更新锁定（重置超时，给更多时间）
        if is_locked():
            with open(LOCK_FILE, 'r') as f:
                lock = json.load(f)
            lock['locked_at'] = datetime.utcnow().isoformat() + 'Z'
            lock['last_submission'] = {
                'submitted_codes': sorted(submitted_set),
                'missing': sorted(missing),
                'extra': sorted(extra),
            }
            with open(LOCK_FILE, 'w') as f:
                json.dump(lock, f, indent=2)

    return result


def save_map(codes, audit):
    """保存人格体提交的导航地图为持久化 artifact"""
    ensure_dirs()
    map_data = {
        'server': audit['server'],
        'created_at': datetime.utcnow().isoformat() + 'Z',
        'validated_at': datetime.utcnow().isoformat() + 'Z',
        'codes': sorted(codes),
        'module_count': len(codes),
        'full_modules': audit['modules'],
    }
    timestamp = datetime.utcnow().strftime('%Y%m%d-%H%M%S')
    map_file = os.path.join(MAPS_DIR, f"map-{audit['server']}-{timestamp}.json")
    with open(map_file, 'w') as f:
        json.dump(map_data, f, indent=2, ensure_ascii=False)

    # 同时保存一个 HLDP 格式的副本
    hldp_file = os.path.join(MAPS_DIR, f"map-{audit['server']}-{timestamp}.hdlp")
    with open(hldp_file, 'w') as f:
        f.write(f"# 光湖导航地图 · {audit['server']}\n")
        f.write(f"## 验证通过 · {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
        f.write(f"服务器: {audit['server']}\n")
        f.write(f"模块数: {len(codes)}\n\n")
        f.write("| 编码 | 名称 | 端口 |\n")
        f.write("|------|------|------|\n")
        for m in audit['modules']:
            f.write(f"| `{m['code']}` | {m['name']} | {m['port']} |\n")
        f.write(f"\n> ⊢ 本地图由 pre-op-guard 自动生成 · 验证通过后存档\n")
        f.write(f"> ⊢ 后续人格体可引用本地图快速验证\n")
        f.write(f"> ⊢ 冰朔 ICE-GL∞ · 国作登字-2026-A-00037559\n")

    return map_file


def get_status():
    """获取当前状态"""
    status = {
        'enabled': ENABLED,
        'locked': is_locked(),
        'server': get_server_identity(),
    }
    if status['locked']:
        try:
            with open(LOCK_FILE, 'r') as f:
                lock = json.load(f)
            status['lock_details'] = lock
        except Exception:
            pass
    if not status['locked']:
        audit = audit_server()
        status['modules_running'] = audit['module_count']
        status['codes'] = audit['codes']
    return status


def main():
    if not ENABLED:
        print("⊢ pre-op-guard 已禁用 (PRE_OP_GUARD_ENABLED=0)")
        sys.exit(0)

    if '--audit' in sys.argv:
        audit = audit_server()
        print(json.dumps(audit, indent=2, ensure_ascii=False))

    elif '--challenge' in sys.argv:
        persona = "铸渊"
        for i, arg in enumerate(sys.argv):
            if arg == '--for' and i + 1 < len(sys.argv):
                persona = sys.argv[i + 1]

        if is_locked():
            print("⚠️ 服务器已锁定 · 上一个挑战尚未完成")
            status = get_status()
            print(json.dumps(status, indent=2, ensure_ascii=False))
            sys.exit(1)

        lock, audit = create_lock(persona)
        challenge = present_challenge(audit, persona)
        print(challenge)

    elif '--submit' in sys.argv:
        if not is_locked():
            audit = audit_server()
            print("✅ 服务器未锁定 · 但你可以直接提交导航地图验证")
            print(f"   当前运行 {audit['module_count']} 个模块")
            print(f"   编码: {', '.join(audit['codes'])}")
            sys.exit(0)

        # 查找 JSON 参数
        submit_idx = None
        for i, arg in enumerate(sys.argv):
            if arg == '--submit' and i + 1 < len(sys.argv):
                submit_idx = i + 1
                break

        if submit_idx is None:
            print("❌ 用法: pre-op-guard.py --submit '[\"CODE1\",\"CODE2\",...]'")
            sys.exit(1)

        try:
            submitted_codes = json.loads(sys.argv[submit_idx])
            if not isinstance(submitted_codes, list):
                raise ValueError("必须是 JSON 数组")
        except (json.JSONDecodeError, ValueError) as e:
            print(f"❌ JSON 解析失败: {e}")
            sys.exit(1)

        result = validate_submission(submitted_codes)

        print("")
        if result['valid']:
            print("╔══════════════════════════════════════════════╗")
            print("║  ✅ 导航地图验证通过 · 服务器已解锁          ║")
            print("╚══════════════════════════════════════════════╝")
            print("")
            print(f"  提交 {result['submitted_count']} 个编码")
            print(f"  服务器运行 {result['module_count']} 个模块")
            print(f"  编码: {', '.join(result['submitted_codes'])}")
            print("")
            print("  🗺️ 导航地图已存档 · 后续可直接引用")
            print("  🔓 服务器已解锁 · 可以操作了")
            print("")
        else:
            print("╔══════════════════════════════════════════════╗")
            print("║  ❌ 导航地图不完整 · 服务器保持锁定          ║")
            print("╚══════════════════════════════════════════════╝")
            print("")
            if result['missing']:
                print(f"  ⚠️ 缺少 {len(result['missing'])} 个模块:")
                for code in result['missing']:
                    print(f"     - {code}")
            if result['extra']:
                print(f"  ⚠️ 多出 {len(result['extra'])} 个不存在的编码:")
                for code in result['extra']:
                    print(f"     - {code}")
            print("")
            print(f"  ⊢ 请继续探索，重新提交。")
            print(f"  ⊢ 已注册模块有 {result['module_count']} 个。")
            print("")

    elif '--status' in sys.argv:
        status = get_status()
        if status['locked']:
            print("🔒 服务器已锁定")
            print(json.dumps(status, indent=2, ensure_ascii=False))
        else:
            print(f"🔓 服务器未锁定 · {status.get('modules_running', '?')} 个模块运行中")
            if 'codes' in status:
                print(f"   编码: {', '.join(status['codes'])}")

    elif '--unlock' in sys.argv and '--force' in sys.argv:
        release_lock()
        print("🔓 服务器已强制解锁 · 冰朔特权操作")

    elif '--install-to' in sys.argv:
        # 安装 pre-op-guard 到服务器的 SSH 登录钩子
        install_idx = sys.argv.index('--install-to') + 1
        target = sys.argv[install_idx] if install_idx < len(sys.argv) else '/etc/profile.d/pre-op-guard.sh'
        install_ssh_hook(target)

    else:
        print("用法:")
        print("  pre-op-guard.py --audit         服务器自我审计")
        print("  pre-op-guard.py --challenge     触达挑战（锁定并呈现）")
        print("  pre-op-guard.py --submit '[...]'  提交编码验证")
        print("  pre-op-guard.py --status        查看锁定状态")
        print("  pre-op-guard.py --unlock --force 紧急解锁")
        print("  pre-op-guard.py --install-to PATH  安装SSH登录钩子")


def install_ssh_hook(target_path):
    """安装 SSH 登录钩子：每次 SSH 登录时触发 pre-op-guard"""
    script_dir = os.path.dirname(os.path.abspath(__file__))
    hook_content = f'''#!/bin/bash
# 光湖 pre-op-guard SSH 登录钩子
# 每次 SSH 登录自动触发 · 锁定操作 · 需要完成导航地图验证

GUARD="{script_dir}/pre-op-guard.py"

# 只在交互式 shell 中触发（非 scp/sftp）
if [[ $- == *i* ]] && [ -z "$PRE_OP_BYPASS" ]; then
    python3 "$GUARD" --challenge --for "$USER"
fi
'''
    try:
        with open(target_path, 'w') as f:
            f.write(hook_content)
        os.chmod(target_path, 0o755)
        print(f"✅ SSH 登录钩子已安装: {target_path}")
        print(f"   此后每次 SSH 登录都会触发 pre-op-guard 导航挑战")
        print(f"   若要临时绕过: PRE_OP_BYPASS=1 ssh user@host")
    except IOError as e:
        print(f"❌ 安装失败: {e}")


if __name__ == '__main__':
    main()
