"""Cointime Economics — coinblocks created metrics."""

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

from boxtime.clients.coingecko_client import CoinGeckoClient
from boxtime.clients.ergo_node_client import ErgoNodeClient
from boxtime.models.cointime.cointime_data_point import CointimeDataPoint
from boxtime.models.price.price import PricePoint

logger = logging.getLogger(__name__)

NANO_ERG_FACTOR = 1_000_000_000


def _nano_to_erg(nano: int) -> float:
    """Convert a nanoERG amount to ERG."""
    return nano / NANO_ERG_FACTOR


class Cointime:
    """
    Cointime Economics calculator.

    Internally creates an :class:`ErgoNodeClient` and, optionally, a
    :class:`CoinGeckoClient` from the supplied connection parameters.

    :param node_url: Base URL of the Ergo node API.
    :param node_api_key: Optional API key for protected node endpoints.
    :param coingecko_api_key: Optional CoinGecko Demo API key.  When
        provided, price-denominated methods become available.
    """

    def __init__(
        self,
        node_url: str = "http://127.0.0.1:9053",
        node_api_key: Optional[str] = None,
        coingecko_api_key: Optional[str] = None,
    ):
        self._node_client = ErgoNodeClient(node_url, node_api_key)
        self._price_client: Optional[CoinGeckoClient] = (
            CoinGeckoClient(coingecko_api_key) if coingecko_api_key else None
        )

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def node_client(self) -> ErgoNodeClient:
        """The underlying Ergo node client."""
        return self._node_client

    @property
    def price_client(self) -> Optional[CoinGeckoClient]:
        """The underlying CoinGecko price client, or ``None``."""
        return self._price_client

    # ------------------------------------------------------------------
    # Price
    # ------------------------------------------------------------------

    def _require_price_client(self) -> CoinGeckoClient:
        """Return the price client or raise if not configured."""
        if self._price_client is None:
            raise ValueError(
                "CoinGecko API key required for price methods"
            )
        return self._price_client

    def get_price(self) -> Optional[float]:
        """Return the current ERG/USD spot price."""
        return self._require_price_client().get_current_price("usd")

    def get_price_history(
        self, start_height: int, end_height: int
    ) -> Optional[List[PricePoint]]:
        """Return ERG/USD price history between two block heights.

        Fetches the chain slice to obtain timestamps, then queries
        CoinGecko for the corresponding time range.  Granularity is
        determined by CoinGecko (5-min / hourly / daily).
        """
        client = self._require_price_client()
        headers = self._node_client.get_chain_slice(start_height, end_height)
        if not headers or len(headers) < 2:
            return None
        from_ts = headers[0].timestamp // 1000
        to_ts = headers[-1].timestamp // 1000
        history = client.get_price_history_range(from_ts, to_ts, "usd")
        if history is None:
            return None
        return history.prices

    # ------------------------------------------------------------------
    # Coinblocks created
    # ------------------------------------------------------------------

    def coinblocks_created(self, height: int) -> Optional[int]:
        """Return coinblocks created at *height*.

        Coinblocks created at a given block height equals the total
        circulating supply at that height (in nanoERGs).
        """
        emission = self._node_client.get_emission_at(height)
        if emission is None:
            return None
        return emission.total_coins_issued

    def total_coinblocks_created(
        self, start_height: int, end_height: int
    ) -> Optional[int]:
        """Return total coinblocks created over a height range.

        Sums ``coinblocks_created`` for every height from *start_height*
        to *end_height* (inclusive).
        """
        total = 0
        for h in range(start_height, end_height + 1):
            cbc = self.coinblocks_created(h)
            if cbc is None:
                logger.warning("Failed to fetch emission at height %d", h)
                return None
            total += cbc
        return total

    # ------------------------------------------------------------------
    # Coinblocks destroyed
    # ------------------------------------------------------------------

    def coinblocks_destroyed(self, height: int) -> Optional[int]:
        """Return coinblocks destroyed at *height*.

        For each input spent in the block's transactions, the destroyed
        coinblocks equal ``input.value * (height - input.inclusion_height)``.
        The total CBD is the sum across all inputs.
        """
        tx_ids = self._node_client.get_block_transaction_ids(height)
        if tx_ids is None:
            return None
        total_cbd = 0
        for tx_id in tx_ids:
            tx = self._node_client.get_transaction(tx_id)
            if tx is None:
                logger.warning("Failed to fetch transaction %s", tx_id)
                return None
            for inp in tx.inputs:
                if inp.inclusion_height is None:
                    continue
                lifespan = height - inp.inclusion_height
                total_cbd += inp.value * lifespan
        return total_cbd

    def total_coinblocks_destroyed(
        self, start_height: int, end_height: int
    ) -> Optional[int]:
        """Return total coinblocks destroyed over a height range.

        Sums ``coinblocks_destroyed`` for every height from *start_height*
        to *end_height* (inclusive).
        """
        total = 0
        for h in range(start_height, end_height + 1):
            cbd = self.coinblocks_destroyed(h)
            if cbd is None:
                logger.warning("Failed to fetch CBD at height %d", h)
                return None
            total += cbd
        return total

    # ------------------------------------------------------------------
    # Coinblocks stored
    # ------------------------------------------------------------------

    def coinblocks_stored(self, height: int) -> Optional[int]:
        """Return coinblocks stored at *height*.

        CBS is the net coinblocks added at each block:
        ``CBS(h) = CBC(h) - CBD(h)``.
        Can be negative when destruction exceeds creation.
        """
        cbc = self.coinblocks_created(height)
        cbd = self.coinblocks_destroyed(height)
        if cbc is None or cbd is None:
            return None
        return cbc - cbd

    def total_coinblocks_stored(
        self, start_height: int, end_height: int
    ) -> Optional[int]:
        """Return total coinblocks stored over a height range.

        Sums ``coinblocks_stored`` for every height from *start_height*
        to *end_height* (inclusive).
        """
        total = 0
        for h in range(start_height, end_height + 1):
            cbs = self.coinblocks_stored(h)
            if cbs is None:
                logger.warning("Failed to fetch CBS at height %d", h)
                return None
            total += cbs
        return total

    # ------------------------------------------------------------------
    # Convenience get_* methods (price-enriched)
    # ------------------------------------------------------------------

    def _build_cointime_data_points(
        self,
        start_height: int,
        end_height: int,
        compute_value: Callable[[int], Optional[int]],
    ) -> Optional[List[CointimeDataPoint]]:
        """Shared logic for the ``get_coinblocks_*`` convenience methods.

        Fetches the chain slice and price history for the given height
        range, then iterates CoinGecko price points.  Only price points
        whose timestamp exactly matches a block header timestamp are
        included in the result.
        """
        client = self._require_price_client()
        headers = self._node_client.get_chain_slice(start_height, end_height)
        if not headers:
            return None

        # Build timestamp → height lookup from chain slice
        ts_to_height: Dict[int, int] = {
            h.timestamp: h.height for h in headers
        }

        # Fetch price history covering the same time range
        from_ts = headers[0].timestamp // 1000
        to_ts = headers[-1].timestamp // 1000
        history = client.get_price_history_range(from_ts, to_ts, "usd")
        if history is None or not history.prices:
            return None

        results: List[CointimeDataPoint] = []
        for price_point in history.prices:
            height = ts_to_height.get(price_point.timestamp)
            if height is None:
                continue
            value = compute_value(height)
            if value is None:
                continue
            results.append(
                CointimeDataPoint(
                    height=height,
                    timestamp=price_point.timestamp,
                    value=value,
                    price=price_point.price,
                )
            )

        return results if results else None

    def get_coinblocks_created(
        self, start_height: int, end_height: int
    ) -> Optional[List[CointimeDataPoint]]:
        """Return coinblocks created with ERG/USD price for a height range.

        Each :class:`CointimeDataPoint` bundles height, timestamp,
        coinblocks-created value, and the ERG/USD price at that
        timestamp.  Only heights whose block timestamp exactly matches
        a CoinGecko price timestamp are included.
        """
        return self._build_cointime_data_points(
            start_height, end_height, self.coinblocks_created
        )

    def get_coinblocks_destroyed(
        self, start_height: int, end_height: int
    ) -> Optional[List[CointimeDataPoint]]:
        """Return coinblocks destroyed with ERG/USD price for a height range.

        Each :class:`CointimeDataPoint` bundles height, timestamp,
        coinblocks-destroyed value, and the ERG/USD price at that
        timestamp.  Only heights whose block timestamp exactly matches
        a CoinGecko price timestamp are included.
        """
        return self._build_cointime_data_points(
            start_height, end_height, self.coinblocks_destroyed
        )

    def get_coinblocks_stored(
        self, start_height: int, end_height: int
    ) -> Optional[List[CointimeDataPoint]]:
        """Return coinblocks stored with ERG/USD price for a height range.

        Each :class:`CointimeDataPoint` bundles height, timestamp,
        coinblocks-stored value, and the ERG/USD price at that
        timestamp.  Only heights whose block timestamp exactly matches
        a CoinGecko price timestamp are included.
        """
        return self._build_cointime_data_points(
            start_height, end_height, self.coinblocks_stored
        )
