import logging
from typing import Any, Dict, List, Optional

from boxtime.http.http import http_get
from boxtime.models import Balance, BlockHeader, EmissionInfo, ErgoBox, Transaction

logger = logging.getLogger(__name__)


class ErgoNodeClient:
    """
    Client for the Ergo Node REST API.

    Requires a node running with the extra blockchain indexer enabled
    (``ergo.node.extraIndex = true`` in the node config).

    :param node_url: Base URL of the Ergo node API.
    :param api_key: Optional API key for protected endpoints.
    """

    def __init__(
        self,
        node_url: str = "http://127.0.0.1:9053",
        api_key: Optional[str] = None,
    ):
        self.base_url = node_url
        self._headers: Dict[str, str] = {}
        if api_key is not None:
            self._headers["api_key"] = api_key

    def _get(self, path: str, params: Optional[dict] = None) -> Optional[Any]:
        return http_get(
            f"{self.base_url}{path}",
            params=params,
            headers=self._headers or None,
        )

    # ------------------------------------------------------------------
    # Node info
    # ------------------------------------------------------------------

    def get_node_info(self) -> Optional[dict]:
        """Get general information about the node."""
        return self._get("/info")

    def get_indexed_height(self) -> Optional[int]:
        """Get the current indexed block height."""
        raw = self._get("/blockchain/indexedHeight")
        if raw is None:
            return None
        return raw.get("indexedHeight") or raw.get("fullHeight")

    # ------------------------------------------------------------------
    # Transactions
    # ------------------------------------------------------------------

    def get_transaction(self, tx_id: str) -> Optional[Transaction]:
        """Get a transaction by its id."""
        raw = self._get(f"/blockchain/transaction/byId/{tx_id}")
        if raw is None:
            return None
        return Transaction(**raw)

    def get_transaction_by_index(self, index: int) -> Optional[Transaction]:
        """Get a transaction by its global index."""
        raw = self._get(f"/blockchain/transaction/byIndex/{index}")
        if raw is None:
            return None
        return Transaction(**raw)

    def get_transactions_by_address(
        self,
        address: str,
        offset: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> Optional[List[Transaction]]:
        """Get transactions for an address."""
        params: Dict[str, Any] = {}
        if offset is not None:
            params["offset"] = offset
        if limit is not None:
            params["limit"] = limit
        raw = self._get(
            f"/blockchain/transaction/byAddress/{address}",
            params=params or None,
        )
        if raw is None:
            return None
        return [Transaction(**tx) for tx in raw]

    # ------------------------------------------------------------------
    # Boxes
    # ------------------------------------------------------------------

    def get_box(self, box_id: str) -> Optional[ErgoBox]:
        """Get a box by its id."""
        raw = self._get(f"/blockchain/box/byId/{box_id}")
        if raw is None:
            return None
        return ErgoBox(**raw)

    def get_box_by_index(self, index: int) -> Optional[ErgoBox]:
        """Get a box by its global index."""
        raw = self._get(f"/blockchain/box/byIndex/{index}")
        if raw is None:
            return None
        return ErgoBox(**raw)

    def get_unspent_boxes_by_address(
        self,
        address: str,
        offset: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> Optional[List[ErgoBox]]:
        """Get unspent boxes for an address."""
        params: Dict[str, Any] = {}
        if offset is not None:
            params["offset"] = offset
        if limit is not None:
            params["limit"] = limit
        raw = self._get(
            f"/blockchain/box/unspent/byAddress/{address}",
            params=params or None,
        )
        if raw is None:
            return None
        return [ErgoBox(**box) for box in raw]

    # ------------------------------------------------------------------
    # Blocks
    # ------------------------------------------------------------------

    def get_block_header_at_height(self, height: int) -> Optional[BlockHeader]:
        """Get the block header at a given height."""
        header_ids = self._get(f"/blocks/at/{height}")
        if not header_ids:
            return None
        header_id = header_ids[0] if isinstance(header_ids, list) else header_ids
        raw = self._get(f"/blocks/{header_id}/header")
        if raw is None:
            return None
        return BlockHeader(**raw)

    def get_chain_slice(
        self, from_height: int, to_height: int
    ) -> Optional[List[BlockHeader]]:
        """Get block headers for a range of heights in a single call."""
        raw = self._get(
            "/blocks/chainSlice",
            params={"fromHeight": from_height, "toHeight": to_height},
        )
        if raw is None:
            return None
        return [BlockHeader(**h) for h in raw]

    def get_block_transaction_ids(self, height: int) -> Optional[List[str]]:
        """Get transaction IDs for the block at a given height."""
        header_ids = self._get(f"/blocks/at/{height}")
        if not header_ids:
            return None
        header_id = header_ids[0] if isinstance(header_ids, list) else header_ids
        raw = self._get(f"/blocks/{header_id}/transactions")
        if raw is None:
            return None
        txs = raw.get("transactions", [])
        return [tx["id"] for tx in txs if "id" in tx]

    # ------------------------------------------------------------------
    # Emission
    # ------------------------------------------------------------------

    def get_emission_at(self, height: int) -> Optional[EmissionInfo]:
        """Get emission data for a given block height."""
        raw = self._get(f"/emission/at/{height}")
        if raw is None:
            return None
        return EmissionInfo(**raw)

    # ------------------------------------------------------------------
    # Balance (derived from unspent boxes)
    # ------------------------------------------------------------------

    def get_address_balance(self, address: str) -> Optional[Balance]:
        """Get the confirmed balance for an address by summing unspent boxes."""
        raw = self._get(f"/blockchain/box/unspent/byAddress/{address}")
        if raw is None:
            return None
        total_nano = sum(box.get("value", 0) for box in raw)
        token_map: Dict[str, int] = {}
        for box in raw:
            for asset in box.get("assets", []):
                tid = asset.get("tokenId", "")
                token_map[tid] = token_map.get(tid, 0) + asset.get("amount", 0)
        tokens = [{"tokenId": tid, "amount": amt} for tid, amt in token_map.items()]
        return Balance(**{"nanoErgs": total_nano, "tokens": tokens})
