import json
import time
from typing import Any, Dict, List, Optional, TypeVar

from eth_abi.abi import decode, encode
from eth_utils.address import to_checksum_address

T = TypeVar("T")

# Constants for eth_call transaction defaults
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
DEFAULT_GAS_PRICE = 0  # For eth_call, gas price is always 0

# Gas limits for different contract operations
GAS_LIMIT_CAMPAIGNS = 30_000_000  # BatchCampaigns contract - fetches multiple campaigns
GAS_LIMIT_INSERTED_PROOFS = 10_000_000  # GetInsertedProofs - multiple oracle calls
GAS_LIMIT_CCIP_FEE = 500_000  # GetCCIPFee - single fee calculation
GAS_LIMIT_ACTIVE_CAMPAIGNS = 5_000_000  # GetActiveCampaignIds - scans campaign range


class ContractReader:
    """
    Contract reader that works with pre-compiled contract artifacts.
    Expects artifacts to be stored in a JSON format with bytecode.
    """

    # Cache for loaded contract artifacts
    _contract_artifacts: Dict[str, Dict] = {}

    @classmethod
    def load_artifact(cls, artifact_path: str) -> Dict:
        """
        Load a contract artifact from JSON file.
        """
        if artifact_path not in cls._contract_artifacts:
            with open(artifact_path, "r") as f:
                artifact = json.load(f)
                cls._contract_artifacts[artifact_path] = {
                    "bytecode": artifact["bytecode"]
                }
        return cls._contract_artifacts[artifact_path]

    @staticmethod
    def _extract_bytecode(artifact: Dict) -> str:
        """
        Extract bytecode string from artifact, handling both formats.

        Artifacts may have bytecode as:
        - Direct string: {"bytecode": "0x..."}
        - Nested dict: {"bytecode": {"bytecode": "0x..."}}
        """
        bytecode = artifact["bytecode"]
        if isinstance(bytecode, dict):
            return bytecode["bytecode"]
        return bytecode

    @staticmethod
    def _build_constructor_tx(
        artifact: Dict,
        constructor_types: List[str],
        constructor_args: List[Any],
        gas_limit: int,
        tx_params: Optional[Dict] = None,
    ) -> Dict:
        """
        Generic constructor transaction builder for eth_call pattern.

        Args:
            artifact: Contract artifact with bytecode
            constructor_types: ABI types for constructor parameters
            constructor_args: Actual constructor arguments
            gas_limit: Gas limit for the operation
            tx_params: Optional transaction parameters to override defaults

        Returns:
            Transaction dictionary ready for eth_call
        """
        encoded_args = encode(constructor_types, constructor_args)
        bytecode = ContractReader._extract_bytecode(artifact)
        data = bytecode + encoded_args.hex()

        default_params = {
            "from": ZERO_ADDRESS,
            "data": data,
            "gas": gas_limit,
            "gasPrice": DEFAULT_GAS_PRICE,
        }

        if tx_params:
            default_params.update(tx_params)

        return default_params

    @staticmethod
    def build_get_campaigns_constructor_tx(
        artifact: Dict,
        constructor_args: List[Any],
        tx_params: Optional[Dict] = None,
    ) -> Dict:
        """
        Build constructor transaction for BatchCampaigns contract.

        Args:
            artifact: Contract artifact with bytecode
            constructor_args: [platform_address, skip, limit]
            tx_params: Optional transaction parameters

        Returns:
            Transaction dictionary for eth_call
        """
        constructor_types = [
            "address",  # platform address
            "uint256",  # skip
            "uint256",  # limit
        ]
        return ContractReader._build_constructor_tx(
            artifact, constructor_types, constructor_args, GAS_LIMIT_CAMPAIGNS, tx_params
        )

    @staticmethod
    def build_get_campaigns_with_periods_constructor_tx(
        artifact: Dict,
        constructor_args: List[Any],
        tx_params: Optional[Dict] = None,
    ) -> Dict:
        """
        Build constructor transaction for BatchCampaignsWithPeriods contract.

        Args:
            artifact: Contract artifact with bytecode
            constructor_args: [platform_address, skip, limit]
            tx_params: Optional transaction parameters

        Returns:
            Transaction dictionary for eth_call
        """
        constructor_types = [
            "address",  # platform address
            "uint256",  # skip
            "uint256",  # limit
        ]
        return ContractReader._build_constructor_tx(
            artifact, constructor_types, constructor_args, GAS_LIMIT_CAMPAIGNS, tx_params
        )

    @staticmethod
    def decode_campaign_data(result: bytes) -> List[Dict]:
        """
        Decoder for campaign data from BatchCampaignsWithPeriods contract
        """
        try:
            # Define the nested struct types
            campaign_struct = [
                "uint256",  # chainId
                "address",  # gauge
                "address",  # manager
                "address",  # rewardToken
                "uint8",  # numberOfPeriods
                "uint256",  # maxRewardPerVote
                "uint256",  # totalRewardAmount
                "uint256",  # totalDistributed
                "uint256",  # startTimestamp
                "uint256",  # endTimestamp
                "address",  # hook
            ]

            period_struct = [
                "uint256",  # rewardPerPeriod
                "uint256",  # rewardPerVote
                "uint256",  # leftover
                "bool",  # updated
            ]

            period_data_struct = [
                "uint256",  # epoch
                f"({','.join(period_struct)})",  # period
            ]

            campaign_data_type = [
                "uint256",  # id
                f"({','.join(campaign_struct)})",  # campaign
                "bool",  # isClosed
                "bool",  # isWhitelistOnly
                "address[]",  # addresses
                "uint256",  # currentEpoch
                "uint256",  # remainingPeriods
                f"({','.join(period_data_struct)})[]",  # periods array
            ]

            full_type = f"({','.join(campaign_data_type)})[]"

            if len(result) % 32 != 0:
                result = result[: -(len(result) % 32)]

            raw_data = decode([full_type], result)[0]

            # Convert to extended campaign data with all periods
            campaigns = []
            for idx, data in enumerate(raw_data):
                try:
                    # Validate data structure before processing
                    if not data or len(data) < 8:
                        print(f"Warning: Campaign at index {idx} has insufficient data fields (got {len(data) if data else 0}, expected >= 8)")
                        continue

                    # Validate nested campaign struct
                    if not data[1] or len(data[1]) < 11:
                        print(f"Warning: Campaign at index {idx} has malformed campaign struct (got {len(data[1]) if data[1] else 0}, expected 11 fields)")
                        continue

                    # Extract campaign metadata first to validate against periods
                    number_of_periods = data[1][4]
                    campaign_id = data[0]

                    # Extract all periods
                    periods = []
                    for period_idx, period_data in enumerate(data[7]):
                        try:
                            if not period_data or len(period_data) < 2:
                                # Malformed period data - treat as decode failure
                                raise ValueError(f"Period {period_idx} has insufficient data fields")

                            periods.append(
                                {
                                    "timestamp": period_data[0],
                                    "reward_per_period": period_data[1][0],
                                    "reward_per_vote": period_data[1][1],
                                    "leftover": period_data[1][2],
                                    "updated": period_data[1][3],
                                    "point_data_inserted": False,  # This would need to be checked separately
                                }
                            )
                        except (IndexError, TypeError) as e:
                            # Period decode error - treat as decode failure
                            raise ValueError(f"Period {period_idx} decode error: {str(e)}")

                    # Validate period count matches expected number_of_periods
                    # This catches truncated responses from "max code size" errors
                    if len(periods) != number_of_periods:
                        # Calculate how many periods should have occurred by now
                        start_timestamp = data[1][8]
                        current_time = int(time.time())
                        PERIOD_DURATION = 604800  # Weekly periods in seconds

                        expected_past_periods = 0
                        future_periods = 0
                        for i in range(number_of_periods):
                            epoch = start_timestamp + (i * PERIOD_DURATION)
                            if epoch <= current_time:
                                expected_past_periods += 1
                            else:
                                future_periods += 1

                        # Only warn if we're missing past periods that should exist
                        if len(periods) < expected_past_periods:
                            print(
                                f"Warning: Campaign {campaign_id} period count mismatch: "
                                f"expected {expected_past_periods} past periods, got {len(periods)}. "
                                f"Response truncated - returning campaign with available period data. "
                                f"({future_periods} future periods not yet started)"
                            )
                        # If all missing periods are in the future, this is expected - no warning needed

                    campaign = {
                        "id": campaign_id,
                        "campaign": {
                            "chain_id": data[1][0],
                            "gauge": data[1][1],
                            "manager": data[1][2],
                            "reward_token": data[1][3],
                            "number_of_periods": number_of_periods,
                            "max_reward_per_vote": data[1][5],
                            "total_reward_amount": data[1][6],
                            "total_distributed": data[1][7],
                            "start_timestamp": data[1][8],
                            "end_timestamp": data[1][9],
                            "hook": data[1][10],
                        },
                        "is_closed": data[2],
                        "is_whitelist_only": data[3],
                        "addresses": data[4],
                        "current_epoch": data[5],
                        "remaining_periods": data[6],
                        "periods": periods,
                    }

                    # Validate final campaign structure
                    if "id" not in campaign or "campaign" not in campaign:
                        print(f"Warning: Campaign at index {idx} missing required fields after decode")
                        continue

                    campaigns.append(campaign)

                except (IndexError, TypeError, KeyError) as e:
                    print(f"Warning: Failed to decode campaign at index {idx} (ID: {data[0] if data and len(data) > 0 else 'unknown'}): {str(e)}")
                    continue

            return campaigns

        except Exception as e:
            error_msg = str(e)
            # Only print detailed debug info for unexpected errors
            # For known truncation issues, let the caller handle it
            is_truncation_error = (
                "Period count mismatch" in error_msg or
                "truncated response" in error_msg.lower()
            )

            if not is_truncation_error:
                print(f"Error decoding campaign data with periods: {error_msg}")
                print(f"Raw result length: {len(result)} bytes")
                print(f"Raw result (first 200 chars): {result.hex()[:200]}")
            raise

    @staticmethod
    def decode_result(types: List[str], result: bytes) -> Any:
        """Generic decoder for other contract calls"""
        return decode(types, result)

    @staticmethod
    def build_get_inserted_proofs_constructor_tx(
        artifact: Dict,
        oracle_address: str,
        gauge_address: str,
        user_addresses: List[str],
        epochs: List[int],
        tx_params: Optional[Dict] = None,
    ) -> Dict:
        """
        Build constructor transaction for GetInsertedProofs contract.

        Args:
            artifact: Contract artifact with bytecode
            oracle_address: Address of the Oracle contract
            gauge_address: Address of the gauge to query
            user_addresses: List of user addresses to check
            epochs: List of epochs to query
            tx_params: Optional transaction parameters

        Returns:
            Transaction dictionary ready for eth_call
        """
        constructor_types = [
            "address",  # oracle
            "address",  # gauge
            "address[]",  # users array
            "uint256[]",  # epochs array
        ]

        constructor_args = [
            to_checksum_address(oracle_address),
            to_checksum_address(gauge_address),
            [to_checksum_address(addr) for addr in user_addresses],
            epochs,
        ]

        return ContractReader._build_constructor_tx(
            artifact, constructor_types, constructor_args, GAS_LIMIT_INSERTED_PROOFS, tx_params
        )

    @staticmethod
    def decode_inserted_proofs(result: bytes) -> List[Dict[str, Any]]:
        """
        Decode the result from GetInsertedProofs contract call.

        Returns a list of dictionaries, one for each epoch, with:
        - epoch: uint256
        - is_block_updated: bool
        - point_data_results: List of (gauge, is_updated) tuples
        - voted_slope_data_results: List of (account, gauge, is_updated) tuples
        """
        try:
            # Define the nested struct types for decoding
            point_data_result_type = "(address,bool)"  # (gauge, is_updated)
            voted_slope_data_result_type = (
                "(address,address,bool)"  # (account, gauge, is_updated)
            )

            epoch_return_data_type = f"(uint256,bool,{point_data_result_type}[],{voted_slope_data_result_type}[])"

            # Decode as array of EpochReturnData
            return_data_type = f"{epoch_return_data_type}[]"

            # Decode the result
            decoded = decode([return_data_type], result)[0]

            # Format the result for each epoch
            epoch_results = []
            for epoch_data in decoded:
                epoch_results.append(
                    {
                        "epoch": epoch_data[0],
                        "is_block_updated": epoch_data[1],
                        "point_data_results": [
                            {"gauge": point[0], "is_updated": point[1]}
                            for point in epoch_data[2]
                        ],
                        "voted_slope_data_results": [
                            {
                                "account": vote[0],
                                "gauge": vote[1],
                                "is_updated": vote[2],
                            }
                            for vote in epoch_data[3]
                        ],
                    }
                )

            return epoch_results

        except Exception as e:
            print(f"Error decoding inserted proofs result: {str(e)}")
            print(f"Raw result (hex): {result.hex()}")
            raise

    @staticmethod
    def build_get_ccip_fee_constructor_tx(
        artifact: Dict,
        router_address: str,
        dest_chain_selector: int,
        dest_chain_id: int,
        receiver: str,
        execution_gas_limit: int,
        tokens: List[Dict[str, Any]],
        payload: bytes,
        tx_params: Optional[Dict] = None,
    ) -> Dict:
        """
        Build constructor transaction for GetCCIPFee contract.

        Args:
            artifact: Contract artifact with bytecode
            router_address: CCIP router address
            dest_chain_selector: Destination chain selector (CCIP identifier)
            dest_chain_id: Destination chain ID
            receiver: Receiver address on destination chain
            execution_gas_limit: Gas limit for execution
            tokens: List of token dictionaries with tokenAddress and amount
            payload: Encoded payload bytes
            tx_params: Optional transaction parameters

        Returns:
            Transaction dictionary for eth_call
        """
        # Convert token list to the expected format
        formatted_tokens = [
            (to_checksum_address(token["tokenAddress"]), token["amount"])
            for token in tokens
        ]

        constructor_types = [
            "address",  # _router
            "uint64",  # _destChainSelector
            "uint256",  # _destChainId
            "address",  # _receiver
            "uint256",  # _executionGasLimit
            "(address,uint256)[]",  # _tokens array
            "bytes",  # _payload
        ]

        constructor_args = [
            router_address,
            dest_chain_selector,
            dest_chain_id,
            receiver,
            execution_gas_limit,
            formatted_tokens,
            payload,
        ]

        return ContractReader._build_constructor_tx(
            artifact, constructor_types, constructor_args, GAS_LIMIT_CCIP_FEE, tx_params
        )

    @staticmethod
    def build_get_active_campaign_ids_constructor_tx(
        artifact: Dict,
        platform_address: str,
        start_id: int = 0,
        limit: int = 1000,
        tx_params: Optional[Dict] = None,
    ) -> Dict:
        """
        Build constructor transaction for GetActiveCampaignIds contract.

        Args:
            artifact: Contract artifact with bytecode
            platform_address: VoteMarket platform address
            start_id: Starting campaign ID to check
            limit: Maximum number of campaigns to check
            tx_params: Optional transaction parameters

        Returns:
            Transaction dictionary for eth_call
        """
        constructor_types = [
            "address",  # platform
            "uint256",  # startId
            "uint256",  # limit
        ]

        constructor_args = [
            platform_address,
            start_id,
            limit,
        ]

        return ContractReader._build_constructor_tx(
            artifact, constructor_types, constructor_args, GAS_LIMIT_ACTIVE_CAMPAIGNS, tx_params
        )

    @staticmethod
    def decode_active_campaign_ids(result: bytes) -> Dict[str, Any]:
        """
        Decode the result from GetActiveCampaignIds contract.

        Args:
            result: Raw result bytes from eth_call

        Returns:
            Dictionary containing:
                - campaign_ids: List of active campaign IDs
                - total_checked: Number of campaigns checked
                - total_active: Number of active campaigns found
        """
        try:
            # The contract returns ActiveCampaignBatch struct
            batch_struct_type = [
                "uint256[]",  # campaignIds
                "uint256",  # totalChecked
                "uint256",  # totalActive
            ]

            full_type = f"({','.join(batch_struct_type)})"

            # Decode the result
            decoded = decode([full_type], result)[0]

            return {
                "campaign_ids": list(decoded[0]),
                "total_checked": decoded[1],
                "total_active": decoded[2],
            }

        except Exception as e:
            print(f"Error decoding active campaign IDs: {str(e)}")
            raise
