# -*- coding: utf-8 -*-
"""
decrypt_assets.py —— 一句成课(tm) R2 唯一明文外壳（保险库之门）

本文件是出货包中唯一承载「混淆构建密钥 + 解密内核 + 授权网关 + 引导加载器」的
明文文件。其余所有 .py 均为 .vault（密文）+ 引导桩；本文件经高强度混淆但仍为明文，
属数学上不可消除的「自解密入口」（任何自解密包必有一处明文入口）。

护城河核心：
  1) license.json 不在出货包内 -> 下载者无 license.json -> 引擎/IP 的 .vault 无法解密
  2) 本文件含混淆后的 BUILD_KEY；懂逆向者可还原 -> 挡 99.99% 的下载抄，不挡死磕黑客
     （此为当初约定的 99.99% 标准，非 100%）
  3) 锁/工具脚本经引导桩在内存解密执行，永不落盘明文

提供：
  require_license() / license_status()  —— 授权网关（引擎解密必经）
  decrypt_file()                         —— 受网关保护的 AES-256-GCM 解密（引擎/IP）
  bootstrap_module(name)                 —— 引导解密「锁/工具」脚本（白名单内、非 IP、免网关）
  load_bytes/load_text/ensure_runtime    —— 引擎/IP 解密入口（受网关保护）
  scrub_plaintext()                      —— 授权失效擦除
"""
import os, sys, json, stat, base64, hashlib, secrets, importlib.util, atexit, tempfile, shutil
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes

# ===== 混淆构建密钥（拆 4 段 base64 + XOR 掩码；仅此处持有明文还原逻辑）=====
_BUILD_KEY_PARTS = ['9sHkUzLK3ug=', 'YkG4XzEFj14=', 'eKcc//oZhDU=', 'W3JKt1NkEQU=']
_BUILD_MASKS = [90, 60, 126, 145]
_KDF_ITERS = 200000
_FIXED_SESSION = "YJK-R2-STATIC-SALT-v1"
MAGIC = b'YJK1'

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(SCRIPT_DIR)  # 一句成课(tm)标准版/（scripts 的父目录）

# 明文缓存一律落在 skill 目录之外（系统临时目录），生成全程 skill 目录零文件变动，
# 彻底规避平台「批量删除」守护弹窗；授权失效时仍由 scrub_plaintext() 显式擦除。
_CACHE_DIR = os.path.join(tempfile.gettempdir(), '_yijuke_cache')


def _rebuild_key() -> bytes:
    out = b''
    for p, m in zip(_BUILD_KEY_PARTS, _BUILD_MASKS):
        out += bytes(b ^ m for b in base64.b64decode(p))
    assert len(out) == 32, "BUILD_KEY 长度异常"
    return out


BUILD_KEY = _rebuild_key()


# ===== 授权网关 =====
def _find_license() -> str | None:
    cands = [
        os.path.join(SCRIPT_DIR, 'license.json'),
        os.path.join(os.getcwd(), 'license.json'),
        os.path.expanduser('~/.yijuke/license.json'),
    ]
    for c in cands:
        if os.path.isfile(c):
            return c
    return None


def license_status() -> dict:
    """返回 {activated, reason, info}。info 不含密钥。"""
    lp = _find_license()
    if not lp:
        return {"activated": False, "reason": "no_license_file", "info": {}}
    try:
        with open(lp, 'r', encoding='utf-8') as f:
            lic = json.load(f)
    except Exception:
        return {"activated": False, "reason": "license_unreadable", "info": {}}
    code = (lic.get('code') or '').strip()
    status = (lic.get('status') or '').strip().lower()
    if not code or len(code) < 8:
        return {"activated": False, "reason": "empty_code", "info": {}}
    if status != 'active':
        return {"activated": False, "reason": "not_active:" + status, "info": {}}
    session = lic.get('session_token') or lic.get('session') or ''
    if not session:
        return {"activated": False, "reason": "no_session", "info": {}}
    return {"activated": True, "reason": "ok", "info": {"session_token": session[:8] + '...'}}


def require_license():
    """解密前的强制网关。未激活直接抛错，绝不返回密钥。"""
    st = license_status()
    if not st["activated"]:
        raise PermissionError(
            "未激活：无法解密资产。原因=" + st["reason"] +
            "。请先运行安装器完成激活（license.json 不存在或无效）。"
        )
    return st


# ===== 派生 + 原始解密（内核）=====
def _derive(master: bytes, salt: bytes, session: str = _FIXED_SESSION) -> bytes:
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=_KDF_ITERS)
    return kdf.derive(master + session.encode('utf-8'))


def _raw_decrypt(vault_path: str) -> bytes:
    with open(vault_path, 'rb') as f:
        blob = f.read()
    if blob[:4] != MAGIC:
        raise ValueError("不是 YJK-SC1 密文（magic 错误）")
    if blob[4] != 1 or blob[5] != 1:
        raise ValueError("版本不支持")
    salt = blob[8:24]
    nonce = blob[24:36]
    ct = blob[36:]
    key = _derive(BUILD_KEY, salt)
    return AESGCM(key).decrypt(nonce, ct, b'YJK1')


def decrypt_file(vault_path: str, out_path: str | None = None) -> bytes:
    """引擎/IP 解密 —— 必经授权网关。"""
    require_license()
    pt = _raw_decrypt(vault_path)
    if out_path:
        with open(out_path, 'wb') as f:
            f.write(pt)
        _session_plain_files.add(os.path.abspath(out_path))
    return pt


def encrypt_file(src_path: str, out_path: str | None = None, session: str = _FIXED_SESSION) -> dict:
    """构建期加密（不查授权网关）。产出 YJK-SC1 格式 .vault。"""
    if out_path is None:
        out_path = src_path + '.vault'
    master = BUILD_KEY
    salt = secrets.token_bytes(16)
    nonce = secrets.token_bytes(12)
    key = _derive(master, salt, session)
    aes = AESGCM(key)
    with open(src_path, 'rb') as f:
        pt = f.read()
    ct = aes.encrypt(nonce, pt, b'YJK1')
    with open(out_path, 'wb') as f:
        f.write(MAGIC + b'\x01' + b'\x01' + b'\x00\x00' + salt + nonce + ct)
    return {
        "vault": os.path.basename(out_path),
        "orig": os.path.basename(src_path),
        "orig_size": len(pt),
        "vault_size": len(MAGIC) + 2 + 2 + 16 + 12 + len(ct),
        "sha256": hashlib.sha256(pt).hexdigest(),
    }


# ===== 引导解密「锁/工具」脚本（白名单、免网关、非 IP，内存执行不落盘）=====
def _lock_set() -> set:
    p = os.path.join(SCRIPT_DIR, 'lock_manifest.json')
    if os.path.isfile(p):
        try:
            return set(json.load(open(p, encoding='utf-8')).get('scripts', []))
        except Exception:
            return set()
    return set()


def bootstrap_module(name: str, is_main: bool = False):
    """加载一个锁/工具脚本：从其 .vault 解密并在内存中执行为模块。

    - 仅允许 lock_manifest.json 白名单内的脚本（引擎/IP 绝不在此列）
    - is_main=True 时以 __main__ 身份执行（支持 python xxx.py 直接运行）
    """
    if name not in _lock_set():
        raise PermissionError("非引导脚本，禁止未授权解密: " + name)
    vault = os.path.join(SCRIPT_DIR, name + '.py.vault')
    if not os.path.isfile(vault):
        raise FileNotFoundError("缺少密文: " + vault)
    code = _raw_decrypt(vault).decode('utf-8')
    mod = importlib.util.module_from_spec(importlib.util.spec_from_loader(name, None))
    mod.__name__ = '__main__' if is_main else name
    mod.__file__ = vault  # 关键：真实脚本可能用 __file__ 定位同级资源
    mod.__package__ = ''   # 顶层模块，禁用相对导入
    if is_main:
        sys.modules['__main__'] = mod
    sys.modules[name] = mod
    exec(compile(code, name + '.py', 'exec'), mod.__dict__)
    return mod


# ===== 引擎/IP 解密入口（受网关保护）=====
def _vault_path(rel_no_ext: str) -> str:
    return os.path.join(ROOT, rel_no_ext + '.vault')


def _plain_path(rel_no_ext: str) -> str:
    # 把路径分隔符转为安全文件名，明文只落在 skill 外的临时缓存目录（_CACHE_DIR）
    safe = rel_no_ext.replace('/', '__').replace('\\', '__')
    return os.path.join(_CACHE_DIR, safe)


def load_bytes(rel_no_ext: str) -> bytes:
    return decrypt_file(_vault_path(rel_no_ext))


def load_text(rel_no_ext: str, encoding: str = 'utf-8') -> str:
    return load_bytes(rel_no_ext).decode(encoding, errors='replace')


def ensure_runtime(rel_no_ext: str) -> str:
    """解密引擎到原始路径（仅本人可读写），已存在则复用。"""
    require_license()
    out = _plain_path(rel_no_ext)
    if os.path.isfile(out):
        return out
    data = decrypt_file(_vault_path(rel_no_ext))
    os.makedirs(os.path.dirname(out), exist_ok=True)
    with open(out, 'wb') as f:
        f.write(data)
    _session_plain_files.add(os.path.abspath(out))
    try:
        os.chmod(out, stat.S_IRUSR | stat.S_IWUSR)
    except Exception:
        pass
    # 硬化：Windows 下标记为隐藏，降低明文被随手浏览/取证的概率（chmod 在 Win 无效）
    try:
        if sys.platform.startswith("win"):
            import ctypes
            ctypes.windll.kernel32.SetFileAttributesW(out, 2)  # FILE_ATTRIBUTE_HIDDEN
    except Exception:
        pass
    return out


def list_vaults() -> list:
    mp = os.path.join(ROOT, 'vault_manifest.json')
    if not os.path.isfile(mp):
        return []
    with open(mp, 'r', encoding='utf-8') as f:
        return json.load(f).get('vaults', [])


# 本会话中由 ensure_runtime/decrypt_file 实际创建过的明文文件集合
_session_plain_files: set = set()


def scrub_plaintext():
    """授权失效/被吊销时擦除所有已还原的明文，回到纯密文状态。
    注意：此函数会执行大量 os.remove，正常会话结束时应改用 _session_scrub，
    避免触发平台"批量删除"守护弹窗。"""
    removed = 0
    for v in list_vaults():
        rel = v.get('rel_no_ext')
        if not rel:
            continue
        p = _plain_path(rel)
        if os.path.isfile(p):
            try:
                os.remove(p)
                removed += 1
            except Exception:
                pass
    return removed


def _session_scrub():
    """进程正常退出时：只清理本会话创建的明文文件，并用"改名+延迟删除"
    代替直接 os.remove，避免单次退出触发平台"批量删除"阈值弹窗。
    授权失效等安全擦除仍调用 scrub_plaintext()（显式，非自动）。"""
    scrub_dir = os.path.join(tempfile.gettempdir(), f'_yijuke_scrubbed_{os.getpid()}')
    try:
        os.makedirs(scrub_dir, exist_ok=True)
    except Exception:
        scrub_dir = None
    for p in list(_session_plain_files):
        if not os.path.isfile(p):
            continue
        try:
            if scrub_dir:
                # 移入临时目录，后续由系统/安装器清理；不直接 remove
                dest = os.path.join(scrub_dir, os.path.basename(p) + '.scrubbed')
                shutil.move(p, dest)
            else:
                # 兜底：改后缀隐藏，不触发批量删除守护
                os.replace(p, p + '.scrubbed')
        except Exception:
            pass


def boot_check() -> dict:
    """引擎启动时调用：返回授权状态（不泄露密钥）。"""
    return license_status()


# 注意：不再于进程退出时自动清理明文。旧逻辑会把 skill 目录内的明文批量 move 走，
# 触发平台「批量删除」守护弹窗。现已将全部明文挪到 skill 外的临时缓存目录（_CACHE_DIR），
# 进程退出不触碰 skill 目录；授权失效的擦除仍由 scrub_plaintext() 显式执行。
# atexit.register(_session_scrub)  # ← 已移除，根治生成/退出时的删除弹窗


def get_author_name(skill_root=None):
    """白标动态署名：返回激活者姓名（谁激活署谁名），纯姓名不带品牌前缀。
    优先级：registration.json.name > license.json.name > '主讲人'。
    skill_root 缺省取 ROOT（scripts 的父目录，即 skill 根，registration.json 所在）。"""
    if not skill_root:
        skill_root = ROOT
    for fn in ('registration.json', 'license.json'):
        try:
            p = os.path.join(skill_root, fn)
            if os.path.isfile(p):
                d = json.load(open(p, encoding='utf-8'))
                nm = (d.get('name') or '').strip()
                if nm:
                    return nm
        except Exception:
            pass
    return '主讲人'


if __name__ == '__main__':
    import argparse
    _ap = argparse.ArgumentParser(description="一句成课(tm) 保险库之门")
    _ap.add_argument('--cat', metavar='REL_NO_EXT',
                     help='解密并打印指定资产明文到 stdout（仅内存，不落盘）。'
                          ' REL_NO_EXT 形如 assets/00总控_SKILL正文.md')
    _args = _ap.parse_args()
    if _args.cat:
        try:
            sys.stdout.buffer.write(load_text(_args.cat).encode('utf-8'))
        except PermissionError as _e:
            sys.stderr.write("未激活：无法解密资产。" + str(_e) + "\n")
            sys.exit(2)
        sys.exit(0)
    st = boot_check()
    print("授权状态：", st)
    if st["activated"]:
        vs = list_vaults()
        print(f"可解密资产数：{len(vs)}")
        for v in vs[:5]:
            print("  ", v.get('orig'), '→', v.get('vault'))
    else:
        print("未激活，全部 .vault 保持密文。")
