from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List, Dict, Any, ClassVar
from pydantic import BaseModel
from web3 import Web3
from eth_utils import keccak


class EIP712Struct(ABC, BaseModel):
    SOL_TYPE_NAME: ClassVar[str]
    TYPE_STRING: ClassVar[str]
    TYPE_STRUCT: ClassVar[List[Dict]]

    @property
    def sol_type_name(self) -> str:
        return self.__class__.SOL_TYPE_NAME

    @property
    def type_string(self) -> str:
        return self.__class__.TYPE_STRING

    @property
    def type_struct(self) -> List[Dict]:
        return self.__class__.TYPE_STRUCT

    @property
    def type_hash(self) -> bytes:
        return keccak(text=self.__class__.TYPE_STRING)

    @abstractmethod
    def hash(self) -> bytes:
        pass

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "EIP712Struct":
        field_names = cls.__annotations__.keys()
        kwargs = {field: data.get(field) for field in field_names if field in data}

        for field, value in kwargs.items():
            field_type = cls.__annotations__.get(field, None)

            if (
                isinstance(value, dict)
                and hasattr(field_type, "__origin__")
                and issubclass(field_type.__origin__, EIP712Struct)
            ):
                kwargs[field] = field_type.from_dict(value)

            elif (
                isinstance(value, dict)
                and isinstance(field_type, type)
                and issubclass(field_type, EIP712Struct)
            ):
                kwargs[field] = field_type.from_dict(value)

        return cls(**kwargs)

    @staticmethod
    def _clean(value: Any):
        if isinstance(value, (bytes, bytearray)):
            return Web3.to_hex(value)
        if isinstance(value, EIP712Struct):
            return value.eip_signable_struct
        if isinstance(value, (list, tuple)):
            return [EIP712Struct._clean(v) for v in value]
        if isinstance(value, dict):
            return {k: EIP712Struct._clean(v) for k, v in value.items()}
        return value

    @property
    def eip_signable_struct(self) -> Dict[str, Any]:
        raw = self.model_dump(mode="python", exclude_none=True)
        return {k: self._clean(v) for k, v in raw.items()}
