"""Synor RPC Client."""

import asyncio
import json
import time
from typing import Any, Callable, Optional
import httpx
import websockets

from .types import (
    RpcConfig,
    Network,
    Priority,
    TransactionStatus,
    SubscriptionType,
    BlockHeader,
    Block,
    TxInput,
    TxOutput,
    ScriptPubKey,
    Transaction,
    FeeEstimate,
    ChainInfo,
    MempoolInfo,
    UTXO,
    Balance,
    Subscription,
)


class RpcError(Exception):
    """Synor RPC SDK error."""

    def __init__(
        self,
        message: str,
        status_code: Optional[int] = None,
        code: Optional[str] = None,
    ):
        super().__init__(message)
        self.status_code = status_code
        self.code = code


class SynorRpc:
    """
    Synor RPC SDK client.

    Example:
        >>> async with SynorRpc(api_key="sk_...") as rpc:
        ...     block = await rpc.get_latest_block()
        ...     print(f"Height: {block.height}")
        ...
        ...     # Subscribe to new blocks
        ...     async def on_block(block):
        ...         print(f"New block: {block.height}")
        ...     sub = await rpc.subscribe_blocks(on_block)
    """

    def __init__(
        self,
        api_key: str,
        endpoint: str = "https://rpc.synor.cc/api/v1",
        ws_endpoint: str = "wss://rpc.synor.cc/ws",
        network: Network = Network.MAINNET,
        timeout: float = 30.0,
        debug: bool = False,
    ):
        self.config = RpcConfig(
            api_key=api_key,
            endpoint=endpoint,
            ws_endpoint=ws_endpoint,
            network=network,
            timeout=timeout,
            debug=debug,
        )
        self._client = httpx.AsyncClient(
            base_url=endpoint,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "X-Network": network.value,
            },
            timeout=timeout,
        )
        self._ws: Optional[websockets.WebSocketClientProtocol] = None
        self._subscriptions: dict[str, Callable] = {}
        self._ws_task: Optional[asyncio.Task] = None

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

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    async def close(self) -> None:
        """Close the client."""
        await self._client.aclose()
        if self._ws:
            await self._ws.close()
        if self._ws_task:
            self._ws_task.cancel()

    # ==================== Block Operations ====================

    async def get_block(self, hash_or_height: str | int) -> Block:
        """Get block by hash or height."""
        if isinstance(hash_or_height, int):
            path = f"/blocks/height/{hash_or_height}"
        else:
            path = f"/blocks/{hash_or_height}"

        data = await self._request("GET", path)
        return self._parse_block(data)

    async def get_latest_block(self) -> Block:
        """Get the latest block."""
        data = await self._request("GET", "/blocks/latest")
        return self._parse_block(data)

    async def get_block_header(self, hash_or_height: str | int) -> BlockHeader:
        """Get block header only."""
        if isinstance(hash_or_height, int):
            path = f"/blocks/height/{hash_or_height}/header"
        else:
            path = f"/blocks/{hash_or_height}/header"

        data = await self._request("GET", path)
        return self._parse_block_header(data)

    async def get_blocks(self, start_height: int, end_height: int) -> list[Block]:
        """Get blocks in a range."""
        data = await self._request(
            "GET",
            "/blocks",
            params={"start": str(start_height), "end": str(end_height)},
        )
        return [self._parse_block(b) for b in data.get("blocks", [])]

    # ==================== Transaction Operations ====================

    async def get_transaction(self, txid: str) -> Transaction:
        """Get transaction by hash."""
        data = await self._request("GET", f"/transactions/{txid}")
        return self._parse_transaction(data)

    async def get_raw_transaction(self, txid: str) -> str:
        """Get raw transaction hex."""
        data = await self._request("GET", f"/transactions/{txid}/raw")
        return data["raw"]

    async def send_raw_transaction(self, raw_tx: str) -> str:
        """Send a raw transaction."""
        data = await self._request("POST", "/transactions", {"raw": raw_tx})
        return data["txid"]

    async def get_address_transactions(
        self,
        address: str,
        limit: int = 20,
        offset: int = 0,
    ) -> list[Transaction]:
        """Get transactions for an address."""
        data = await self._request(
            "GET",
            f"/addresses/{address}/transactions",
            params={"limit": str(limit), "offset": str(offset)},
        )
        return [self._parse_transaction(tx) for tx in data.get("transactions", [])]

    # ==================== Fee Estimation ====================

    async def estimate_fee(self, priority: Priority = Priority.MEDIUM) -> FeeEstimate:
        """Estimate fee for a priority level."""
        data = await self._request(
            "GET", "/fees/estimate", params={"priority": priority.value}
        )
        return FeeEstimate(
            priority=Priority(data["priority"]),
            fee_rate=data["feeRate"],
            estimated_blocks=data["estimatedBlocks"],
        )

    async def get_all_fee_estimates(self) -> list[FeeEstimate]:
        """Get all fee estimates."""
        data = await self._request("GET", "/fees/estimate/all")
        return [
            FeeEstimate(
                priority=Priority(e["priority"]),
                fee_rate=e["feeRate"],
                estimated_blocks=e["estimatedBlocks"],
            )
            for e in data.get("estimates", [])
        ]

    # ==================== Chain Information ====================

    async def get_chain_info(self) -> ChainInfo:
        """Get chain information."""
        data = await self._request("GET", "/chain")
        return ChainInfo(
            chain=data["chain"],
            network=data["network"],
            height=data["height"],
            best_block_hash=data["bestBlockHash"],
            difficulty=data["difficulty"],
            median_time=data["medianTime"],
            chain_work=data["chainWork"],
            syncing=data["syncing"],
            sync_progress=data["syncProgress"],
        )

    async def get_mempool_info(self) -> MempoolInfo:
        """Get mempool information."""
        data = await self._request("GET", "/mempool")
        return MempoolInfo(
            size=data["size"],
            bytes=data["bytes"],
            usage=data["usage"],
            max_mempool=data["maxMempool"],
            min_fee=data["minFee"],
        )

    async def get_mempool_transactions(self, limit: int = 100) -> list[str]:
        """Get mempool transaction IDs."""
        data = await self._request(
            "GET", "/mempool/transactions", params={"limit": str(limit)}
        )
        return data.get("txids", [])

    # ==================== UTXO Operations ====================

    async def get_utxos(self, address: str) -> list[UTXO]:
        """Get UTXOs for an address."""
        data = await self._request("GET", f"/addresses/{address}/utxos")
        return [
            UTXO(
                txid=u["txid"],
                vout=u["vout"],
                amount=u["amount"],
                address=u["address"],
                confirmations=u["confirmations"],
                script_pub_key=u.get("scriptPubKey"),
            )
            for u in data.get("utxos", [])
        ]

    async def get_balance(self, address: str) -> Balance:
        """Get balance for an address."""
        data = await self._request("GET", f"/addresses/{address}/balance")
        return Balance(
            confirmed=data["confirmed"],
            unconfirmed=data["unconfirmed"],
            total=data["total"],
        )

    # ==================== Subscriptions ====================

    async def subscribe_blocks(
        self, callback: Callable[[Block], None]
    ) -> Subscription:
        """Subscribe to new blocks."""
        return await self._subscribe(
            SubscriptionType.BLOCKS,
            lambda data: callback(self._parse_block(data["block"])),
        )

    async def subscribe_address(
        self, address: str, callback: Callable[[Transaction], None]
    ) -> Subscription:
        """Subscribe to address transactions."""
        return await self._subscribe(
            SubscriptionType.ADDRESS,
            lambda data: callback(self._parse_transaction(data["transaction"])),
            {"address": address},
        )

    async def subscribe_mempool(
        self, callback: Callable[[Transaction], None]
    ) -> Subscription:
        """Subscribe to mempool transactions."""
        return await self._subscribe(
            SubscriptionType.MEMPOOL,
            lambda data: callback(self._parse_transaction(data["transaction"])),
        )

    async def _subscribe(
        self,
        sub_type: SubscriptionType,
        callback: Callable,
        params: Optional[dict] = None,
    ) -> Subscription:
        """Create a subscription."""
        await self._ensure_websocket()

        sub_id = f"{sub_type.value}_{int(time.time() * 1000)}"
        self._subscriptions[sub_id] = callback

        message = {
            "type": "subscribe",
            "id": sub_id,
            "subscription": sub_type.value,
            **(params or {}),
        }
        await self._ws.send(json.dumps(message))

        def unsubscribe():
            self._subscriptions.pop(sub_id, None)
            if self._ws:
                asyncio.create_task(
                    self._ws.send(json.dumps({"type": "unsubscribe", "id": sub_id}))
                )

        return Subscription(
            id=sub_id,
            type=sub_type,
            created_at=int(time.time() * 1000),
            unsubscribe=unsubscribe,
        )

    async def _ensure_websocket(self) -> None:
        """Ensure WebSocket connection is established."""
        if self._ws and self._ws.open:
            return

        ws_url = (
            f"{self.config.ws_endpoint}"
            f"?apiKey={self.config.api_key}"
            f"&network={self.config.network.value}"
        )

        self._ws = await websockets.connect(ws_url)
        self._ws_task = asyncio.create_task(self._ws_listener())

    async def _ws_listener(self) -> None:
        """Listen for WebSocket messages."""
        try:
            async for message in self._ws:
                try:
                    data = json.loads(message)
                    sub_id = data.get("subscriptionId")
                    if sub_id and sub_id in self._subscriptions:
                        self._subscriptions[sub_id](data.get("data", {}))
                except json.JSONDecodeError:
                    pass
        except websockets.ConnectionClosed:
            if self.config.debug:
                print("[SynorRpc] WebSocket closed")

    # ==================== HTTP Request ====================

    async def _request(
        self,
        method: str,
        path: str,
        data: Optional[dict[str, Any]] = None,
        params: Optional[dict[str, str]] = None,
    ) -> dict[str, Any]:
        """Make an API request."""
        if self.config.debug:
            print(f"[SynorRpc] {method} {path}")

        response = await self._client.request(
            method,
            path,
            json=data,
            params=params,
        )

        if response.status_code >= 400:
            error = (
                response.json()
                if response.content
                else {"message": response.reason_phrase}
            )
            raise RpcError(
                error.get("message", "Request failed"),
                response.status_code,
                error.get("code"),
            )

        return response.json()

    # ==================== Parsers ====================

    def _parse_block_header(self, data: dict) -> BlockHeader:
        """Parse block header from API response."""
        return BlockHeader(
            hash=data["hash"],
            height=data["height"],
            version=data["version"],
            previous_hash=data["previousHash"],
            merkle_root=data["merkleRoot"],
            timestamp=data["timestamp"],
            difficulty=data["difficulty"],
            nonce=data["nonce"],
        )

    def _parse_block(self, data: dict) -> Block:
        """Parse block from API response."""
        return Block(
            hash=data["hash"],
            height=data["height"],
            version=data["version"],
            previous_hash=data["previousHash"],
            merkle_root=data["merkleRoot"],
            timestamp=data["timestamp"],
            difficulty=data["difficulty"],
            nonce=data["nonce"],
            transactions=data.get("transactions", []),
            size=data.get("size", 0),
            weight=data.get("weight", 0),
            tx_count=data.get("txCount", 0),
        )

    def _parse_transaction(self, data: dict) -> Transaction:
        """Parse transaction from API response."""
        inputs = [
            TxInput(
                txid=i["txid"],
                vout=i["vout"],
                script_sig=i["scriptSig"],
                sequence=i["sequence"],
            )
            for i in data.get("inputs", [])
        ]

        outputs = [
            TxOutput(
                value=o["value"],
                n=o["n"],
                script_pub_key=ScriptPubKey(
                    asm=o["scriptPubKey"]["asm"],
                    hex=o["scriptPubKey"]["hex"],
                    type=o["scriptPubKey"]["type"],
                    addresses=o["scriptPubKey"].get("addresses", []),
                ),
            )
            for o in data.get("outputs", [])
        ]

        return Transaction(
            txid=data["txid"],
            confirmations=data.get("confirmations", 0),
            status=TransactionStatus(data.get("status", "pending")),
            size=data.get("size", 0),
            fee=data.get("fee", "0"),
            inputs=inputs,
            outputs=outputs,
            block_hash=data.get("blockHash"),
            block_height=data.get("blockHeight"),
            timestamp=data.get("timestamp"),
            raw=data.get("raw"),
        )
