"""
Synor Contract SDK Client

Smart contract deployment, interaction, and event handling.
"""

import asyncio
from typing import Optional, List, Dict, Any, Union
import httpx

from .types import (
    ContractConfig,
    ContractError,
    AbiEntry,
    DeploymentResult,
    TransactionResult,
    EventLog,
    DecodedEvent,
    ContractInterface,
    GasEstimation,
    BytecodeInfo,
    VerificationResult,
    MulticallRequest,
    MulticallResult,
    DEFAULT_CONTRACT_ENDPOINT,
)


# Type alias for ABI
Abi = List[AbiEntry]


class SynorContract:
    """
    Synor Contract Client.

    Provides smart contract deployment, interaction, and event handling.

    Example:
        async with SynorContract(api_key="sk_...") as contract:
            # Deploy a contract
            result = await contract.deploy(
                bytecode="0x608060...",
                abi=[...],
                args=[100, "Token"],
            )

            # Call a view function
            balance = await contract.call(
                address=result.address,
                method="balanceOf",
                args=["0x..."],
                abi=[...],
            )

            # Send a transaction
            tx = await contract.send(
                address=result.address,
                method="transfer",
                args=["0x...", "1000000"],
                abi=[...],
            )
    """

    def __init__(
        self,
        api_key: str,
        endpoint: str = DEFAULT_CONTRACT_ENDPOINT,
        timeout: float = 30.0,
        retries: int = 3,
        debug: bool = False,
        default_gas_limit: Optional[str] = None,
        default_gas_price: Optional[str] = None,
    ):
        self.config = ContractConfig(
            api_key=api_key,
            endpoint=endpoint,
            timeout=timeout,
            retries=retries,
            debug=debug,
            default_gas_limit=default_gas_limit,
            default_gas_price=default_gas_price,
        )
        self._client = httpx.AsyncClient(
            base_url=endpoint,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "X-SDK-Version": "python/0.1.0",
            },
            timeout=timeout,
        )
        self._closed = False

    # ==================== Deployment ====================

    async def deploy(
        self,
        bytecode: str,
        abi: Optional[Abi] = None,
        args: Optional[List[Any]] = None,
        gas_limit: Optional[str] = None,
        gas_price: Optional[str] = None,
        value: Optional[str] = None,
        salt: Optional[str] = None,
    ) -> DeploymentResult:
        """
        Deploy a smart contract.

        Args:
            bytecode: Compiled bytecode (hex)
            abi: Contract ABI
            args: Constructor arguments
            gas_limit: Gas limit
            gas_price: Gas price
            value: Value to send with deployment
            salt: Salt for deterministic deployment (CREATE2)

        Returns:
            Deployment result with contract address
        """
        body: Dict[str, Any] = {"bytecode": bytecode}
        if abi:
            body["abi"] = [e.to_dict() for e in abi]
        if args:
            body["args"] = args
        if gas_limit or self.config.default_gas_limit:
            body["gasLimit"] = gas_limit or self.config.default_gas_limit
        if gas_price or self.config.default_gas_price:
            body["gasPrice"] = gas_price or self.config.default_gas_price
        if value:
            body["value"] = value
        if salt:
            body["salt"] = salt

        response = await self._request("POST", "/contracts/deploy", body)
        return DeploymentResult.from_dict(response["deployment"])

    async def deploy_deterministic(
        self,
        bytecode: str,
        salt: str,
        abi: Optional[Abi] = None,
        args: Optional[List[Any]] = None,
        gas_limit: Optional[str] = None,
        gas_price: Optional[str] = None,
        value: Optional[str] = None,
    ) -> DeploymentResult:
        """Deploy using CREATE2 for deterministic addresses."""
        return await self.deploy(
            bytecode=bytecode,
            abi=abi,
            args=args,
            gas_limit=gas_limit,
            gas_price=gas_price,
            value=value,
            salt=salt,
        )

    async def predict_address(
        self,
        bytecode: str,
        salt: str,
        constructor_args: Optional[List[Any]] = None,
    ) -> str:
        """
        Predict CREATE2 deployment address.

        Args:
            bytecode: Contract bytecode
            salt: Deployment salt
            constructor_args: Constructor arguments

        Returns:
            Predicted address
        """
        body: Dict[str, Any] = {"bytecode": bytecode, "salt": salt}
        if constructor_args:
            body["constructorArgs"] = constructor_args

        response = await self._request("POST", "/contracts/predict-address", body)
        return response["address"]

    # ==================== Contract Interaction ====================

    async def call(
        self,
        address: str,
        method: str,
        abi: Abi,
        args: Optional[List[Any]] = None,
        block_number: Optional[Union[int, str]] = None,
    ) -> Any:
        """
        Call a view/pure function (no transaction).

        Args:
            address: Contract address
            method: Method name
            abi: Contract ABI
            args: Method arguments
            block_number: Block number or 'latest'

        Returns:
            Function return value
        """
        body: Dict[str, Any] = {
            "address": address,
            "method": method,
            "abi": [e.to_dict() for e in abi],
            "blockNumber": block_number or "latest",
        }
        if args:
            body["args"] = args

        response = await self._request("POST", "/contracts/call", body)
        return response["result"]

    async def send(
        self,
        address: str,
        method: str,
        abi: Abi,
        args: Optional[List[Any]] = None,
        gas_limit: Optional[str] = None,
        gas_price: Optional[str] = None,
        value: Optional[str] = None,
    ) -> TransactionResult:
        """
        Send a state-changing transaction.

        Args:
            address: Contract address
            method: Method name
            abi: Contract ABI
            args: Method arguments
            gas_limit: Gas limit
            gas_price: Gas price
            value: Value to send

        Returns:
            Transaction result
        """
        body: Dict[str, Any] = {
            "address": address,
            "method": method,
            "abi": [e.to_dict() for e in abi],
        }
        if args:
            body["args"] = args
        if gas_limit or self.config.default_gas_limit:
            body["gasLimit"] = gas_limit or self.config.default_gas_limit
        if gas_price or self.config.default_gas_price:
            body["gasPrice"] = gas_price or self.config.default_gas_price
        if value:
            body["value"] = value

        response = await self._request("POST", "/contracts/send", body)
        return TransactionResult.from_dict(response["transaction"])

    async def multicall(
        self,
        requests: List[MulticallRequest],
        abis: Optional[Dict[str, Abi]] = None,
    ) -> List[MulticallResult]:
        """
        Execute multiple calls in a single request.

        Args:
            requests: Array of multicall requests
            abis: Optional ABIs for decoding (keyed by address)

        Returns:
            Array of results
        """
        body: Dict[str, Any] = {"calls": [r.to_dict() for r in requests]}
        if abis:
            body["abis"] = {
                addr: [e.to_dict() for e in abi] for addr, abi in abis.items()
            }

        response = await self._request("POST", "/contracts/multicall", body)
        return [MulticallResult.from_dict(r) for r in response["results"]]

    # ==================== Events ====================

    async def get_events(
        self,
        address: str,
        abi: Optional[Abi] = None,
        event_name: Optional[str] = None,
        from_block: Optional[Union[int, str]] = None,
        to_block: Optional[Union[int, str]] = None,
        filter_values: Optional[Dict[str, Any]] = None,
    ) -> List[DecodedEvent]:
        """
        Get historical events.

        Args:
            address: Contract address
            abi: Contract ABI (required for decoding)
            event_name: Event name (optional)
            from_block: From block
            to_block: To block
            filter_values: Indexed filter values

        Returns:
            Array of decoded events
        """
        body: Dict[str, Any] = {
            "address": address,
            "fromBlock": from_block or "earliest",
            "toBlock": to_block or "latest",
        }
        if abi:
            body["abi"] = [e.to_dict() for e in abi]
        if event_name:
            body["eventName"] = event_name
        if filter_values:
            body["filter"] = filter_values

        response = await self._request("POST", "/contracts/events", body)
        return [DecodedEvent.from_dict(e) for e in response["events"]]

    async def get_logs(
        self,
        address: str,
        abi: Optional[Abi] = None,
        event_name: Optional[str] = None,
        from_block: Optional[Union[int, str]] = None,
        to_block: Optional[Union[int, str]] = None,
        filter_values: Optional[Dict[str, Any]] = None,
    ) -> List[EventLog]:
        """
        Get raw event logs.

        Args:
            address: Contract address
            abi: Contract ABI
            event_name: Event name (optional)
            from_block: From block
            to_block: To block
            filter_values: Indexed filter values

        Returns:
            Array of raw logs
        """
        body: Dict[str, Any] = {
            "address": address,
            "fromBlock": from_block or "earliest",
            "toBlock": to_block or "latest",
        }
        if abi:
            body["abi"] = [e.to_dict() for e in abi]
        if event_name:
            body["eventName"] = event_name
        if filter_values:
            body["filter"] = filter_values

        response = await self._request("POST", "/contracts/logs", body)
        return [EventLog.from_dict(l) for l in response["logs"]]

    async def decode_log(self, log: EventLog, abi: Abi) -> DecodedEvent:
        """
        Decode a raw event log.

        Args:
            log: Raw log
            abi: Contract ABI

        Returns:
            Decoded event
        """
        response = await self._request(
            "POST",
            "/contracts/decode-log",
            {"log": log.to_dict(), "abi": [e.to_dict() for e in abi]},
        )
        return DecodedEvent.from_dict(response["event"])

    # ==================== ABI Utilities ====================

    def load_abi(self, abi: Abi) -> ContractInterface:
        """
        Load and parse a contract ABI.

        Args:
            abi: Contract ABI

        Returns:
            Parsed contract interface
        """
        functions: Dict[str, AbiEntry] = {}
        events: Dict[str, AbiEntry] = {}
        errors: Dict[str, AbiEntry] = {}

        for entry in abi:
            if entry.type == "function" and entry.name:
                functions[entry.name] = entry
            elif entry.type == "event" and entry.name:
                events[entry.name] = entry
            elif entry.type == "error" and entry.name:
                errors[entry.name] = entry

        return ContractInterface(
            abi=abi,
            functions=functions,
            events=events,
            errors=errors,
        )

    async def encode_call(
        self, method: str, args: List[Any], abi: Abi
    ) -> str:
        """
        Encode a function call.

        Args:
            method: Method name
            args: Arguments
            abi: Contract ABI

        Returns:
            Encoded calldata (hex)
        """
        response = await self._request(
            "POST",
            "/contracts/encode",
            {"method": method, "args": args, "abi": [e.to_dict() for e in abi]},
        )
        return response["data"]

    async def decode_result(
        self, method: str, data: str, abi: Abi
    ) -> Any:
        """
        Decode function return data.

        Args:
            method: Method name
            data: Raw data to decode
            abi: Contract ABI

        Returns:
            Decoded result
        """
        response = await self._request(
            "POST",
            "/contracts/decode",
            {"method": method, "data": data, "abi": [e.to_dict() for e in abi]},
        )
        return response["result"]

    async def get_function_selector(self, signature: str) -> str:
        """
        Get function selector (first 4 bytes of keccak256 hash).

        Args:
            signature: Function signature (e.g., "transfer(address,uint256)")

        Returns:
            Function selector (hex)
        """
        response = await self._request(
            "POST", "/contracts/selector", {"signature": signature}
        )
        return response["selector"]

    # ==================== Gas Estimation ====================

    async def estimate_gas(
        self,
        address: Optional[str] = None,
        method: Optional[str] = None,
        args: Optional[List[Any]] = None,
        abi: Optional[Abi] = None,
        bytecode: Optional[str] = None,
        value: Optional[str] = None,
        from_address: Optional[str] = None,
    ) -> GasEstimation:
        """
        Estimate gas for a contract call or deployment.

        Args:
            address: Contract address (for calls)
            method: Method name
            args: Arguments
            abi: Contract ABI
            bytecode: Bytecode (for deployment)
            value: Value to send
            from_address: Sender address

        Returns:
            Gas estimation
        """
        body: Dict[str, Any] = {}
        if address:
            body["address"] = address
        if method:
            body["method"] = method
        if args:
            body["args"] = args
        if abi:
            body["abi"] = [e.to_dict() for e in abi]
        if bytecode:
            body["bytecode"] = bytecode
        if value:
            body["value"] = value
        if from_address:
            body["from"] = from_address

        response = await self._request("POST", "/contracts/estimate-gas", body)
        return GasEstimation.from_dict(response["estimation"])

    # ==================== Contract Info ====================

    async def get_bytecode(self, address: str) -> BytecodeInfo:
        """
        Get contract bytecode.

        Args:
            address: Contract address

        Returns:
            Bytecode info
        """
        response = await self._request("GET", f"/contracts/{address}/bytecode")
        return BytecodeInfo.from_dict(response)

    async def is_contract(self, address: str) -> bool:
        """
        Check if an address is a contract.

        Args:
            address: Address to check

        Returns:
            True if address has code
        """
        response = await self._request("GET", f"/contracts/{address}/is-contract")
        return response["isContract"]

    async def read_storage(
        self,
        address: str,
        slot: str,
        block_number: Optional[Union[int, str]] = None,
    ) -> str:
        """
        Read contract storage slot.

        Args:
            address: Contract address
            slot: Storage slot
            block_number: Block number

        Returns:
            Storage value (hex)
        """
        response = await self._request(
            "POST",
            "/contracts/storage",
            {
                "address": address,
                "slot": slot,
                "blockNumber": block_number or "latest",
            },
        )
        return response["value"]

    # ==================== Verification ====================

    async def verify_contract(
        self,
        address: str,
        source_code: str,
        compiler_version: str,
        constructor_arguments: Optional[str] = None,
        optimization: bool = False,
        optimization_runs: int = 200,
        contract_name: Optional[str] = None,
    ) -> VerificationResult:
        """
        Submit contract for verification.

        Args:
            address: Contract address
            source_code: Source code
            compiler_version: Compiler version
            constructor_arguments: Constructor arguments (hex)
            optimization: Optimization enabled
            optimization_runs: Optimization runs
            contract_name: Contract name

        Returns:
            Verification result
        """
        body: Dict[str, Any] = {
            "address": address,
            "sourceCode": source_code,
            "compilerVersion": compiler_version,
            "optimization": optimization,
            "optimizationRuns": optimization_runs,
        }
        if constructor_arguments:
            body["constructorArguments"] = constructor_arguments
        if contract_name:
            body["contractName"] = contract_name

        response = await self._request("POST", "/contracts/verify", body)
        return VerificationResult.from_dict(response)

    async def get_verification_status(self, address: str) -> VerificationResult:
        """Check verification status."""
        response = await self._request("GET", f"/contracts/{address}/verification")
        return VerificationResult.from_dict(response)

    async def get_verified_abi(self, address: str) -> Optional[Abi]:
        """Get verified contract ABI."""
        try:
            response = await self._request("GET", f"/contracts/{address}/abi")
            return [AbiEntry.from_dict(e) for e in response["abi"]]
        except ContractError:
            return None

    # ==================== Lifecycle ====================

    async def close(self) -> None:
        """Close the client."""
        self._closed = True
        await self._client.aclose()

    def is_closed(self) -> bool:
        """Check if client is closed."""
        return self._closed

    async def health_check(self) -> bool:
        """Health check."""
        try:
            response = await self._request("GET", "/health")
            return response.get("status") == "healthy"
        except ContractError:
            return False

    async def __aenter__(self) -> "SynorContract":
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.close()

    # ==================== Private Methods ====================

    async def _request(
        self,
        method: str,
        path: str,
        body: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make HTTP request with retries."""
        if self._closed:
            raise ContractError("Client has been closed")

        last_error: Optional[Exception] = None

        for attempt in range(self.config.retries):
            try:
                return await self._do_request(method, path, body)
            except ContractError as e:
                if self.config.debug:
                    print(f"Attempt {attempt + 1} failed: {e}")
                last_error = e
                if attempt < self.config.retries - 1:
                    await asyncio.sleep(2**attempt)

        if last_error:
            raise last_error
        raise ContractError("Unknown error after retries")

    async def _do_request(
        self,
        method: str,
        path: str,
        body: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Execute HTTP request."""
        try:
            response = await self._client.request(
                method,
                path,
                json=body if body else None,
            )

            if response.status_code >= 400:
                error_body = response.json() if response.content else {}
                message = (
                    error_body.get("message")
                    or error_body.get("error")
                    or f"HTTP {response.status_code}"
                )
                raise ContractError(
                    message,
                    code=error_body.get("code"),
                    status_code=response.status_code,
                    details=error_body,
                )

            return response.json()
        except httpx.TimeoutException as e:
            raise ContractError(f"Request timeout: {e}")
        except httpx.RequestError as e:
            raise ContractError(f"Request failed: {e}")
