"""
license_client.py
=================
Python integration layer for license_core.dll.

Security design:
- ALL validation logic lives in the C++ DLL
- Python NEVER checks a boolean - it uses the runtime_key
- If the DLL returns a poisoned key, Python's crypto/compute ops will silently fail

Usage:
    from license_client import LicenseClient

    lic = LicenseClient()
    key = lic.activate("XXXX-YYYY-ZZZZ-WWWW", "INIContract")
    result = lic.compute_something(key, my_data)
"""

import ctypes
import ctypes.util
import hashlib
import os
import sys
import struct
import platform
from pathlib import Path
from typing import Optional
import json
from datetime import datetime

OFFICIAL_DLL_HASHES = {
    "INI_core.dll": {
        "ddb325c52aea948417ed6f57351c4a7b4443801745b6efa7e1f19a03cb9b8c8c",
    },
}


# =============================================================================
# DLL Result Structure - MUST match C++ struct layout
# =============================================================================
class LicenseResult(ctypes.Structure):
    _pack_ = 1
    _fields_ = [
        ("status_code",    ctypes.c_int32),    # 0=ok, 1=invalid, 2=expired, 3=device_limit, 4=offline_degraded
        ("runtime_key",    ctypes.c_uint32),   # Derived key - WRONG if invalid (poison)
        ("plan_flags",     ctypes.c_uint8),    # Bitfield: bit0=pro, bit1=vip, bit2=online_verified
        ("days_remaining", ctypes.c_int32),    # Days left - wrong if invalid
        ("plan_name",      ctypes.c_char * 32) # plan_id null-terminated, vd "office_lifetime"
    ]

    @property
    def is_pro(self) -> bool:
        return bool(self.plan_flags & 0x01)

    @property
    def is_vip(self) -> bool:
        return bool(self.plan_flags & 0x02)

    @property
    def is_online_verified(self) -> bool:
        return bool(self.plan_flags & 0x04)

    @property
    def plan(self) -> str:
        return self.plan_name.decode("utf-8", errors="replace").rstrip("\x00")


# =============================================================================
# DLL Integrity Verification
# =============================================================================
def _compute_dll_hash(dll_path: Path) -> str:
    """Compute SHA256 hash of DLL file for integrity verification."""
    sha256 = hashlib.sha256()
    with open(dll_path, 'rb') as f:
        for chunk in iter(lambda: f.read(4096), b''):
            sha256.update(chunk)
    return sha256.hexdigest()


def _get_trusted_hashes_path() -> Path:
    """Get path to trusted DLL hashes file (~/.ini_license/trusted_dlls.json)."""
    config_dir = Path.home() / ".ini_license"
    config_dir.mkdir(exist_ok=True)
    return config_dir / "trusted_dlls.json"


def _load_trusted_hashes() -> dict:
    """Load trusted DLL hashes from config file."""
    hashes_file = _get_trusted_hashes_path()
    if hashes_file.exists():
        try:
            with open(hashes_file, 'r') as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            return {}
    return {}


def _save_trusted_hash(dll_name: str, dll_hash: str) -> None:
    """Save trusted DLL hash to config file."""
    hashes_file = _get_trusted_hashes_path()
    hashes = _load_trusted_hashes()
    hashes[dll_name] = {
        "hash": dll_hash,
        "trusted_at": datetime.now().isoformat(),
        "warn": "⚠️  Only trust DLLs downloaded from official sources"
    }
    with open(hashes_file, 'w') as f:
        json.dump(hashes, f, indent=2)


def _verify_dll_integrity(dll_path: Path, enforce: bool = True) -> bool:
    """Verify DLL integrity against trusted hash.
    
    Args:
        dll_path: Path to the DLL file
        enforce: If True, raise error on mismatch; if False, just warn
    
    Returns:
        True if hash matches or verification skipped, False if mismatch
        
    Raises:
        RuntimeError if enforce=True and hash doesn't match
    """
    dll_hash = _compute_dll_hash(dll_path)
    dll_name = dll_path.name
    
    trusted_hashes = _load_trusted_hashes()
    
    if dll_name in trusted_hashes:
        expected_hash = trusted_hashes[dll_name].get("hash", "")
        if dll_hash != expected_hash:
            official_hashes = OFFICIAL_DLL_HASHES.get(dll_name, set())
            if dll_hash.lower() in official_hashes:
                _save_trusted_hash(dll_name, dll_hash)
                return True
            msg = (
                f"🔴 DLL INTEGRITY CHECK FAILED for {dll_name}\n"
                f"Expected: {expected_hash}\n"
                f"Got:      {dll_hash}\n"
                f"\nThe DLL may have been tampered with or replaced!\n"
                f"Only load DLLs from trusted sources."
            )
            if enforce:
                raise RuntimeError(msg)
            else:
                print(f"⚠️  WARNING: {msg}")
                return False
        return True
    else:
        # First time seeing this DLL - ask user to trust it
        print(f"ℹ️  New DLL detected: {dll_name}")
        print(f"SHA256: {dll_hash}")
        print(f"Location: {dll_path}")
        
        if enforce:
            _save_trusted_hash(dll_name, dll_hash)
            print(f"✅ Trusted hash saved to {_get_trusted_hashes_path()}")
            print(f"   To move to production, review and commit this file to version control.")
        
        return True


# =============================================================================
# DLL Loader
# =============================================================================
def _load_dll() -> ctypes.CDLL:
    """Locate and load license_core.dll with integrity verification."""
    candidates = [
        Path(sys.executable).parent / "INI_core.dll",  # same dir as EXE
        Path(__file__).parent / "INI_core.dll",         # same dir as this .py
        Path("INI_core.dll"),                           # CWD
    ]
    if getattr(sys, "frozen", False):
        meipass = Path(getattr(sys, "_MEIPASS", ""))
        if meipass:
            candidates.insert(1, meipass / "INI_core.dll")
            candidates.insert(2, meipass / "python_client_PhanMemHopDong" / "INI_core.dll")

    for path in candidates:
        if path.exists():
            # Verify DLL integrity (enforce in production)
            enforce_check = os.environ.get("INI_LICENSE_ENFORCE_DLL_CHECK", "1") == "1"
            if not _verify_dll_integrity(path, enforce=enforce_check):
                if enforce_check:
                    raise RuntimeError(f"DLL integrity check failed for {path}")
                # If not enforced, continue anyway (dev mode)
            
            try:
                dll = ctypes.CDLL(str(path))
                _configure_dll(dll)
                return dll
            except OSError as e:
                raise RuntimeError(f"Failed to load DLL from {path}: {e}")

    raise FileNotFoundError(
        "INI_core.dll not found. Place it in the same directory as this application."
    )


def _configure_dll(dll: ctypes.CDLL) -> None:
    """Set argument and return types for exported functions."""
    # verify_and_get_key(license_key: char*, app_id: char*) -> LicenseResult
    dll.verify_and_get_key.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    dll.verify_and_get_key.restype  = LicenseResult

    # get_machine_id(out_buf: char*, buf_len: int) -> void
    dll.get_machine_id.argtypes = [ctypes.c_char_p, ctypes.c_int]
    dll.get_machine_id.restype  = None

    # register_customer_once_json(input_json: char*, out_json: char*, out_len: int) -> int
    if hasattr(dll, "register_customer_once_json"):
        dll.register_customer_once_json.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
        dll.register_customer_once_json.restype = ctypes.c_int


# =============================================================================
# LicenseClient - Main Integration Class
# =============================================================================
class LicenseClient:
    """
    Secure license client that delegates ALL validation to the C++ DLL.

    SECURITY DESIGN:
    - Python code never sees a "valid/invalid" boolean
    - It receives a runtime_key that is only correct if license is valid
    - All feature gates should use process(key, data) pattern,
      not if/else boolean checks
    """

    _EXPECTED_KEY_MAGIC = 0x1337  # Low 16 bits of a valid runtime_key (per your derivation)
    # NOTE: In production, remove this constant and use the key directly
    # in your processing functions. Don't validate it here.

    def __init__(self, app_id: str = "INIContract"):
        self._app_id = app_id
        self._dll = _load_dll()
        self._runtime_key: Optional[int] = None
        self._result: Optional[LicenseResult] = None

    # -------------------------------------------------------------------------
    # Public API
    # -------------------------------------------------------------------------

    def activate(self, license_key: str) -> int:
        """
        Call DLL to verify license and return runtime_key.

        The runtime_key is meaningful data derived from:
        - Your machine ID hash
        - The license key
        - The plan name
        - The expiry timestamp

        If the license is INVALID, the key will be a poison value.
        Python code should USE this key for operations — not check its validity.

        Returns:
            runtime_key (uint32) - Use in all subsequent operations
        """
        result: LicenseResult = self._dll.verify_and_get_key(
            license_key.encode("utf-8"),
            self._app_id.encode("utf-8")
        )
        self._result = result
        self._runtime_key = result.runtime_key
        return result.runtime_key

    def get_machine_id(self) -> str:
        """Return this machine's hashed identifier (for support/display)."""
        buf = ctypes.create_string_buffer(128)
        self._dll.get_machine_id(buf, 128)
        return buf.value.decode("utf-8", errors="replace")

    @property
    def status(self) -> int:
        """0=ok, 1=invalid, 2=expired, 3=device_limit, 4=offline_degraded"""
        return self._result.status_code if self._result else -1

    @property
    def plan(self) -> str:
        return self._result.plan if self._result else "unknown"

    @property
    def days_remaining(self) -> int:
        return self._result.days_remaining if self._result else 0

    # -------------------------------------------------------------------------
    # Feature Gate Methods
    # These use the runtime_key to process data — wrong key = wrong result
    # DO NOT use if/else boolean checks based on self.status
    # -------------------------------------------------------------------------

    def process_data(self, data: bytes) -> bytes:
        """
        Example: XOR-mix data with runtime_key.
        If key is poison value, output will be garbled (but silently).
        """
        if self._runtime_key is None:
            raise RuntimeError("Call activate() first")

        key_bytes = struct.pack("<I", self._runtime_key)
        result = bytearray(len(data))
        for i, byte in enumerate(data):
            result[i] = byte ^ key_bytes[i % 4]
        return bytes(result)

    def derive_feature_token(self, feature_name: str) -> str:
        """
        Generate a feature-specific token from runtime_key + feature name.
        Use as an auth token for premium feature access.
        Wrong key -> wrong token -> feature fails silently.
        """
        if self._runtime_key is None:
            raise RuntimeError("Call activate() first")

        combined = f"{self._runtime_key}:{feature_name}"
        return hashlib.sha256(combined.encode()).hexdigest()[:32]

    def encrypt_local_data(self, plaintext: str) -> bytes:
        """
        Encrypt local data using runtime_key as seed.
        Wrong key -> encrypted with wrong key -> decryption will fail later.
        """
        if self._runtime_key is None:
            raise RuntimeError("Call activate() first")

        # Simple key stream XOR (real app: use AES with key derived from runtime_key)
        key32 = struct.pack("<I", self._runtime_key)
        key_extended = hashlib.sha256(key32).digest()  # 32 bytes
        data = plaintext.encode("utf-8")
        result = bytearray(len(data))
        for i, b in enumerate(data):
            result[i] = b ^ key_extended[i % 32]
        # Prepend length for integrity
        return struct.pack("<I", len(result)) + bytes(result)

    def decrypt_local_data(self, ciphertext: bytes) -> Optional[str]:
        """
        Decrypt data encrypted by encrypt_local_data().
        Wrong key -> garbage output or exception.
        """
        if self._runtime_key is None:
            return None
        try:
            length = struct.unpack("<I", ciphertext[:4])[0]
            data = ciphertext[4:4 + length]
            key32 = struct.pack("<I", self._runtime_key)
            key_extended = hashlib.sha256(key32).digest()
            result = bytearray(len(data))
            for i, b in enumerate(data):
                result[i] = b ^ key_extended[i % 32]
            return result.decode("utf-8")
        except Exception:
            return None  # Silently fail — wrong key or corrupt data


# =============================================================================
# Example Application
# =============================================================================
def run_demo():
    print("=" * 60)
    print("  Secure License System - Demo")
    print("=" * 60)

    # Set env vars for testing (remove in production, bake into DLL)
    # os.environ["SUPABASE_URL"] = "https://YOUR_PROJECT.supabase.co"
    # os.environ["SUPABASE_ANON_KEY"] = "eyJhb..."

    client = LicenseClient(app_id="INIContract")

    # Show machine ID
    machine_id = client.get_machine_id()
    print(f"\n[*] Machine ID (hashed): {machine_id[:16]}...")

    # Activate license
    license_key = os.environ.get("LICENSE_KEY", "TEST-0000-0000-0001")
    print(f"[*] Activating license: {license_key}")

    runtime_key = client.activate(license_key)

    plan_map = {
        "trial_30_days": "Gói Dùng Thử 30 Ngày",
        "free": "Gói Miễn Phí",
        "3_months": "Key 3 Tháng",
        "6_months": "Key 6 Tháng",
        "1_year": "Key 1 Năm",
        "lifetime": "Key Trọn Đời"
    }
    plan_str = plan_map.get(client.plan, client.plan)

    print(f"\n[+] Runtime Key  : 0x{runtime_key:08X}")
    print(f"[+] Status Code  : {client.status}")
    print(f"[+] Loại Key     : {plan_str}")
    print(f"[+] Ngày còn lại : {client.days_remaining} ngày")

    # -------------------------------------------------------------------------
    # CORRECT PATTERN: Use runtime_key to compute results
    # Wrong key -> wrong output, not an exception
    # -------------------------------------------------------------------------
    test_data = b"Hello, this is sensitive application data!"

    processed = client.process_data(test_data)
    restored  = client.process_data(processed)  # XOR is its own inverse

    print(f"\n[*] Data round-trip matches: {restored == test_data}")
    print(f"[*] Feature token (export): {client.derive_feature_token('export')[:16]}...")

    # Encrypt local settings
    settings = '{"theme":"dark","limit":100}'
    enc = client.encrypt_local_data(settings)
    dec = client.decrypt_local_data(enc)
    print(f"[*] Settings round-trip  : {dec == settings}")

    # -------------------------------------------------------------------------
    # WRONG PATTERN (Never do this):
    # if client.status == 0:
    #     run_premium_features()
    # -------------------------------------------------------------------------

    print("\n[*] Application running using derived key...")
    print("=" * 60)


if __name__ == "__main__":
    run_demo()
