"""
数据文件加密模块 — 单机版
- AES-256-CBC 加密/解密
- 密钥从解锁密码通过 PBKDF2 派生
- 每个文件独立随机 IV（前 16 字节）
- 支持目录级批量加密/解密
- 运行时密码缓存在进程内存中
"""
import os
import logging
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend

logger = logging.getLogger(__name__)

SALT_SIZE = 16
IV_SIZE = 16
KEY_SIZE = 32  # AES-256
PBKDF2_ITERATIONS = 100_000

# 固定应用密钥 — 用于分发包预加密（.docx.enc / .md.enc 等文件加密）
# 此密钥编译在 EXE 中，防普通复制，不防逆向
APP_DEFAULT_KEY = "ZFZS-Standalone-2026-SecureKey"

# 旧版硬编码密钥（仅用于迁移兼容，迁移完成后可移除）
_LEGACY_SYSTEM_DB_KEY = "zhifazhoushou2026"

# ---- 进程内存中的密码缓存 ----
_cached_password: str | None = None


def set_password(password: str):
    """缓存密码到进程内存（解锁时调用）"""
    global _cached_password
    _cached_password = password


def get_password() -> str | None:
    """获取缓存的密码"""
    return _cached_password


def clear_password():
    """清除缓存密码（锁定/退出时调用）"""
    global _cached_password
    _cached_password = None


def derive_key(password: str, salt: bytes) -> bytes:
    """从密码派生 AES-256 密钥"""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=KEY_SIZE,
        salt=salt,
        iterations=PBKDF2_ITERATIONS,
        backend=default_backend(),
    )
    return kdf.derive(password.encode("utf-8"))


# ============ SQLCipher 数据库密钥派生 ============

# 数据库加密 salt（打包时写入，与密钥管理文档一致）
SYSTEM_DB_SALT = "d2086088a64c0a2db46abc61d3cefdc6cc7797bdce302db2d5921d4d4ce3d3f7"
USER_DB_SALT = "597c376f9f968682fa003ef84dd716e55d4dee12dcd92b76c912905076aad6aa"


def get_machine_id() -> str:
    """
    获取机器唯一标识（用于 system.db 密钥派生）
    基于 CPU ID + 主板 UUID + 磁盘序列号
    """
    import hashlib
    import platform
    import subprocess

    parts = []

    # CPU ID
    try:
        if platform.system() == "Windows":
            result = subprocess.run(
                ["wmic", "cpu", "get", "ProcessorId"],
                capture_output=True, text=True, timeout=5,
                creationflags=0x08000000
            )
            cpu_id = result.stdout.strip().split("\n")[-1].strip()
            if cpu_id and cpu_id != "ProcessorId":
                parts.append(cpu_id)
        else:
            # Linux/Mac: use /proc/cpuinfo or sysctl
            result = subprocess.run(
                ["cat", "/proc/cpuinfo"], capture_output=True, text=True, timeout=5
            )
            for line in result.stdout.split("\n"):
                if "Serial" in line or "processor" in line.lower():
                    parts.append(line.strip())
                    break
    except Exception:
        pass

    # 主板 UUID
    try:
        if platform.system() == "Windows":
            result = subprocess.run(
                ["wmic", "csproduct", "get", "UUID"],
                capture_output=True, text=True, timeout=5,
                creationflags=0x08000000
            )
            uuid = result.stdout.strip().split("\n")[-1].strip()
            if uuid and uuid != "UUID":
                parts.append(uuid)
    except Exception:
        pass

    # 磁盘序列号
    try:
        if platform.system() == "Windows":
            result = subprocess.run(
                ["wmic", "diskdrive", "get", "SerialNumber"],
                capture_output=True, text=True, timeout=5,
                creationflags=0x08000000
            )
            serial = result.stdout.strip().split("\n")[-1].strip()
            if serial and serial != "SerialNumber":
                parts.append(serial)
    except Exception:
        pass

    # 如果获取失败，使用 hostname + 用户名作为兜底
    if not parts:
        import getpass
        parts = [platform.node(), getpass.getuser()]

    combined = "|".join(parts)
    return hashlib.sha256(combined.encode("utf-8")).hexdigest()


def derive_system_db_key(db_path: str = None) -> str:
    """
    获取 system.db 的 SQLCipher 密钥（机器绑定）。

    密钥派生方式：PBKDF2(get_machine_id(), SYSTEM_DB_SALT, 256000轮)
    不同机器 → 不同密钥，system.db 无法跨机器打开。

    参数：
      db_path: system.db 文件路径（由调用方 database.py 传入，避免循环导入）

    迁移兼容：
    - 新安装：打包分发明文 system.db，首次启动时自动用本机密钥加密
    - 旧版升级：检测到旧硬编码密钥加密的 system.db → 自动 rekey 为本机密钥
    """
    machine_id = get_machine_id()
    salt = bytes.fromhex(SYSTEM_DB_SALT)

    # PBKDF2 派生本机专属密钥
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA512(),
        length=32,
        salt=salt,
        iterations=256_000,
        backend=default_backend(),
    )
    new_key = kdf.derive(machine_id.encode("utf-8")).hex()

    # 未提供路径（开发模式或尚未初始化）→ 直接返回新密钥
    if not db_path or not os.path.exists(db_path):
        return new_key

    # 尝试用新密钥直接打开（已迁移或全新安装后已加密的场景）
    if _can_open_with_key(db_path, new_key):
        return new_key

    # 新密钥失败 → 可能是旧版硬编码密钥加密的数据 → 执行迁移
    if _can_open_with_key(db_path, _LEGACY_SYSTEM_DB_KEY):
        logger.info("检测到旧版硬编码密钥加密的 system.db，正在迁移至机器绑定密钥...")
        _migrate_system_db_key(db_path, _LEGACY_SYSTEM_DB_KEY, new_key)
        logger.info("system.db 迁移完成")
    else:
        # 明文数据库（新安装未加密）→ 不迁移，由调用方决定是否加密
        pass

    return new_key


def _can_open_with_key(db_path: str, key: str) -> bool:
    """尝试用指定密钥打开数据库，验证是否成功"""
    try:
        from sqlcipher3 import dbapi2 as sqlite
    except ImportError:
        try:
            from pysqlcipher3 import dbapi2 as sqlite
        except ImportError:
            return False
    try:
        conn = sqlite.connect(db_path)
        conn.execute(f"PRAGMA key = '{key}'")
        conn.execute("PRAGMA cipher_page_size = 4096")
        conn.execute("PRAGMA kdf_iter = 256000")
        conn.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA512")
        conn.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")
        conn.execute("SELECT count(*) FROM sqlite_master")
        conn.close()
        return True
    except Exception:
        return False


def _migrate_system_db_key(db_path: str, old_key: str, new_key: str):
    """将 system.db 从旧密钥迁移到新密钥（SQLCipher PRAGMA rekey）"""
    try:
        from sqlcipher3 import dbapi2 as sqlite
    except ImportError:
        from pysqlcipher3 import dbapi2 as sqlite

    conn = sqlite.connect(db_path)
    conn.execute(f"PRAGMA key = '{old_key}'")
    conn.execute("PRAGMA cipher_page_size = 4096")
    conn.execute("PRAGMA kdf_iter = 256000")
    conn.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA512")
    conn.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")

    # 验证旧密钥可用
    try:
        conn.execute("SELECT count(*) FROM sqlite_master")
    except Exception:
        conn.close()
        raise ValueError("旧密钥验证失败，无法迁移")

    # 执行 rekey
    conn.execute(f"PRAGMA rekey = '{new_key}'")
    conn.close()


def encrypt_plaintext_db(db_path: str, key: str) -> bool:
    """
    将明文 SQLite 数据库加密为 SQLCipher 格式（运行时首次启动自动调用）。

    使用 SQLCipher ATTACH + sqlcipher_export 方式：
    1. 创建空的加密数据库作为 main
    2. ATTACH 明文源库（KEY '' = 明文）
    3. SELECT sqlcipher_export('main', 'src') 导入全部表和数据
    4. 替换原文件

    返回 True 表示加密成功，False 表示失败（如 sqlcipher3 不可用）。
    """
    try:
        from sqlcipher3 import dbapi2 as sqlite
    except ImportError:
        try:
            from pysqlcipher3 import dbapi2 as sqlite
        except ImportError:
            logger.warning("sqlcipher3 不可用，跳过 system.db 自动加密")
            return False

    tmp_path = db_path + ".tmpenc"
    try:
        # 1. 创建加密空库
        enc = sqlite.connect(tmp_path)
        enc.execute(f"PRAGMA key = '{key}'")
        enc.execute("PRAGMA cipher_page_size = 4096")
        enc.execute("PRAGMA kdf_iter = 256000")
        enc.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA512")
        enc.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")

        # 2. ATTACH 明文源库并导入
        enc.execute(f"ATTACH DATABASE '{db_path}' AS src KEY ''")
        enc.execute("SELECT sqlcipher_export('main', 'src')")
        enc.execute("DETACH DATABASE src")
        enc.close()

        # 3. 替换原文件
        os.replace(tmp_path, db_path)
        logger.info(f"system.db 首次自动加密完成（{os.path.getsize(db_path)/1024/1024:.0f} MB）")
        return True
    except Exception as e:
        if os.path.exists(tmp_path):
            try:
                os.remove(tmp_path)
            except Exception:
                pass
        logger.error(f"system.db 自动加密失败: {e}")
        return False


def derive_user_db_key(password: str, salt_hex: str = None) -> str:
    """
    派生 user.db 的 SQLCipher 密钥
    key = PBKDF2(password, USER_DB_SALT, 100000)
    返回 hex 字符串
    """
    salt = bytes.fromhex(salt_hex or USER_DB_SALT)
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA512(),
        length=32,
        salt=salt,
        iterations=100_000,
        backend=default_backend(),
    )
    key = kdf.derive(password.encode("utf-8"))
    return key.hex()


def derive_user_db_key_from_cached_password(salt_hex: str = None) -> str | None:
    """用缓存密码派生 user.db 密钥，无缓存返回 None"""
    pwd = get_password()
    if not pwd:
        return None
    return derive_user_db_key(pwd, salt_hex)


def encrypt_bytes(plaintext: bytes, password: str) -> bytes:
    """加密字节，返回 salt(16) + iv(16) + ciphertext"""
    salt = os.urandom(SALT_SIZE)
    iv = os.urandom(IV_SIZE)
    key = derive_key(password, salt)
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()
    pad_len = 16 - (len(plaintext) % 16)
    padded = plaintext + bytes([pad_len] * pad_len)
    ciphertext = encryptor.update(padded) + encryptor.finalize()
    return salt + iv + ciphertext


def decrypt_bytes(data: bytes, password: str) -> bytes:
    """解密 salt(16) + iv(16) + ciphertext，返回明文"""
    salt = data[:SALT_SIZE]
    iv = data[SALT_SIZE : SALT_SIZE + IV_SIZE]
    ciphertext = data[SALT_SIZE + IV_SIZE :]
    key = derive_key(password, salt)
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    decryptor = cipher.decryptor()
    padded = decryptor.update(ciphertext) + decryptor.finalize()
    pad_len = padded[-1]
    return padded[:-pad_len]


def encrypt_file(filepath: str, password: str) -> str:
    """加密单个文件，返回 .enc 文件路径，删除原明文文件"""
    with open(filepath, "rb") as f:
        plaintext = f.read()
    encrypted = encrypt_bytes(plaintext, password)
    enc_path = filepath + ".enc"
    with open(enc_path, "wb") as f:
        f.write(encrypted)
    os.remove(filepath)
    return enc_path


def decrypt_file(enc_filepath: str, password: str) -> bytes:
    """解密 .enc 文件，返回明文内容（bytes，不落盘）"""
    with open(enc_filepath, "rb") as f:
        data = f.read()
    return decrypt_bytes(data, password)


def is_encrypted(filepath: str) -> bool:
    """判断文件是否为加密文件"""
    return filepath.endswith(".enc")


def read_text_file(filepath: str, password: str = None) -> str:
    """
    统一读取文本文件（自动判断加密/明文）
    - .enc 文件：解密后返回文本
    - 普通 .md 文件：直接读取
    - password 为 None 时：优先用缓存密码，兜底用固定应用密钥
    """
    pwd = password or get_password() or APP_DEFAULT_KEY
    if filepath.endswith(".enc"):
        raw = decrypt_file(filepath, pwd)
        return raw.decode("utf-8")
    else:
        with open(filepath, "r", encoding="utf-8") as f:
            return f.read()


def encrypt_dir(directory: str, password: str, extensions: tuple = (".md",)) -> int:
    """
    加密目录下所有指定扩展名的文件
    返回加密的文件数
    """
    count = 0
    for root, dirs, files in os.walk(directory):
        for fname in files:
            if any(fname.endswith(ext) for ext in extensions) and not fname.endswith(".enc"):
                filepath = os.path.join(root, fname)
                try:
                    encrypt_file(filepath, password)
                    count += 1
                except Exception as e:
                    logger.error(f"加密失败 {filepath}: {e}")
    logger.info(f"目录加密完成: {directory}, 加密 {count} 个文件")
    return count


def re_encrypt_dir(directory: str, old_password: str, new_password: str) -> int:
    """
    修改密码后重新加密目录下所有 .enc 文件
    返回重新加密的文件数
    """
    count = 0
    for root, dirs, files in os.walk(directory):
        for fname in files:
            if fname.endswith(".enc"):
                filepath = os.path.join(root, fname)
                try:
                    plaintext = decrypt_file(filepath, old_password)
                    # 重新加密
                    encrypted = encrypt_bytes(plaintext, new_password)
                    with open(filepath, "wb") as f:
                        f.write(encrypted)
                    count += 1
                except Exception as e:
                    logger.error(f"重新加密失败 {filepath}: {e}")
    logger.info(f"目录重新加密完成: {directory}, 重新加密 {count} 个文件")
    return count


def scan_dir(directory: str, extensions: tuple = (".md",)) -> list[str]:
    """
    扫描目录，返回文件路径列表
    同时支持加密 (.enc) 和明文文件
    """
    results = []
    for root, dirs, files in os.walk(directory):
        for fname in sorted(files):
            # 匹配 .md.enc 或 .md
            if any(fname.endswith(ext + ".enc") for ext in extensions):
                results.append(os.path.join(root, fname))
            elif any(fname.endswith(ext) for ext in extensions) and not fname.endswith(".enc"):
                results.append(os.path.join(root, fname))
    return results
