# EmpoorioChain Python SDK
import json
import asyncio
from typing import Dict, List, Optional, Any, Union
from dataclasses import dataclass
import websockets
import aiohttp
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from web3.contract import Contract
from web3.providers.rpc import HTTPProvider


@dataclass
class EmpoorioChainConfig:
    rpc_url: str
    ws_url: Optional[str] = None
    chain_id: int = 1234


@dataclass
class Transaction:
    from_address: str
    to_address: str
    value: str
    gas: Optional[str] = None
    gas_price: Optional[str] = None
    data: Optional[str] = None
    nonce: Optional[int] = None


@dataclass
class Block:
    number: int
    hash: str
    parent_hash: str
    timestamp: int
    transactions: List[str]
    gas_used: str
    gas_limit: str


@dataclass
class AccountBalance:
    address: str
    balance: str
    symbol: str = "DMS"


class EmpoorioChainSDK:
    def __init__(self, config: EmpoorioChainConfig):
        self.config = config
        self.w3 = Web3(HTTPProvider(config.rpc_url))
        self.session: Optional[aiohttp.ClientSession] = None
        self.ws: Optional[websockets.WebSocketServerProtocol] = None

    async def connect(self) -> None:
        """Conectar al nodo de EmpoorioChain"""
        if not self.w3.is_connected():
            raise ConnectionError("No se pudo conectar al nodo RPC")

        network_id = self.w3.eth.chain_id
        if network_id != self.config.chain_id:
            raise ValueError(f"Chain ID mismatch. Expected {self.config.chain_id}, got {network_id}")

        print("Conectado a EmpoorioChain")

    async def disconnect(self) -> None:
        """Desconectar del nodo"""
        if self.session:
            await self.session.close()
        if self.ws:
            await self.ws.close()

    async def get_chain_id(self) -> int:
        """Obtener el ID de la cadena"""
        return self.w3.eth.chain_id

    async def get_node_info(self) -> Dict[str, Any]:
        """Obtener información del nodo"""
        info = {
            "client_version": self.w3.client_version,
            "chain_id": self.w3.eth.chain_id,
            "network_id": self.w3.net.version,
            "peer_count": self.w3.net.peer_count,
            "syncing": self.w3.eth.syncing,
        }
        return info

    async def get_gas_price(self) -> int:
        """Obtener gas price actual"""
        return int(self.w3.eth.gas_price)

    async def get_latest_block(self) -> Block:
        """Obtener el último bloque"""
        latest = self.w3.eth.block_number
        block = await self.get_block(latest)
        if not block:
            raise Exception("No se pudo obtener el último bloque")
        return block

    async def get_block_number(self) -> int:
        """Obtener el número del último bloque"""
        return self.w3.eth.block_number

    async def get_balance(self, address: str) -> AccountBalance:
        """Obtener el balance de una cuenta"""
        try:
            balance = self.w3.eth.get_balance(address)
            return AccountBalance(
                address=address,
                balance=str(balance),
                symbol="DMS"
            )
        except Exception as e:
            raise Exception(f"Error al obtener balance: {e}")

    async def get_transaction_count(self, address: str) -> int:
        """Obtener el nonce de una cuenta"""
        return self.w3.eth.get_transaction_count(address)

    async def get_block(self, block_number: int) -> Optional[Block]:
        """Obtener información de un bloque"""
        try:
            block = self.w3.eth.get_block(block_number)
            if not block:
                return None

            return Block(
                number=block.number,
                hash=block.hash.hex(),
                parent_hash=block.parentHash.hex(),
                timestamp=block.timestamp,
                transactions=[tx.hex() for tx in block.transactions],
                gas_used=str(block.gasUsed),
                gas_limit=str(block.gasLimit)
            )
        except Exception as e:
            raise Exception(f"Error al obtener bloque: {e}")

    async def get_block_by_hash(self, hash_str: str) -> Optional[Block]:
        """Obtener información de un bloque por hash"""
        try:
            block = self.w3.eth.get_block(hash_str)
            if not block:
                return None

            return Block(
                number=block.number,
                hash=block.hash.hex(),
                parent_hash=block.parentHash.hex(),
                timestamp=block.timestamp,
                transactions=[tx.hex() for tx in block.transactions],
                gas_used=str(block.gasUsed),
                gas_limit=str(block.gasLimit)
            )
        except Exception as e:
            raise Exception(f"Error al obtener bloque: {e}")

    async def get_transaction(self, tx_hash: str) -> Optional[Dict[str, Any]]:
        """Obtener información de una transacción"""
        try:
            tx = self.w3.eth.get_transaction(tx_hash)
            if not tx:
                return None

            return {
                "hash": tx.hash.hex(),
                "block_number": tx.blockNumber,
                "from": tx["from"],
                "to": tx.to,
                "value": str(tx.value),
                "gas": str(tx.gas),
                "gas_price": str(tx.gasPrice),
                "data": tx.input.hex(),
                "nonce": tx.nonce
            }
        except Exception as e:
            raise Exception(f"Error al obtener transacción: {e}")

    async def send_transaction(self, tx: Transaction, private_key: Optional[str] = None) -> str:
        """Enviar una transacción"""
        try:
            transaction = {
                'from': tx.from_address,
                'to': tx.to_address,
                'value': int(tx.value),
                'gas': int(tx.gas) if tx.gas else 21000,
                'gasPrice': int(tx.gas_price) if tx.gas_price else self.w3.eth.gas_price,
                'data': tx.data or '0x',
                'nonce': tx.nonce or self.w3.eth.get_transaction_count(tx.from_address)
            }

            if private_key:
                # Firmar con clave privada
                signed_tx = self.w3.eth.account.sign_transaction(transaction, private_key)
                tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
            else:
                # Enviar sin firma (requiere wallet externa)
                tx_hash = self.w3.eth.send_transaction(transaction)

            return tx_hash.hex()
        except Exception as e:
            raise Exception(f"Error al enviar transacción: {e}")

    async def estimate_gas(self, tx: Transaction) -> str:
        """Estimar el gas para una transacción"""
        try:
            gas_estimate = self.w3.eth.estimate_gas({
                'to': tx.to_address,
                'value': int(tx.value),
                'data': tx.data or '0x'
            })
            return str(gas_estimate)
        except Exception as e:
            raise Exception(f"Error al estimar gas: {e}")

    def get_contract(self, address: str, abi: List[Dict[str, Any]]) -> Contract:
        """Obtener una instancia de contrato"""
        return self.w3.eth.contract(address=address, abi=abi)

    async def subscribe_to_new_blocks(self, callback: callable) -> None:
        """Suscribirse a nuevos bloques (requiere WebSocket)"""
        if not self.config.ws_url:
            raise ValueError("WebSocket URL no configurada")

        async with websockets.connect(self.config.ws_url) as websocket:
            subscription_request = {
                "jsonrpc": "2.0",
                "method": "eth_subscribe",
                "params": ["newHeads"],
                "id": 1
            }
            await websocket.send(json.dumps(subscription_request))

            response = await websocket.recv()
            subscription_id = json.loads(response)["result"]

            while True:
                message = await websocket.recv()
                data = json.loads(message)
                if data.get("method") == "eth_subscription":
                    block_number = int(data["params"]["result"]["number"], 16)
                    block = await self.get_block(block_number)
                    if block:
                        callback(block)

    async def subscribe_to_pending_transactions(self, callback: callable) -> None:
        """Suscribirse a transacciones pendientes"""
        if not self.config.ws_url:
            raise ValueError("WebSocket URL no configurada")

        async with websockets.connect(self.config.ws_url) as websocket:
            subscription_request = {
                "jsonrpc": "2.0",
                "method": "eth_subscribe",
                "params": ["pendingTransactions"],
                "id": 1
            }
            await websocket.send(json.dumps(subscription_request))

            while True:
                message = await websocket.recv()
                data = json.loads(message)
                if data.get("method") == "eth_subscription":
                    tx_hash = data["params"]["result"]
                    callback(tx_hash)

    async def wait_for_transaction(self, tx_hash: str, confirmations: int = 1) -> Dict[str, Any]:
        """Esperar a que una transacción sea confirmada"""
        try:
            receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
            return dict(receipt)
        except Exception as e:
            raise Exception(f"Error esperando transacción: {e}")

    # Utilidades
    @staticmethod
    def format_ether(wei: str) -> str:
        """Convertir wei a ether"""
        return Web3.from_wei(int(wei), 'ether')

    @staticmethod
    def parse_ether(ether: str) -> str:
        """Convertir ether a wei"""
        return str(Web3.to_wei(ether, 'ether'))

    # Staking (placeholder - implementar cuando esté disponible)
    async def stake(self, amount: str, private_key: str) -> str:
        """Stake tokens (no implementado aún)"""
        raise NotImplementedError("Staking no implementado")

    async def unstake(self, amount: str, private_key: str) -> str:
        """Unstake tokens (no implementado aún)"""
        raise NotImplementedError("Unstaking no implementado")

    # Gobernanza (placeholder)
    async def vote(self, proposal_id: int, vote: bool, private_key: str) -> str:
        """Votar en una propuesta (no implementado aún)"""
        raise NotImplementedError("Voting no implementado")


class Utils:
    @staticmethod
    def is_valid_address(address: str) -> bool:
        """Verificar si una dirección es válida"""
        return Web3.is_address(address)

    @staticmethod
    def generate_private_key() -> str:
        """Generar una clave privada aleatoria"""
        account = Account.create()
        return account.key.hex()

    @staticmethod
    def get_address_from_private_key(private_key: str) -> str:
        """Obtener dirección desde clave privada"""
        account = Account.from_key(private_key)
        return account.address

    @staticmethod
    def keccak256(data: str) -> str:
        """Calcular hash Keccak-256"""
        return Web3.keccak(text=data).hex()
