"""纯 Python 微信 4.x Linux SQLCipher/WCDB 解密器。

参考 wechat-cli-main 项目实现，针对微信 4.x Linux 加密数据库：
- 页面大小 4096 字节
- 第 1 页前 16 字节为 salt
- 保留区 80 字节：IV(16) + HMAC-SHA512(64)
- AES-256-CBC 解密
- HMAC-SHA512 页校验
- mac_salt = salt XOR 0x3A，PBKDF2-HMAC-SHA512 2 轮派生 mac_key
"""

from __future__ import annotations

import hashlib
import hmac
import os
import shutil
import struct

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes


# SQLite 文件头
SQLITE_HEADER = b"SQLite format 3\x00"
PAGE_SIZE = 4096
KEY_SIZE = 32
SALT_SIZE = 16
IV_SIZE = 16
HMAC_SIZE = 64  # SHA512 HMAC
RESERVE_SIZE = 80  # IV(16) + HMAC(64)
ROUND_COUNT = 256000  # PBKDF2 迭代轮数（微信 4.x Linux 使用 SHA512）
MAC_SALT_XOR = 0x3A  # mac_salt = salt ^ 0x3A

# WAL 文件常量
WAL_HEADER_SIZE = 32
WAL_FRAME_HEADER_SIZE = 24


def _derive_enc_key(key_material: bytes, salt: bytes) -> bytes:
    """从 32 字节 key material 派生 AES 加密密钥（PBKDF2-SHA512 256000 轮）。"""
    return hashlib.pbkdf2_hmac("sha512", key_material, salt, ROUND_COUNT, dklen=KEY_SIZE)


def _derive_mac_key(enc_key: bytes, salt: bytes) -> bytes:
    """派生微信 4.x 页 HMAC 密钥（mac_salt = salt XOR 0x3A，PBKDF2-SHA512 2 轮）。"""
    mac_salt = bytes(b ^ MAC_SALT_XOR for b in salt)
    return hashlib.pbkdf2_hmac("sha512", enc_key, mac_salt, 2, dklen=KEY_SIZE)


def _compute_page_hmac(mac_key: bytes, page: bytes, page_num: int) -> bytes:
    """计算页面 HMAC-SHA512。

    微信 4.x Linux 页 HMAC 计算范围（含 IV）：
    - 第 1 页：salt 之后到 HMAC 之前的数据（即 offset=16 到 data_end=4096-80+16=4032）
    - 其他页：从头开始到 HMAC 之前的数据（offset=0 到 data_end=4032）
    最后追加 4 字节小端页号。
    """
    offset = SALT_SIZE if page_num == 1 else 0
    data_end = PAGE_SIZE - RESERVE_SIZE + IV_SIZE
    mac = hmac.new(mac_key, digestmod=hashlib.sha512)
    mac.update(page[offset:data_end])
    mac.update(page_num.to_bytes(4, "little"))
    return mac.digest()


def _resolve_page1_key_material(
    key_material: bytes, page1: bytes
) -> tuple[bytes, bytes, str] | None:
    """检测输入密钥是否匹配第 1 页 HMAC。

    先尝试把输入当作已派生的 enc_key 直接验证；失败则当作 raw key_material
    派生 enc_key 后再验证。返回 (enc_key, mac_key, mode) 或 None。
    """
    if len(page1) < PAGE_SIZE:
        return None

    salt = page1[:SALT_SIZE]
    stored_page1_hmac = page1[PAGE_SIZE - HMAC_SIZE: PAGE_SIZE]

    # 尝试 1：输入就是 enc_key
    mac_key = _derive_mac_key(key_material, salt)
    if hmac.compare_digest(stored_page1_hmac, _compute_page_hmac(mac_key, page1, 1)):
        return key_material, mac_key, "enc_key"

    # 尝试 2：输入是 raw key_material，需派生 enc_key
    enc_key = _derive_enc_key(key_material, salt)
    mac_key = _derive_mac_key(enc_key, salt)
    if hmac.compare_digest(stored_page1_hmac, _compute_page_hmac(mac_key, page1, 1)):
        return enc_key, mac_key, "key_material"

    return None


def _decrypt_page(enc_key: bytes, page: bytes, page_num: int) -> bytes:
    """使用 AES-256-CBC 解密单页。

    明文 SQLite 页不携带加密保留区，通过零填充保留页大小不变。
    """
    iv = page[PAGE_SIZE - RESERVE_SIZE: PAGE_SIZE - RESERVE_SIZE + IV_SIZE]
    offset = SALT_SIZE if page_num == 1 else 0
    encrypted_page = page[offset: PAGE_SIZE - RESERVE_SIZE]

    cipher = Cipher(
        algorithms.AES(enc_key),
        modes.CBC(iv),
        backend=default_backend(),
    )
    decryptor = cipher.decryptor()
    decrypted_page = decryptor.update(encrypted_page) + decryptor.finalize()

    if page_num == 1:
        return SQLITE_HEADER + decrypted_page + (b"\x00" * RESERVE_SIZE)
    return decrypted_page + (b"\x00" * RESERVE_SIZE)


def decrypt_wal(enc_key: bytes, wal_path: str, output_db_path: str) -> int:
    """把加密的 WAL 文件合并到已解密的明文 DB 中。

    微信 4.x Linux 使用 SQLCipher WAL 格式，每帧结构为：
    - frame header (24 bytes): pgno(4) + commit_size(4) + salt1(4) + salt2(4) + checksum(8)
    - frame page (4096 bytes): 加密页数据

    只合并与 WAL header 中 salt 匹配的帧，避免应用不完整数据。

    Args:
        enc_key: 32 字节 AES 加密密钥
        wal_path: 加密 WAL 文件路径
        output_db_path: 已解密的明文 DB 路径（会被原地 patch）

    Returns:
        int: 成功合并的帧数
    """
    if not os.path.exists(wal_path):
        return 0

    wal_size = os.path.getsize(wal_path)
    if wal_size <= WAL_HEADER_SIZE:
        return 0

    patched = 0
    with open(wal_path, "rb") as wf, open(output_db_path, "r+b") as df:
        wal_header = wf.read(WAL_HEADER_SIZE)
        if len(wal_header) < WAL_HEADER_SIZE:
            return 0

        wal_salt1 = struct.unpack(">I", wal_header[16:20])[0]
        wal_salt2 = struct.unpack(">I", wal_header[20:24])[0]
        frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE

        while wf.tell() + frame_size <= wal_size:
            frame_header = wf.read(WAL_FRAME_HEADER_SIZE)
            if len(frame_header) < WAL_FRAME_HEADER_SIZE:
                break

            pgno = struct.unpack(">I", frame_header[0:4])[0]
            frame_salt1 = struct.unpack(">I", frame_header[8:12])[0]
            frame_salt2 = struct.unpack(">I", frame_header[12:16])[0]
            encrypted_page = wf.read(PAGE_SIZE)
            if len(encrypted_page) < PAGE_SIZE:
                break

            # 过滤异常帧
            if pgno == 0 or pgno > 10000000:
                continue
            # salt 不匹配说明 WAL 已轮换或数据不完整，跳过
            if frame_salt1 != wal_salt1 or frame_salt2 != wal_salt2:
                continue

            decrypted_page = _decrypt_page(enc_key, encrypted_page, pgno)
            df.seek((pgno - 1) * PAGE_SIZE)
            df.write(decrypted_page)
            patched += 1

    return patched


class Decryptor:
    """微信 4.x Linux SQLCipher/WCDB 数据库解密器。"""

    def __init__(self, key_hex: str):
        """初始化解密器，key_hex 为 64 位十六进制密钥。

        Args:
            key_hex: 64 位十六进制字符串（32 字节 enc_key 或 raw key_material）

        Raises:
            ValueError: 密钥长度不为 64 或非有效十六进制
        """
        if len(key_hex) != 64:
            raise ValueError("密钥必须是 64 位十六进制字符串")

        try:
            self.key_bytes = bytes.fromhex(key_hex)
        except ValueError:
            raise ValueError("密钥必须是有效的十六进制字符串")

        self._key_hex = key_hex
        self._enc_key: bytes | None = None
        self._mac_key: bytes | None = None
        self._key_mode: str = ""

    def _resolve(self, page1: bytes) -> bool:
        """用第 1 页解析出 enc_key / mac_key / mode，成功返回 True。"""
        resolved = _resolve_page1_key_material(self.key_bytes, page1)
        if resolved is None:
            return False
        self._enc_key, self._mac_key, self._key_mode = resolved
        return True

    def decrypt_db(self, enc_db_path: str, output_path: str) -> dict:
        """解密微信 4.x 加密数据库到明文 SQLite 文件。

        采用流式分页读写：
        - 仅把当前页保留在内存，不一次性读入整库。
        - 明文 SQLite 直接流式复制。

        Args:
            enc_db_path: 加密 DB 文件路径
            output_path: 解密后明文 SQLite 文件路径

        Returns:
            dict: 解密结果统计信息
        """
        result: dict = {
            "success": False,
            "key_mode": "",
            "total_pages": 0,
            "successful_pages": 0,
            "failed_pages": 0,
            "copied_as_sqlite": False,
            "error": "",
        }

        try:
            file_size = os.path.getsize(enc_db_path)
        except Exception as e:
            result["error"] = f"stat_error: {e}"
            return result

        if file_size < PAGE_SIZE:
            result["error"] = "file_too_small"
            return result

        try:
            with open(enc_db_path, "rb") as f:
                page1 = f.read(PAGE_SIZE)
        except Exception as e:
            result["error"] = f"read_error: {e}"
            return result

        if page1.startswith(SQLITE_HEADER):
            try:
                with open(enc_db_path, "rb") as src, open(output_path, "wb") as dst:
                    shutil.copyfileobj(src, dst)
            except Exception as e:
                result["error"] = f"write_error: {e}"
                return result
            result["success"] = True
            result["copied_as_sqlite"] = True
            return result

        if not self._resolve(page1):
            result["error"] = "key_mismatch"
            return result

        result["key_mode"] = self._key_mode

        total_pages = (file_size + PAGE_SIZE - 1) // PAGE_SIZE
        result["total_pages"] = total_pages
        successful_pages = 0
        failed_pages = 0

        try:
            with open(enc_db_path, "rb") as src, open(output_path, "wb") as dst:
                for page_num in range(1, total_pages + 1):
                    page = src.read(PAGE_SIZE)
                    if not page:
                        break
                    if len(page) < PAGE_SIZE:
                        page = page + (b"\x00" * (PAGE_SIZE - len(page)))

                    # 直接解密，不做逐页 HMAC 验证
                    # 参考 wechat-cli-main：微信运行时 DB 页面可能因 WAL 并发写入
                    # 等原因 HMAC 不匹配，但 key 本身正确（page1 验证已通过）。
                    # 逐页 HMAC 验证导致大量页面误判为损坏，实际 AES 解密仍可得到有效数据
                    try:
                        dst.write(_decrypt_page(self._enc_key, page, page_num))
                        successful_pages += 1
                    except Exception:
                        failed_pages += 1
                        dst.write(b"\x00" * PAGE_SIZE)
        except Exception as e:
            result["error"] = f"write_error: {e}"
            return result

        result["successful_pages"] = successful_pages
        result["failed_pages"] = failed_pages
        result["success"] = True
        return result

    def verify_key(self, enc_db_path: str) -> bool:
        """便捷方法：验证 key 是否匹配指定 DB 文件。"""
        try:
            with open(enc_db_path, "rb") as f:
                data = f.read(PAGE_SIZE)
        except Exception:
            return False

        if len(data) < PAGE_SIZE:
            return False

        if data.startswith(SQLITE_HEADER):
            return True

        return self._resolve(data)

    def decrypt_wal(self, wal_path: str, output_db_path: str) -> int:
        """使用已解析的 enc_key 合并 WAL 到明文 DB。

        必须先调用 decrypt_db 或 verify_key 解析出 enc_key。
        """
        if self._enc_key is None:
            raise RuntimeError("enc_key 尚未解析，请先调用 decrypt_db 或 verify_key")
        return decrypt_wal(self._enc_key, wal_path, output_db_path)
