"""Base class shared by all SDK modules.

Provides JSON-RPC helpers, HTTP client access, chain configuration,
and ABI encoding/decoding via ``eth_abi``.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Sequence, Tuple

import httpx
from eth_abi import decode, encode
from eth_hash.auto import keccak as _keccak256

from openhedge.constants import ABIS, get_chain_config
from openhedge.exceptions import (
    AgentNotApprovedError,
    AuthenticationError,
    NetworkError,
    NotFoundError,
    OpenHedgeError,
    ServerError,
    TransactionError,
    ValidationError,
)
from openhedge.utils.rate_limit import check_rate_limit
from openhedge.utils.retry import with_retry
from openhedge.utils.signing import build_transaction, sign_transaction


def function_selector(signature: str) -> bytes:
    """Compute the 4-byte Keccak-256 selector for a Solidity function signature."""
    return _keccak256(signature.encode())[:4]


class BaseModule:
    """Shared functionality injected into every module."""

    def __init__(
        self,
        *,
        http: httpx.AsyncClient,
        rpc_url: str,
        chain_id: int,
        network: str,
        api_key: str,
        wallet_private_key: Optional[str] = None,
        max_retries: int = 3,
    ) -> None:
        self._http = http
        self._rpc_url = rpc_url
        self._chain_id = chain_id
        self._network = network
        self._api_key = api_key
        self._wallet_private_key = wallet_private_key
        self._max_retries = max_retries
        self._config = get_chain_config(network)
        self._contracts = self._config["contracts"]
        self._api_base = self._config["api"]["gateway"]

    # ------------------------------------------------------------------
    # JSON-RPC helpers
    # ------------------------------------------------------------------

    async def _rpc_call(self, method: str, params: list) -> Any:
        """Send a JSON-RPC request and return the ``result`` field."""

        async def _do() -> Any:
            try:
                resp = await self._http.post(
                    self._rpc_url,
                    json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
                )
            except (httpx.ConnectError, httpx.TimeoutException) as exc:
                raise NetworkError(f"RPC connection failed: {exc}") from exc

            body = resp.json()
            if "error" in body:
                msg = body["error"].get("message", str(body["error"]))
                raise TransactionError(msg)
            return body.get("result")

        return await with_retry(_do, max_retries=self._max_retries)

    async def _eth_call(self, to: str, data: str, block: str = "latest") -> str:
        """Execute ``eth_call`` and return the hex-encoded result."""
        result = await self._rpc_call("eth_call", [{"to": to, "data": data}, block])
        return result  # type: ignore[no-any-return]

    async def _get_nonce(self, address: str) -> int:
        raw = await self._rpc_call("eth_getTransactionCount", [address, "pending"])
        return int(raw, 16)

    async def _get_gas_price(self) -> int:
        raw = await self._rpc_call("eth_gasPrice", [])
        return int(raw, 16)

    async def _estimate_gas(self, tx: Dict[str, Any]) -> int:
        call_obj = {"to": tx["to"], "data": tx.get("data", "0x")}
        if tx.get("from"):
            call_obj["from"] = tx["from"]
        if tx.get("value"):
            call_obj["value"] = hex(tx["value"])
        raw = await self._rpc_call("eth_estimateGas", [call_obj])
        return int(raw, 16)

    async def _send_raw_transaction(self, raw_tx: str) -> str:
        """Broadcast a signed transaction and return the tx hash."""
        if not raw_tx.startswith("0x"):
            raw_tx = "0x" + raw_tx
        tx_hash = await self._rpc_call("eth_sendRawTransaction", [raw_tx])
        return tx_hash  # type: ignore[no-any-return]

    async def _wait_for_receipt(
        self, tx_hash: str, *, timeout: float = 60.0, poll_interval: float = 2.0
    ) -> Dict[str, Any]:
        """Poll for a transaction receipt until confirmed or *timeout* elapsed."""
        import asyncio
        import time

        start = time.monotonic()
        while time.monotonic() - start < timeout:
            receipt = await self._rpc_call("eth_getTransactionReceipt", [tx_hash])
            if receipt is not None:
                return receipt  # type: ignore[no-any-return]
            await asyncio.sleep(poll_interval)
        raise TransactionError(f"Tx {tx_hash} not confirmed after {timeout}s", tx_hash=tx_hash)

    # ------------------------------------------------------------------
    # ABI encoding helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _encode_call(sig: str, types: List[str], values: list) -> str:
        """ABI-encode a function call and return the hex calldata."""
        selector = function_selector(sig)
        if types:
            encoded_args = encode(types, values)
        else:
            encoded_args = b""
        return "0x" + (selector + encoded_args).hex()

    @staticmethod
    def _decode_result(types: List[str], data: str) -> Tuple[Any, ...]:
        """ABI-decode hex data returned from ``eth_call``."""
        raw = bytes.fromhex(data[2:]) if data.startswith("0x") else bytes.fromhex(data)
        return decode(types, raw)

    # ------------------------------------------------------------------
    # Transaction signing + sending
    # ------------------------------------------------------------------

    async def _send_tx(
        self,
        *,
        to: str,
        data: str,
        value: int = 0,
        gas: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Sign, broadcast, and wait for a transaction.

        Returns the full receipt dict.
        """
        if self._wallet_private_key is None:
            raise AuthenticationError(
                "A wallet_private_key is required for write transactions"
            )

        from openhedge.utils.signing import get_account

        account = get_account(self._wallet_private_key)
        nonce = await self._get_nonce(account.address)

        if gas is None:
            try:
                gas = await self._estimate_gas(
                    {"to": to, "data": data, "from": account.address, "value": value}
                )
                gas = int(gas * 1.2)  # 20 % headroom
            except (NetworkError, TransactionError):
                gas = 500_000

        gas_price = await self._get_gas_price()

        tx = build_transaction(
            to=to,
            data=data,
            chain_id=self._chain_id,
            nonce=nonce,
            gas=gas,
            max_fee_per_gas=gas_price * 2,
            max_priority_fee_per_gas=gas_price,
            value=value,
        )

        raw = sign_transaction(tx, self._wallet_private_key)
        tx_hash = await self._send_raw_transaction(raw)
        receipt = await self._wait_for_receipt(tx_hash)

        status = int(receipt.get("status", "0x0"), 16)
        if status == 0:
            raise TransactionError(f"Transaction reverted: {tx_hash}", tx_hash=tx_hash)

        return receipt

    # ------------------------------------------------------------------
    # REST helpers
    # ------------------------------------------------------------------

    def _auth_headers(self) -> Dict[str, str]:
        return {"Authorization": f"Bearer {self._api_key}"}

    async def _api_get(self, path: str, **params: Any) -> Any:
        async def _do() -> Any:
            try:
                resp = await self._http.get(
                    f"{self._api_base}{path}",
                    headers=self._auth_headers(),
                    params=params or None,
                )
            except (httpx.ConnectError, httpx.TimeoutException) as exc:
                raise NetworkError(str(exc)) from exc

            check_rate_limit(resp.status_code, dict(resp.headers))
            if resp.status_code == 401:
                raise AuthenticationError()
            if resp.status_code == 404:
                raise NotFoundError()
            if resp.status_code == 403:
                raise AgentNotApprovedError()
            if resp.status_code == 422:
                raise ValidationError(resp.text)
            if resp.status_code >= 500:
                raise ServerError(f"Server returned HTTP {resp.status_code}")
            if resp.status_code >= 400:
                raise OpenHedgeError(f"HTTP {resp.status_code}: {resp.text[:200]}")
            resp.raise_for_status()
            return resp.json()

        return await with_retry(_do, max_retries=self._max_retries)

    async def _api_post(self, path: str, json_body: Any = None) -> Any:
        async def _do() -> Any:
            try:
                resp = await self._http.post(
                    f"{self._api_base}{path}",
                    headers=self._auth_headers(),
                    json=json_body,
                )
            except (httpx.ConnectError, httpx.TimeoutException) as exc:
                raise NetworkError(str(exc)) from exc

            check_rate_limit(resp.status_code, dict(resp.headers))
            if resp.status_code == 401:
                raise AuthenticationError()
            if resp.status_code == 404:
                raise NotFoundError()
            if resp.status_code == 403:
                raise AgentNotApprovedError()
            if resp.status_code == 422:
                raise ValidationError(resp.text)
            if resp.status_code >= 500:
                raise ServerError(f"Server returned HTTP {resp.status_code}")
            if resp.status_code >= 400:
                raise OpenHedgeError(f"HTTP {resp.status_code}: {resp.text[:200]}")
            resp.raise_for_status()
            return resp.json()

        return await with_retry(_do, max_retries=self._max_retries)
