"""ctypes wrapper for cgt_guard.dll (Guard FFI layer)."""

import ctypes
import ctypes.util
from pathlib import Path


class LicenseError(Exception):
    """Base exception for license errors."""
    pass


class InvalidKeyError(LicenseError):
    pass


class MaxDevicesError(LicenseError):
    pass


class ProductMismatchError(LicenseError):
    pass


class NetworkError(LicenseError):
    pass


class NotInitializedError(LicenseError):
    pass


class NoSecretError(LicenseError):
    pass


class DecryptError(LicenseError):
    pass


_ERROR_MAP = {
    -1: InvalidKeyError,
    -2: MaxDevicesError,
    -3: ProductMismatchError,
    -4: NetworkError,
    -5: NoSecretError,
    -6: DecryptError,
    -10: NotInitializedError,
}


class GuardFFI:
    """Python wrapper for cgt_guard.dll via ctypes."""

    def __init__(self, dll_path: str):
        path = Path(dll_path)
        if not path.is_absolute():
            # Search relative to calling script / cwd
            candidates = [
                Path.cwd() / path,
                Path(__file__).parent / path,
            ]
            for c in candidates:
                if c.exists():
                    path = c
                    break

        self._lib = ctypes.CDLL(str(path))
        self._setup_functions()

    def _setup_functions(self):
        lib = self._lib

        # cgt_guard_init
        lib.cgt_guard_init.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
        lib.cgt_guard_init.restype = ctypes.c_int

        # cgt_guard_activate
        lib.cgt_guard_activate.argtypes = [ctypes.c_char_p]
        lib.cgt_guard_activate.restype = ctypes.c_int

        # cgt_guard_lease
        lib.cgt_guard_lease.argtypes = []
        lib.cgt_guard_lease.restype = ctypes.c_int

        # cgt_guard_renew
        lib.cgt_guard_renew.argtypes = []
        lib.cgt_guard_renew.restype = ctypes.c_int

        # cgt_guard_ping
        lib.cgt_guard_ping.argtypes = []
        lib.cgt_guard_ping.restype = ctypes.c_int

        # cgt_guard_decrypt_asset
        lib.cgt_guard_decrypt_asset.argtypes = [
            ctypes.POINTER(ctypes.c_uint8),  # encrypted
            ctypes.c_uint32,                  # encrypted_len
            ctypes.POINTER(ctypes.c_uint8),  # output
            ctypes.c_uint32,                  # output_cap
            ctypes.POINTER(ctypes.c_uint32), # output_len
        ]
        lib.cgt_guard_decrypt_asset.restype = ctypes.c_int

        # cgt_guard_last_error
        lib.cgt_guard_last_error.argtypes = [ctypes.c_char_p, ctypes.c_uint32]
        lib.cgt_guard_last_error.restype = ctypes.c_int

        # cgt_guard_shutdown
        lib.cgt_guard_shutdown.argtypes = []
        lib.cgt_guard_shutdown.restype = None

    def _get_last_error(self) -> str:
        buf = ctypes.create_string_buffer(1024)
        length = self._lib.cgt_guard_last_error(buf, 1024)
        if length > 0:
            return buf.value.decode("utf-8", errors="replace")
        return "Unknown error"

    def _check_result(self, code: int, context: str = ""):
        if code == 0:
            return
        err_msg = self._get_last_error()
        exc_cls = _ERROR_MAP.get(code, LicenseError)
        raise exc_cls(f"{context}: {err_msg}" if context else err_msg)

    def init(self, server_url: str, product_id: str) -> None:
        code = self._lib.cgt_guard_init(
            server_url.encode("utf-8"),
            product_id.encode("utf-8"),
        )
        self._check_result(code, "init")

    def activate(self, license_key: str) -> None:
        code = self._lib.cgt_guard_activate(license_key.encode("utf-8"))
        self._check_result(code, "activate")

    def lease(self) -> None:
        code = self._lib.cgt_guard_lease()
        self._check_result(code, "lease")

    def renew(self) -> None:
        code = self._lib.cgt_guard_renew()
        self._check_result(code, "renew")

    def ping(self) -> bool:
        return self._lib.cgt_guard_ping() == 1

    def decrypt_asset(self, encrypted: bytes) -> bytes:
        enc_buf = (ctypes.c_uint8 * len(encrypted))(*encrypted)
        out_cap = len(encrypted) + 1024  # generous buffer
        out_buf = (ctypes.c_uint8 * out_cap)()
        out_len = ctypes.c_uint32(0)

        code = self._lib.cgt_guard_decrypt_asset(
            enc_buf, len(encrypted),
            out_buf, out_cap,
            ctypes.byref(out_len),
        )
        self._check_result(code, "decrypt_asset")
        return bytes(out_buf[:out_len.value])

    def shutdown(self) -> None:
        self._lib.cgt_guard_shutdown()
