"""
AutoPOE2 - Client Build Fingerprint (TASK-RE-PIPELINE-2026 / Invariant INV-BUILD-01)

Đối xứng 1:1 với `src/core/memory/pe_fingerprint.cpp` (C++23 Core Engine).
Đọc PE header + bảng section của PathOfExile.exe TRỰC TIẾP TỪ ĐĨA:
  - KHÔNG cần attach tiến trình, KHÔNG cần quyền Administrator.
  - KHÔNG cần game đang chạy (chạy được trong CI / User Mode).

VAI TRÒ TRONG PIPELINE RE (Phương án C):
  Mọi tri thức reverse engineering (static root RVA, AOB signature, offset
  component) BẮT BUỘC được khóa theo build fingerprint. Không có bằng chứng
  khớp build thì KHÔNG được dùng tri thức cũ (chống silent-bypass).

BẰNG CHỨNG THỰC ĐỊA (10/09/2026, bin/Release/memprobe_report.txt):
  BUILD CŨ  (5 phiên): .text VSize=0x2E840BE, .rdata VA=0x2E86000, .data VA=0x3A03000
  BUILD MỚI (1 phiên): .text VSize=0x2E83B6E, .rdata VA=0x2E85000, .data VA=0x3A02000
  -> .data dịch -0x1000 làm 8 static root RVA hardcode lệch hoàn toàn,
     cả 4 AOB pattern [MISS], và player_addr = 0x0.

Cách dùng:
  python tools/re_pipeline/fingerprint.py                 # in fingerprint client
  python tools/re_pipeline/fingerprint.py --json data/builds/<BUILD_ID>.json
  python tools/re_pipeline/fingerprint.py --exe "D:\\Games\\...\\PathOfExile.exe"

Xem: docs/development/25_static_reverse_engineering_pipeline_and_offset_registry.md
"""

from __future__ import annotations

import argparse
import hashlib
import json
import struct
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
    sys.stderr.reconfigure(encoding="utf-8")

# ----------------------------------------------------------------------
# Hằng số PE (đồng bộ với windows.h)
# ----------------------------------------------------------------------
IMAGE_DOS_SIGNATURE = 0x5A4D          # 'MZ'
IMAGE_NT_SIGNATURE = 0x00004550       # 'PE\0\0'
IMAGE_FILE_MACHINE_AMD64 = 0x8664
PE32_PLUS_MAGIC = 0x20B

IMAGE_SCN_CNT_CODE = 0x00000020
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040
IMAGE_SCN_MEM_EXECUTE = 0x20000000
IMAGE_SCN_MEM_READ = 0x40000000
IMAGE_SCN_MEM_WRITE = 0x80000000

# Offset trong Optional Header PE32+
_SIZE_OF_IMAGE_OFFSET = 56
_SIZE_OF_HEADERS_OFFSET = 60

# ----------------------------------------------------------------------
# FNV-1a 64-bit — BẮT BUỘC khớp byte-for-byte với C++
# ClientBuildId::SectionLayoutHash() để hai tầng không lệch pha (Rule 10).
# ----------------------------------------------------------------------
FNV_OFFSET_BASIS = 0xCBF29CE484222325
FNV_PRIME = 0x00000100000001B3
_UINT64_MASK = 0xFFFFFFFFFFFFFFFF

# Đồng bộ với src/assistant_tool/client_launcher.py DEFAULT_STANDALONE_PATHS
# và pe_fingerprint::LocatePoe2Executable()
DEFAULT_CANDIDATE_PATHS = [
    r"C:\Program Files (x86)\Grinding Gear Games\Path of Exile 2\PathOfExile.exe",
    r"C:\Program Files (x86)\Grinding Gear Games\Path of Exile 2\PathOfExile_x64.exe",
    r"C:\Program Files\Grinding Gear Games\Path of Exile 2\PathOfExile.exe",
    r"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\PathOfExile.exe",
    r"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\PathOfExileSteam.exe",
    r"D:\Games\Path of Exile 2\PathOfExile.exe",
]


def fnv1a(current: int, data: bytes) -> int:
    """FNV-1a 64-bit. Phải cho kết quả identical với Fnv1a() trong C++."""
    h = current & _UINT64_MASK
    for byte in data:
        h ^= byte
        h = (h * FNV_PRIME) & _UINT64_MASK
    return h


@dataclass
class SectionInfo:
    """Một section trong bảng section của PE (đối xứng C++ SectionInfo)."""

    name: str = ""
    virtual_address: int = 0
    virtual_size: int = 0
    raw_size: int = 0
    raw_pointer: int = 0
    characteristics: int = 0

    @property
    def executable(self) -> bool:
        return bool(self.characteristics & IMAGE_SCN_MEM_EXECUTE)

    @property
    def writable(self) -> bool:
        return bool(self.characteristics & IMAGE_SCN_MEM_WRITE)

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "virtual_address": f"0x{self.virtual_address:X}",
            "virtual_size": f"0x{self.virtual_size:X}",
            "raw_size": f"0x{self.raw_size:X}",
            "raw_pointer": f"0x{self.raw_pointer:X}",
            "characteristics": f"0x{self.characteristics:X}",
            "executable": self.executable,
            "writable": self.writable,
        }


@dataclass
class ClientBuildId:
    """Định danh duy nhất của một bản build client (đối xứng C++ ClientBuildId)."""

    valid: bool = False
    machine: int = 0
    number_of_sections: int = 0
    time_date_stamp: int = 0
    size_of_image: int = 0
    size_of_headers: int = 0
    file_size: int = 0
    path: str = ""
    sections: List[SectionInfo] = field(default_factory=list)
    file_sha256: str = ""
    error: str = ""

    @property
    def short_id(self) -> str:
        """Mã ngắn dùng làm khóa chính của Offset Registry (vd '6A9E477A')."""
        return f"{self.time_date_stamp:08X}"

    def section_layout_hash(self) -> int:
        """Băm FNV-1a trên bảng section.

        THỨ TỰ BYTE PHẢI KHỚP 100% với C++ ClientBuildId::SectionLayoutHash():
          numberOfSections (uint16 LE) -> sizeOfImage (uint32 LE) ->
          mỗi section: name (ASCII, không null) -> virtualAddress -> virtualSize
          -> rawSize -> characteristics (mỗi trường uint32 LE).
        """
        h = FNV_OFFSET_BASIS
        h = fnv1a(h, struct.pack("<H", self.number_of_sections))
        h = fnv1a(h, struct.pack("<I", self.size_of_image))
        for section in self.sections:
            h = fnv1a(h, section.name.encode("ascii", "replace"))
            h = fnv1a(h, struct.pack("<I", section.virtual_address))
            h = fnv1a(h, struct.pack("<I", section.virtual_size))
            h = fnv1a(h, struct.pack("<I", section.raw_size))
            h = fnv1a(h, struct.pack("<I", section.characteristics))
        return h

    def same_build_as(self, other: "ClientBuildId") -> bool:
        if not self.valid or not other.valid:
            return False
        return (
            self.time_date_stamp == other.time_date_stamp
            and self.size_of_image == other.size_of_image
            and self.section_layout_hash() == other.section_layout_hash()
        )

    def find_section(self, name: str) -> Optional[SectionInfo]:
        for section in self.sections:
            if section.name == name:
                return section
        return None

    def to_dict(self) -> dict:
        return {
            "schema": "autopoe2.client_build_id/v1",
            "valid": self.valid,
            "build_id": self.short_id,
            "path": self.path,
            "machine": f"0x{self.machine:04X}",
            "number_of_sections": self.number_of_sections,
            "time_date_stamp": f"0x{self.time_date_stamp:08X}",
            "time_date_stamp_utc": (
                datetime.fromtimestamp(self.time_date_stamp, tz=timezone.utc).strftime(
                    "%d/%m/%Y %H:%M:%S"
                )
                if self.time_date_stamp
                else ""
            ),
            "size_of_image": f"0x{self.size_of_image:X}",
            "size_of_headers": f"0x{self.size_of_headers:X}",
            "file_size": self.file_size,
            "file_sha256": self.file_sha256,
            "section_layout_hash": f"0x{self.section_layout_hash():016X}",
            "sections": [s.to_dict() for s in self.sections],
        }


def locate_poe2_executable(explicit: Optional[str] = None) -> Optional[Path]:
    """Định vị PathOfExile.exe (đối xứng C++ LocatePoe2Executable)."""
    if explicit:
        candidate = Path(explicit)
        return candidate if candidate.is_file() else None

    for raw in DEFAULT_CANDIDATE_PATHS:
        candidate = Path(raw)
        if candidate.is_file():
            return candidate
    return None


def load_pe_headers(path: Path) -> ClientBuildId:
    """Parse DOS/COFF/Optional header + bảng section. Không băm (nhanh)."""
    build = ClientBuildId(path=str(path))

    if not path.is_file():
        build.error = f"Không tìm thấy tệp: {path}"
        return build

    try:
        data = path.read_bytes()
    except OSError as exc:
        build.error = f"Không đọc được tệp: {exc}"
        return build

    build.file_size = len(data)

    if len(data) < 64:
        build.error = "Tệp quá nhỏ, không phải PE hợp lệ"
        return build

    (dos_magic,) = struct.unpack_from("<H", data, 0)
    if dos_magic != IMAGE_DOS_SIGNATURE:
        build.error = "DOS header không hợp lệ (magic != 'MZ')"
        return build

    (e_lfanew,) = struct.unpack_from("<i", data, 0x3C)
    if e_lfanew <= 0 or e_lfanew + 24 > len(data):
        build.error = "e_lfanew trỏ ra ngoài phạm vi tệp"
        return build

    (
        signature,
        machine,
        number_of_sections,
        time_date_stamp,
        _ptr_sym,
        _num_sym,
        size_of_optional_header,
        _characteristics,
    ) = struct.unpack_from("<IHHIIIHH", data, e_lfanew)

    if signature != IMAGE_NT_SIGNATURE:
        build.error = "PE signature không hợp lệ"
        return build
    if machine != IMAGE_FILE_MACHINE_AMD64:
        build.error = "Machine không phải AMD64 (POE2 là tiến trình x64)"
        return build
    if number_of_sections == 0 or number_of_sections > 96:
        build.error = f"Số lượng section bất thường: {number_of_sections}"
        return build
    if size_of_optional_header < 2:
        build.error = "sizeOfOptionalHeader quá nhỏ"
        return build

    build.machine = machine
    build.number_of_sections = number_of_sections
    build.time_date_stamp = time_date_stamp

    optional_start = e_lfanew + 24
    (optional_magic,) = struct.unpack_from("<H", data, optional_start)
    if optional_magic != PE32_PLUS_MAGIC:
        build.error = f"Optional header không phải PE32+ (magic=0x{optional_magic:X})"
        return build
    if optional_start + _SIZE_OF_HEADERS_OFFSET + 4 > len(data):
        build.error = "Optional header trỏ ra ngoài phạm vi tệp"
        return build

    (build.size_of_image,) = struct.unpack_from(
        "<I", data, optional_start + _SIZE_OF_IMAGE_OFFSET
    )
    (build.size_of_headers,) = struct.unpack_from(
        "<I", data, optional_start + _SIZE_OF_HEADERS_OFFSET
    )

    section_table_start = optional_start + size_of_optional_header
    for index in range(number_of_sections):
        offset = section_table_start + index * 40
        if offset + 40 > len(data):
            build.error = f"Bảng section bị cắt ngắn (thiếu section #{index})"
            build.sections.clear()
            return build

        raw_name = data[offset : offset + 8]
        (
            virtual_size,
            virtual_address,
            size_of_raw_data,
            pointer_to_raw_data,
            _ptr_reloc,
            _ptr_linenum,
            _num_reloc,
            _num_linenum,
            characteristics,
        ) = struct.unpack_from("<IIIIIIHHI", data, offset + 8)

        build.sections.append(
            SectionInfo(
                name=raw_name.rstrip(b"\x00").decode("ascii", "replace"),
                virtual_address=virtual_address,
                virtual_size=virtual_size,
                raw_size=size_of_raw_data,
                raw_pointer=pointer_to_raw_data,
                characteristics=characteristics,
            )
        )

    build.valid = True
    return build


def load_pe_headers_with_hash(path: Path) -> ClientBuildId:
    """Parse header + SHA-256 toàn tệp (dùng khi ghi build record)."""
    build = load_pe_headers(path)
    if not build.valid:
        return build

    hasher = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            hasher.update(chunk)
    build.file_sha256 = hasher.hexdigest()
    return build


def main() -> int:
    parser = argparse.ArgumentParser(
        description="AutoPOE2 Client Build Fingerprint (INV-BUILD-01)"
    )
    parser.add_argument("--exe", default=None, help="Đường dẫn PathOfExile.exe")
    parser.add_argument(
        "--json",
        default=None,
        help="Ghi build record ra JSON (khuyến nghị: data/builds/<BUILD_ID>.json)",
    )
    parser.add_argument(
        "--hash", action="store_true", help="Băm SHA-256 toàn tệp (~78MB)"
    )
    args = parser.parse_args()

    exe_path = locate_poe2_executable(args.exe)
    if exe_path is None:
        print(
            "[ERROR] Không tìm thấy PathOfExile.exe. Dùng --exe <đường dẫn>.",
            file=sys.stderr,
        )
        return 2

    build = load_pe_headers_with_hash(exe_path) if args.hash else load_pe_headers(exe_path)
    if not build.valid:
        print(f"[ERROR] Parse PE thất bại: {build.error}", file=sys.stderr)
        return 1

    payload = build.to_dict()
    print("=== AutoPOE2 Client Build Fingerprint (INV-BUILD-01) ===")
    print(f"  Path              : {build.path}")
    print(f"  BuildId           : {build.short_id}")
    print(
        f"  TimeDateStamp     : 0x{build.time_date_stamp:08X} "
        f"({payload['time_date_stamp_utc']} UTC)"
    )
    print(f"  Machine           : 0x{build.machine:04X} (AMD64)")
    print(f"  NumberOfSections  : {build.number_of_sections}")
    print(f"  SizeOfImage       : 0x{build.size_of_image:X}")
    print(f"  SizeOfHeaders     : 0x{build.size_of_headers:X}")
    print(f"  FileSize          : {build.file_size:,} bytes")
    print(f"  SectionLayoutHash : 0x{build.section_layout_hash():016X}")
    if build.file_sha256:
        print(f"  FileSHA256        : {build.file_sha256}")
    print("  Sections:")
    for section in build.sections:
        print(
            f"    {section.name:<9} VA=0x{section.virtual_address:08X} "
            f"VSize=0x{section.virtual_size:08X} RawSize=0x{section.raw_size:08X} "
            f"Char=0x{section.characteristics:08X}"
        )

    if args.json:
        out_path = Path(args.json)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(
            json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
        )
        print(f"[OK] Đã ghi build record -> {out_path}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
