"""
Auto-generated contract error classes for XCCY Protocol.

This module dynamically creates Python exception classes from Solidity ABI error
definitions, enabling typed exception handling for contract reverts.

Example:
    >>> from xccy.errors import PoolExpiredError, decode_error, ContractRevertError
    >>>
    >>> try:
    ...     client.trading.swap(...)
    ... except PoolExpiredError:
    ...     logger.warning("Pool expired, switching to next pool")
    ... except ContractRevertError as e:
    ...     logger.error("Contract error: %s", e)

All error classes are auto-generated at module load from ABI files and registered
in ``_SELECTOR_TO_CLASS`` by their 4-byte selector.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

import eth_abi
from web3 import Web3

from xccy.exceptions import ContractError

# ═══════════════════════════════════════════════════════════════════════════════
# Base Exception
# ═══════════════════════════════════════════════════════════════════════════════


class ContractRevertError(ContractError):
    """
    Base class for all decoded Solidity custom errors.

    Attributes:
        error_name: Solidity error name (e.g. ``"PoolExpired"``).
        error_selector: 4-byte hex selector (e.g. ``"0x398b36db"``).
        error_args: Decoded arguments as a dict.
        raw_data: Full revert data hex string.
        description: Optional human-readable description.
        tx_hash: Transaction hash if available.
    """

    error_name: str = ""
    error_selector: str = ""
    error_args: dict[str, Any]
    raw_data: str = ""
    description: str | None = None

    def __init__(
        self,
        error_name: str = "",
        error_selector: str = "",
        error_args: dict[str, Any] | None = None,
        raw_data: str = "",
        description: str | None = None,
        tx_hash: str | None = None,
        **kwargs: Any,
    ) -> None:
        self.error_name = error_name
        self.error_selector = error_selector
        self.error_args = error_args or {}
        self.raw_data = raw_data
        self.description = description
        # Set any extra kwargs as attributes (for typed params)
        for k, v in kwargs.items():
            setattr(self, k, v)
        super().__init__(str(self), tx_hash=tx_hash)

    def __str__(self) -> str:
        args_str = ", ".join(f"{k}={v}" for k, v in self.error_args.items())
        base = f"{self.error_name}({args_str}) [{self.error_selector}]"
        if self.description:
            base += f": {self.description}"
        return base

    def __repr__(self) -> str:
        return (
            f"{type(self).__name__}(error_name={self.error_name!r}, "
            f"error_selector={self.error_selector!r}, error_args={self.error_args!r})"
        )


# ═══════════════════════════════════════════════════════════════════════════════
# Human-Readable Descriptions
# ═══════════════════════════════════════════════════════════════════════════════

ERROR_DESCRIPTIONS: dict[str, str] = {
    "PoolExpired": "Pool has expired and is no longer tradeable",
    "PoolNotInitialized": "Pool has not been initialized",
    "CannotLiquidate": "Account cannot be liquidated (health factor sufficient)",
    "CannotSettleBeforeMaturity": "Position cannot be settled before pool maturity",
    "MarginRequirementNotMet": "Margin requirement not met for this operation",
    "NotEnoughFunds": "Insufficient funds for the requested operation",
    "NotEnoughFundsForFee": "Insufficient funds to cover the fee",
    "InvalidMarginDelta": "Margin delta value is invalid",
    "IRSNotionalAmountSpecifiedMustBeNonZero": "Notional amount must be non-zero",
    "OnlyOwnerCanUpdatePosition": "Only the account owner can update this position",
    "PositionNetZero": "Position has zero net exposure",
    "PositionNotSettled": "Position has not been settled yet",
    "TokenNotWhitelisted": "Token is not whitelisted for margin",
    "WithdrawalExceedsCurrentMargin": "Withdrawal amount exceeds available margin",
    "CanOnlyTradeIfUnlocked": "VAMM is locked, trading is paused",
    "VAMMLocked": "VAMM is locked",
    "VAMMNotInitialized": "VAMM has not been initialized",
    "IsolatedMarginModeViolation": (
        "Isolated margin account cannot trade pools with different underlying"
    ),
    "InvalidTermTimestamps": "Term start/end timestamps are invalid",
    "FeeRangeInvalid": "Fee is outside the valid range",
    "TickSpacingOutOfBounds": "Tick spacing is outside allowed bounds",
    "SqrtPriceRatioOutOfBounds": "Sqrt price ratio is outside valid bounds",
    "SqrtPriceLimitInvalid": "Sqrt price limit is invalid for the swap direction",
    "ExpectedSqrtPriceZeroBeforeInit": "Expected zero sqrt price before initialization",
    "ExpectedOppositeSigns": "Expected opposite signs for the two values",
    "LiquidityDeltaMustBePositiveInMint": "Liquidity delta must be positive when minting",
    "LiquidityDeltaMustBePositiveInBurn": "Liquidity delta must be positive when burning",
    "MarginLessThanMinimum": "Margin is below the minimum required",
    "InvalidPrice": "Price is invalid for the given asset",
    "InvalidAprRate": "APR rate is invalid",
    "OwnableUnauthorizedAccount": "Caller is not the contract owner",
    "OwnableInvalidOwner": "Invalid owner address",
    "ReentrancyGuardReentrantCall": "Reentrant call detected",
    "Unauthorized": "Caller is not authorized",
    "MessageSenderNotAuthorized": "Message sender is not authorized for this account",
    "OnlyCollateralEngine": "Only the CollateralEngine can call this function",
    "OnlyVAMM": "Only the VAMM can call this function",
    "OnlyVAMMCaller": "Only the VAMM caller can call this function",
    "OnlyFCM": "Only the FCM can call this function",
    "AddressEmptyCode": "Target address has no code deployed",
    "AddressInsufficientBalance": "Address has insufficient balance",
    "FailedInnerCall": "Inner call failed",
    "SafeERC20FailedOperation": "SafeERC20 operation failed",
    "MathOverflowedMulDiv": "Math overflow in mul/div operation",
    "AdapterNotSet": "Oracle adapter is not configured",
    "AprPullFailed": "Failed to pull APR from oracle",
    "InvalidAdapter": "Oracle adapter is invalid",
    "SourceNotConfigured": "Oracle source is not configured",
    "closeToOrBeyondMaturity": "Pool is close to or beyond maturity",
    "PoolNotMatured": "Pool has not matured yet",
    "NoPositionsInPool": "Account has no positions in this pool",
    "ProtocolFeeRangeInvalid": "Protocol fee is outside the valid range",
    "OOO": "Out of order operation",
}

# ═══════════════════════════════════════════════════════════════════════════════
# Solidity type -> Python type mapping (for annotations)
# ═══════════════════════════════════════════════════════════════════════════════

_SOLIDITY_TO_PYTHON: dict[str, type] = {
    "address": str,
    "bool": bool,
    "string": str,
    "bytes": bytes,
    "bytes32": bytes,
}


def _python_type_for(sol_type: str) -> type:
    """Map a Solidity type to a Python type."""
    if sol_type in _SOLIDITY_TO_PYTHON:
        return _SOLIDITY_TO_PYTHON[sol_type]
    if sol_type.startswith(("uint", "int")):
        return int
    if sol_type.startswith("bytes"):
        return bytes
    return type(object)


def _to_snake_case(name: str) -> str:
    """Convert camelCase/PascalCase to snake_case."""
    s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
    s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
    return s.lower()


# ═══════════════════════════════════════════════════════════════════════════════
# Registry
# ═══════════════════════════════════════════════════════════════════════════════

_SELECTOR_TO_CLASS: dict[bytes, type[ContractRevertError]] = {}
_NAME_TO_CLASS: dict[str, type[ContractRevertError]] = {}


# ═══════════════════════════════════════════════════════════════════════════════
# Auto-Generation
# ═══════════════════════════════════════════════════════════════════════════════

_ABI_DIR = Path(__file__).parent / "abi"
_ABI_FILES = [
    "CollateralEngine",
    "VAMMManager",
    "OracleHub",
    "AprOracle",
    "Operator",
    "ERC20",
]


def _build_error_classes() -> None:
    """Parse ABI files and dynamically create error classes."""
    import json

    seen_signatures: set[str] = set()

    for abi_name in _ABI_FILES:
        abi_path = _ABI_DIR / f"{abi_name}.json"
        if not abi_path.exists():
            continue

        with open(abi_path) as f:
            abi = json.load(f)

        for entry in abi:
            if entry.get("type") != "error":
                continue

            name: str = entry["name"]
            inputs: list[dict[str, str]] = entry.get("inputs", [])

            # Build Solidity signature
            sig = name + "(" + ",".join(inp["type"] for inp in inputs) + ")"

            # Skip duplicates (same error may appear in multiple ABIs)
            if sig in seen_signatures:
                continue
            seen_signatures.add(sig)

            # Compute 4-byte selector
            selector = Web3.keccak(text=sig)[:4]

            # Build Python class name: SolidityName -> SolidityNameError
            class_name = f"{name}Error"

            # Parameter info for the class
            param_names = [_to_snake_case(inp["name"]) for inp in inputs]
            param_types = [inp["type"] for inp in inputs]
            description = ERROR_DESCRIPTIONS.get(name)

            # Create __init__ for the generated class
            def _make_init(
                _name: str,
                _selector: bytes,
                _param_names: list[str],
                _param_types: list[str],
                _description: str | None,
            ) -> Any:
                def _init(
                    self: ContractRevertError,
                    raw_data: str = "",
                    tx_hash: str | None = None,
                    **kwargs: Any,
                ) -> None:
                    error_args = {k: kwargs.get(k) for k in _param_names if k in kwargs}
                    ContractRevertError.__init__(
                        self,
                        error_name=_name,
                        error_selector="0x" + _selector.hex(),
                        error_args=error_args,
                        raw_data=raw_data,
                        description=_description,
                        tx_hash=tx_hash,
                        **kwargs,
                    )

                return _init

            # Build class attributes dict
            init_fn = _make_init(name, selector, param_names, param_types, description)
            cls_dict: dict[str, Any] = {
                "__init__": init_fn,
                "__module__": __name__,
                "__qualname__": class_name,
                "__doc__": f"Solidity error: ``{sig}``",
                "_solidity_name": name,
                "_solidity_signature": sig,
                "_selector": selector,
                "_param_names": param_names,
                "_param_types": param_types,
            }

            # Add type annotations for params
            annotations: dict[str, type] = {}
            for pname, ptype in zip(param_names, param_types, strict=True):
                annotations[pname] = _python_type_for(ptype)
            cls_dict["__annotations__"] = annotations

            # Create the class
            cls = type(class_name, (ContractRevertError,), cls_dict)

            # Register
            _SELECTOR_TO_CLASS[selector] = cls
            _NAME_TO_CLASS[name] = cls

            # Add to module globals so it's importable
            globals()[class_name] = cls


# Run at module load
_build_error_classes()


# ═══════════════════════════════════════════════════════════════════════════════
# Public API
# ═══════════════════════════════════════════════════════════════════════════════


def decode_error(raw_data: bytes | str) -> ContractRevertError:
    """
    Decode raw revert data into a typed exception instance.

    Args:
        raw_data: Raw revert data as hex string (with or without ``0x`` prefix)
            or bytes. Must be at least 4 bytes (the selector).

    Returns:
        An instance of the appropriate error class, or a generic
        ``ContractRevertError`` if the selector is unknown.

    Example:
        >>> err = decode_error("0x398b36db")
        >>> isinstance(err, PoolExpiredError)
        True
        >>> print(err)
        PoolExpired() [0x398b36db]: Pool has expired...
    """
    if isinstance(raw_data, str):
        hex_str = raw_data.removeprefix("0x")
        data = bytes.fromhex(hex_str)
    else:
        data = raw_data
        hex_str = data.hex()

    if len(data) < 4:
        return ContractRevertError(
            error_name="Unknown",
            error_selector="0x" + data.hex() if data else "0x",
            raw_data="0x" + hex_str,
        )

    selector = data[:4]
    raw_hex = "0x" + hex_str

    cls = _SELECTOR_TO_CLASS.get(selector)
    if cls is None:
        return ContractRevertError(
            error_name="Unknown",
            error_selector="0x" + selector.hex(),
            raw_data=raw_hex,
        )

    # Decode parameters
    param_types: list[str] = cls._param_types  # type: ignore[attr-defined]
    param_names: list[str] = cls._param_names  # type: ignore[attr-defined]

    kwargs: dict[str, Any] = {}
    if param_types and len(data) > 4:
        try:
            decoded = eth_abi.decode(param_types, data[4:])  # type: ignore[attr-defined]
            for name, value in zip(param_names, decoded, strict=True):
                # Convert bytes20 addresses to hex strings
                if isinstance(value, bytes) and len(value) == 20:
                    kwargs[name] = "0x" + value.hex()
                else:
                    kwargs[name] = value
        except Exception:
            # If decoding fails, return instance without args
            pass

    return cls(raw_data=raw_hex, **kwargs)


def decode_error_selector(selector: bytes | str) -> str | None:
    """
    Look up the Solidity error name for a 4-byte selector.

    Args:
        selector: 4-byte selector as bytes or hex string (with or without ``0x``).

    Returns:
        Solidity error name, or ``None`` if unknown.

    Example:
        >>> decode_error_selector("0x398b36db")
        'PoolExpired'
    """
    if isinstance(selector, str):
        selector = bytes.fromhex(selector.removeprefix("0x"))

    cls = _SELECTOR_TO_CLASS.get(selector)
    if cls is None:
        return None
    name: str = cls._solidity_name  # type: ignore[attr-defined]
    return name


def list_all_errors() -> list[str]:
    """
    List all known Solidity error names.

    Returns:
        Sorted list of error names (without the ``Error`` suffix).

    Example:
        >>> errors = list_all_errors()
        >>> "PoolExpired" in errors
        True
    """
    return sorted(_NAME_TO_CLASS.keys())


def get_error_class(name: str) -> type[ContractRevertError] | None:
    """
    Get the error class for a Solidity error name.

    Args:
        name: Solidity error name (e.g. ``"PoolExpired"``).

    Returns:
        The error class, or ``None`` if not found.
    """
    return _NAME_TO_CLASS.get(name)
