"""
Synor IBC SDK Client

Inter-Blockchain Communication (IBC) client for cross-chain interoperability.
"""

import asyncio
import base64
import hashlib
from typing import Optional, List, Any, Dict, Callable
import httpx
import websockets

from .types import (
    IbcConfig,
    IbcException,
    Height,
    ClientId,
    ClientState,
    ConsensusState,
    Header,
    ConnectionId,
    ConnectionEnd,
    PortId,
    ChannelId,
    Channel,
    ChannelOrder,
    Packet,
    Acknowledgement,
    SwapId,
    SwapState,
    SwapAsset,
    AtomicSwap,
    Htlc,
    Hashlock,
    CommitmentProof,
    IbcEvent,
    Version,
)


class LightClientClient:
    """Light client operations sub-client"""

    def __init__(self, ibc: "SynorIbc"):
        self._ibc = ibc

    async def create(
        self,
        client_type: str,
        client_state: ClientState,
        consensus_state: ConsensusState,
    ) -> ClientId:
        """Create a new light client"""
        result = await self._ibc._post("/clients", {
            "client_type": client_type,
            "client_state": self._serialize_client_state(client_state),
            "consensus_state": self._serialize_consensus_state(consensus_state),
        })
        return ClientId(id=result["client_id"])

    async def update(self, client_id: ClientId, header: Header) -> Height:
        """Update a light client with new header"""
        result = await self._ibc._post(f"/clients/{client_id.id}/update", {
            "header": header.__dict__,
        })
        return Height(**result)

    async def get_state(self, client_id: ClientId) -> ClientState:
        """Get client state"""
        return await self._ibc._get(f"/clients/{client_id.id}/state")

    async def get_consensus_state(self, client_id: ClientId, height: Height) -> ConsensusState:
        """Get consensus state at height"""
        return await self._ibc._get(
            f"/clients/{client_id.id}/consensus/{height.revision_number}-{height.revision_height}"
        )

    async def list(self) -> List[Dict[str, Any]]:
        """List all clients"""
        return await self._ibc._get("/clients")

    async def is_active(self, client_id: ClientId) -> bool:
        """Check if client is active"""
        result = await self._ibc._get(f"/clients/{client_id.id}/status")
        return result.get("active", False)

    def _serialize_client_state(self, state: ClientState) -> Dict[str, Any]:
        return {
            "chain_id": state.chain_id,
            "trust_level": {"numerator": state.trust_level.numerator, "denominator": state.trust_level.denominator},
            "trusting_period": state.trusting_period,
            "unbonding_period": state.unbonding_period,
            "max_clock_drift": state.max_clock_drift,
            "latest_height": {"revision_number": state.latest_height.revision_number, "revision_height": state.latest_height.revision_height},
        }

    def _serialize_consensus_state(self, state: ConsensusState) -> Dict[str, Any]:
        return {
            "timestamp": state.timestamp,
            "root": base64.b64encode(state.root).decode(),
            "next_validators_hash": base64.b64encode(state.next_validators_hash).decode(),
        }


class ConnectionsClient:
    """Connection operations sub-client"""

    def __init__(self, ibc: "SynorIbc"):
        self._ibc = ibc

    async def open_init(
        self,
        client_id: ClientId,
        counterparty_client_id: ClientId,
        version: Optional[Version] = None,
        delay_period: int = 0,
    ) -> ConnectionId:
        """Initialize connection handshake"""
        result = await self._ibc._post("/connections/init", {
            "client_id": client_id.id,
            "counterparty_client_id": counterparty_client_id.id,
            "version": version.__dict__ if version else None,
            "delay_period": delay_period,
        })
        return ConnectionId(id=result["connection_id"])

    async def open_try(
        self,
        client_id: ClientId,
        counterparty_client_id: ClientId,
        counterparty_connection_id: ConnectionId,
        counterparty_versions: List[Version],
        proof_init: bytes,
        proof_height: Height,
    ) -> ConnectionId:
        """Try connection handshake (counterparty)"""
        result = await self._ibc._post("/connections/try", {
            "client_id": client_id.id,
            "counterparty_client_id": counterparty_client_id.id,
            "counterparty_connection_id": counterparty_connection_id.id,
            "counterparty_versions": [v.__dict__ for v in counterparty_versions],
            "proof_init": base64.b64encode(proof_init).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })
        return ConnectionId(id=result["connection_id"])

    async def open_ack(
        self,
        connection_id: ConnectionId,
        counterparty_connection_id: ConnectionId,
        version: Version,
        proof_try: bytes,
        proof_height: Height,
    ) -> None:
        """Acknowledge connection handshake"""
        await self._ibc._post(f"/connections/{connection_id.id}/ack", {
            "counterparty_connection_id": counterparty_connection_id.id,
            "version": version.__dict__,
            "proof_try": base64.b64encode(proof_try).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })

    async def open_confirm(
        self,
        connection_id: ConnectionId,
        proof_ack: bytes,
        proof_height: Height,
    ) -> None:
        """Confirm connection handshake"""
        await self._ibc._post(f"/connections/{connection_id.id}/confirm", {
            "proof_ack": base64.b64encode(proof_ack).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })

    async def get(self, connection_id: ConnectionId) -> ConnectionEnd:
        """Get connection by ID"""
        return await self._ibc._get(f"/connections/{connection_id.id}")

    async def list(self) -> List[Dict[str, Any]]:
        """List all connections"""
        return await self._ibc._get("/connections")


class ChannelsClient:
    """Channel operations sub-client"""

    def __init__(self, ibc: "SynorIbc"):
        self._ibc = ibc

    async def bind_port(self, port_id: PortId, module: str) -> None:
        """Bind a port"""
        await self._ibc._post("/ports/bind", {"port_id": port_id.id, "module": module})

    async def release_port(self, port_id: PortId) -> None:
        """Release port binding"""
        await self._ibc._post(f"/ports/{port_id.id}/release", {})

    async def open_init(
        self,
        port_id: PortId,
        ordering: ChannelOrder,
        connection_id: ConnectionId,
        counterparty_port: PortId,
        version: str,
    ) -> ChannelId:
        """Initialize channel handshake"""
        result = await self._ibc._post("/channels/init", {
            "port_id": port_id.id,
            "ordering": ordering.value,
            "connection_id": connection_id.id,
            "counterparty_port": counterparty_port.id,
            "version": version,
        })
        return ChannelId(id=result["channel_id"])

    async def open_try(
        self,
        port_id: PortId,
        ordering: ChannelOrder,
        connection_id: ConnectionId,
        counterparty_port: PortId,
        counterparty_channel: ChannelId,
        version: str,
        counterparty_version: str,
        proof_init: bytes,
        proof_height: Height,
    ) -> ChannelId:
        """Try channel handshake (counterparty)"""
        result = await self._ibc._post("/channels/try", {
            "port_id": port_id.id,
            "ordering": ordering.value,
            "connection_id": connection_id.id,
            "counterparty_port": counterparty_port.id,
            "counterparty_channel": counterparty_channel.id,
            "version": version,
            "counterparty_version": counterparty_version,
            "proof_init": base64.b64encode(proof_init).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })
        return ChannelId(id=result["channel_id"])

    async def open_ack(
        self,
        port_id: PortId,
        channel_id: ChannelId,
        counterparty_channel_id: ChannelId,
        counterparty_version: str,
        proof_try: bytes,
        proof_height: Height,
    ) -> None:
        """Acknowledge channel handshake"""
        await self._ibc._post(f"/channels/{port_id.id}/{channel_id.id}/ack", {
            "counterparty_channel_id": counterparty_channel_id.id,
            "counterparty_version": counterparty_version,
            "proof_try": base64.b64encode(proof_try).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })

    async def open_confirm(
        self,
        port_id: PortId,
        channel_id: ChannelId,
        proof_ack: bytes,
        proof_height: Height,
    ) -> None:
        """Confirm channel handshake"""
        await self._ibc._post(f"/channels/{port_id.id}/{channel_id.id}/confirm", {
            "proof_ack": base64.b64encode(proof_ack).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })

    async def close_init(self, port_id: PortId, channel_id: ChannelId) -> None:
        """Close channel init"""
        await self._ibc._post(f"/channels/{port_id.id}/{channel_id.id}/close", {})

    async def get(self, port_id: PortId, channel_id: ChannelId) -> Channel:
        """Get channel"""
        return await self._ibc._get(f"/channels/{port_id.id}/{channel_id.id}")

    async def list(self) -> List[Dict[str, Any]]:
        """List all channels"""
        return await self._ibc._get("/channels")


class PacketsClient:
    """Packet operations sub-client"""

    def __init__(self, ibc: "SynorIbc"):
        self._ibc = ibc

    async def send(
        self,
        source_port: PortId,
        source_channel: ChannelId,
        data: bytes,
        timeout_height: Optional[Height] = None,
        timeout_timestamp: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Send a packet"""
        return await self._ibc._post("/packets/send", {
            "source_port": source_port.id,
            "source_channel": source_channel.id,
            "data": base64.b64encode(data).decode(),
            "timeout_height": {"revision_number": timeout_height.revision_number, "revision_height": timeout_height.revision_height} if timeout_height else None,
            "timeout_timestamp": timeout_timestamp,
        })

    async def recv(
        self,
        packet: Packet,
        proof: bytes,
        proof_height: Height,
    ) -> Acknowledgement:
        """Receive a packet (called by relayer)"""
        return await self._ibc._post("/packets/recv", {
            "packet": self._serialize_packet(packet),
            "proof": base64.b64encode(proof).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })

    async def ack(
        self,
        packet: Packet,
        acknowledgement: Acknowledgement,
        proof: bytes,
        proof_height: Height,
    ) -> None:
        """Acknowledge a packet"""
        await self._ibc._post("/packets/ack", {
            "packet": self._serialize_packet(packet),
            "acknowledgement": acknowledgement.__dict__,
            "proof": base64.b64encode(proof).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
        })

    async def timeout(
        self,
        packet: Packet,
        proof: bytes,
        proof_height: Height,
        next_sequence_recv: int,
    ) -> None:
        """Timeout a packet"""
        await self._ibc._post("/packets/timeout", {
            "packet": self._serialize_packet(packet),
            "proof": base64.b64encode(proof).decode(),
            "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height},
            "next_sequence_recv": str(next_sequence_recv),
        })

    def _serialize_packet(self, packet: Packet) -> Dict[str, Any]:
        return {
            "sequence": str(packet.sequence),
            "source_port": packet.source_port.id,
            "source_channel": packet.source_channel.id,
            "dest_port": packet.dest_port.id,
            "dest_channel": packet.dest_channel.id,
            "data": base64.b64encode(packet.data).decode(),
            "timeout_height": {"revision_number": packet.timeout_height.revision_number, "revision_height": packet.timeout_height.revision_height},
            "timeout_timestamp": packet.timeout_timestamp,
        }


class TransferClient:
    """Token transfer operations sub-client (ICS-20)"""

    def __init__(self, ibc: "SynorIbc"):
        self._ibc = ibc

    async def transfer(
        self,
        source_port: str,
        source_channel: str,
        token: Dict[str, str],
        sender: str,
        receiver: str,
        timeout_height: Optional[Height] = None,
        timeout_timestamp: Optional[int] = None,
        memo: str = "",
    ) -> Dict[str, Any]:
        """Transfer tokens to another chain"""
        return await self._ibc._post("/transfer", {
            "source_port": source_port,
            "source_channel": source_channel,
            "token": token,
            "sender": sender,
            "receiver": receiver,
            "timeout_height": {"revision_number": timeout_height.revision_number, "revision_height": timeout_height.revision_height} if timeout_height else None,
            "timeout_timestamp": timeout_timestamp,
            "memo": memo,
        })

    async def get_denom_trace(self, ibc_denom: str) -> Dict[str, str]:
        """Get denom trace for an IBC token"""
        return await self._ibc._get(f"/transfer/denom_trace/{ibc_denom}")

    async def list_denom_traces(self) -> List[Dict[str, str]]:
        """Get all denom traces"""
        return await self._ibc._get("/transfer/denom_traces")


class SwapsClient:
    """Atomic swap operations sub-client (HTLC)"""

    def __init__(self, ibc: "SynorIbc"):
        self._ibc = ibc

    async def initiate(
        self,
        responder: str,
        initiator_asset: Dict[str, Any],
        responder_asset: Dict[str, Any],
        channel_id: Optional[str] = None,
        port_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Initiate an atomic swap"""
        return await self._ibc._post("/swaps/initiate", {
            "responder": responder,
            "initiator_asset": initiator_asset,
            "responder_asset": responder_asset,
            "channel_id": channel_id,
            "port_id": port_id,
        })

    async def lock(self, swap_id: str) -> None:
        """Lock initiator's tokens"""
        await self._ibc._post(f"/swaps/{swap_id}/lock", {})

    async def respond(self, swap_id: str, asset: Dict[str, Any]) -> Dict[str, Any]:
        """Respond to a swap (lock responder's tokens)"""
        return await self._ibc._post(f"/swaps/{swap_id}/respond", {"asset": asset})

    async def claim(self, swap_id: str, secret: bytes) -> Dict[str, Any]:
        """Claim tokens with secret"""
        return await self._ibc._post(f"/swaps/{swap_id}/claim", {
            "secret": base64.b64encode(secret).decode(),
        })

    async def refund(self, swap_id: str) -> Dict[str, Any]:
        """Refund expired swap"""
        return await self._ibc._post(f"/swaps/{swap_id}/refund", {})

    async def cancel(self, swap_id: str) -> None:
        """Cancel pending swap"""
        await self._ibc._post(f"/swaps/{swap_id}/cancel", {})

    async def get(self, swap_id: str) -> Dict[str, Any]:
        """Get swap by ID"""
        return await self._ibc._get(f"/swaps/{swap_id}")

    async def list_active(self) -> List[Dict[str, Any]]:
        """List active swaps"""
        return await self._ibc._get("/swaps/active")

    async def list_by_participant(self, address: str) -> List[Dict[str, Any]]:
        """List swaps by participant"""
        return await self._ibc._get(f"/swaps/participant/{address}")

    def verify_secret(self, hashlock: bytes, secret: bytes) -> bool:
        """Verify hashlock with secret"""
        computed = hashlib.sha256(secret).digest()
        return hashlock == computed


class SynorIbc:
    """
    Synor IBC SDK Client

    Main client for Inter-Blockchain Communication operations.
    """

    def __init__(self, config: IbcConfig):
        self._config = config
        self._closed = False
        self._http = httpx.AsyncClient(
            base_url=config.endpoint,
            timeout=config.timeout,
            headers={
                "Authorization": f"Bearer {config.api_key}",
                "Content-Type": "application/json",
                "X-SDK-Version": "python/0.1.0",
                "X-Chain-Id": config.chain_id,
            },
        )
        self._ws = None

        # Sub-clients
        self.clients = LightClientClient(self)
        self.connections = ConnectionsClient(self)
        self.channels = ChannelsClient(self)
        self.packets = PacketsClient(self)
        self.transfer = TransferClient(self)
        self.swaps = SwapsClient(self)

    @property
    def chain_id(self) -> str:
        """Get chain ID"""
        return self._config.chain_id

    async def get_chain_info(self) -> Dict[str, Any]:
        """Get chain info"""
        return await self._get("/chain")

    async def get_height(self) -> Height:
        """Get current height"""
        result = await self._get("/chain/height")
        return Height(**result)

    async def get_client_state_proof(self, client_id: str) -> CommitmentProof:
        """Get client state proof"""
        return await self._get(f"/proofs/client_state/{client_id}")

    async def get_connection_proof(self, connection_id: str) -> CommitmentProof:
        """Get connection proof"""
        return await self._get(f"/proofs/connection/{connection_id}")

    async def get_channel_proof(self, port_id: str, channel_id: str) -> CommitmentProof:
        """Get channel proof"""
        return await self._get(f"/proofs/channel/{port_id}/{channel_id}")

    async def health_check(self) -> bool:
        """Health check"""
        try:
            result = await self._get("/health")
            return result.get("status") == "healthy"
        except Exception:
            return False

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

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

    async def _get(self, path: str) -> Any:
        """HTTP GET request"""
        return await self._request("GET", path)

    async def _post(self, path: str, body: Dict[str, Any]) -> Any:
        """HTTP POST request"""
        return await self._request("POST", path, body)

    async def _delete(self, path: str) -> Any:
        """HTTP DELETE request"""
        return await self._request("DELETE", path)

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

        last_error = None
        for attempt in range(self._config.retries + 1):
            try:
                if method == "GET":
                    response = await self._http.get(path)
                elif method == "POST":
                    response = await self._http.post(path, json=body)
                elif method == "DELETE":
                    response = await self._http.delete(path)
                else:
                    raise ValueError(f"Unknown method: {method}")

                if response.status_code >= 400:
                    try:
                        error = response.json()
                    except Exception:
                        error = {}
                    raise IbcException(
                        error.get("message", f"HTTP {response.status_code}"),
                        code=error.get("code"),
                        status=response.status_code,
                    )

                return response.json()

            except IbcException:
                raise
            except Exception as e:
                last_error = e
                if attempt < self._config.retries:
                    await asyncio.sleep(2 ** attempt * 0.1)

        raise IbcException(str(last_error), code="NETWORK_ERROR")

    async def subscribe_events(self, callback: Callable[[IbcEvent], None]) -> Callable[[], None]:
        """Subscribe to IBC events"""
        return await self._subscribe("events", callback)

    async def _subscribe(self, topic: str, callback: Callable[[Any], None]) -> Callable[[], None]:
        """Subscribe to WebSocket topic"""
        if not self._ws:
            self._ws = await websockets.connect(
                self._config.ws_endpoint,
                extra_headers={"Authorization": f"Bearer {self._config.api_key}"},
            )

        await self._ws.send(f'{{"type":"subscribe","topic":"{topic}"}}')

        async def listener():
            async for message in self._ws:
                import json
                data = json.loads(message)
                if data.get("topic") == topic:
                    callback(data.get("event"))

        task = asyncio.create_task(listener())

        def unsubscribe():
            task.cancel()
            asyncio.create_task(self._ws.send(f'{{"type":"unsubscribe","topic":"{topic}"}}'))

        return unsubscribe
