"""Robust BTC price fetching with retry logic and multiple API sources.

This module provides a reliable way to fetch current BTC-USD prices with:
- Retry logic with exponential backoff
- Multiple API sources as fallbacks
- Price validation and sanity checks
"""

import logging
from datetime import datetime
from typing import Optional

from tenacity import (
    Retrying,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

from ._optional import import_optional

requests = import_optional(
    "requests",
    extra="network",
    feature="BTC price fetching helpers",
)

logger = logging.getLogger(__name__)

# Price validation constants
MIN_BTC_PRICE = 1000.0  # Minimum reasonable BTC price (USD)
MAX_BTC_PRICE = 1000000.0  # Maximum reasonable BTC price (USD)
MAX_PRICE_CHANGE_PCT = 20.0  # Maximum reasonable day-over-day change (%)
DEFAULT_RETRY_POLICY = Retrying(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type(requests.RequestException),
    reraise=True,
)


def fetch_price_coingecko() -> float:
    """Fetch BTC price from CoinGecko API.

    Returns:
        float: BTC price in USD

    Raises:
        requests.RequestException: If the API request fails
        ValueError: If the response format is invalid
    """
    response = requests.get(
        "https://api.coingecko.com/api/v3/simple/price",
        params={"ids": "bitcoin", "vs_currencies": "usd"},
        timeout=10,
    )
    response.raise_for_status()
    data = response.json()

    if "bitcoin" not in data or "usd" not in data["bitcoin"]:
        raise ValueError(f"Invalid CoinGecko response format: {data}")

    price = float(data["bitcoin"]["usd"])
    logger.debug(f"CoinGecko returned price: ${price:,.2f}")
    return price


def fetch_price_coinbase() -> float:
    """Fetch BTC price from Coinbase API.

    Returns:
        float: BTC price in USD

    Raises:
        requests.RequestException: If the API request fails
        ValueError: If the response format is invalid
    """
    response = requests.get(
        "https://api.coinbase.com/v2/prices/BTC-USD/spot",
        timeout=10,
    )
    response.raise_for_status()
    data = response.json()

    if "data" not in data or "amount" not in data["data"]:
        raise ValueError(f"Invalid Coinbase response format: {data}")

    price = float(data["data"]["amount"])
    logger.debug(f"Coinbase returned price: ${price:,.2f}")
    return price


def fetch_price_bitstamp() -> float:
    """Fetch BTC price from Bitstamp API.

    Returns:
        float: BTC price in USD

    Raises:
        requests.RequestException: If the API request fails
        ValueError: If the response format is invalid
    """
    response = requests.get(
        "https://www.bitstamp.net/api/v2/ticker/btcusd/",
        timeout=10,
    )
    response.raise_for_status()
    data = response.json()

    if "last" not in data:
        raise ValueError(f"Invalid Bitstamp response format: {data}")

    price = float(data["last"])
    logger.debug(f"Bitstamp returned price: ${price:,.2f}")
    return price


def fetch_price_kraken() -> float:
    """Fetch BTC price from Kraken API.

    Returns:
        float: BTC price in USD

    Raises:
        requests.RequestException: If the API request fails
        ValueError: If the response format is invalid
    """
    response = requests.get(
        "https://api.kraken.com/0/public/Ticker?pair=XBTUSD",
        timeout=10,
    )
    response.raise_for_status()
    data = response.json()

    if data.get("error"):
        raise ValueError(f"Kraken API error: {data['error']}")

    # Kraken uses XXBTZUSD as the pair name
    result = data.get("result", {})
    pair_data = result.get("XXBTZUSD") or result.get("XBTUSD")

    if not pair_data or "c" not in pair_data:
        raise ValueError(f"Invalid Kraken response format: {data}")

    # "c" is the last trade closed array [price, lot volume]
    price = float(pair_data["c"][0])
    logger.debug(f"Kraken returned price: ${price:,.2f}")
    return price


def fetch_price_binance() -> float:
    """Fetch BTC price from Binance API.

    Returns:
        float: BTC price in USD

    Raises:
        requests.RequestException: If the API request fails
        ValueError: If the response format is invalid
    """
    response = requests.get(
        "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",
        timeout=10,
    )
    response.raise_for_status()
    data = response.json()

    if "price" not in data:
        raise ValueError(f"Invalid Binance response format: {data}")

    price = float(data["price"])
    logger.debug(f"Binance returned price: ${price:,.2f}")
    return price


def fetch_historical_price_coingecko(date: datetime) -> float:
    """Fetch historical BTC price from CoinGecko API for a specific date.

    Args:
        date: datetime for the date to fetch

    Returns:
        float: BTC price in USD
    """
    date_str = date.strftime("%d-%m-%Y")
    response = requests.get(
        "https://api.coingecko.com/api/v3/coins/bitcoin/history",
        params={"date": date_str, "localization": "false"},
        timeout=15,
    )
    response.raise_for_status()
    data = response.json()

    if "market_data" not in data or "current_price" not in data["market_data"]:
        raise ValueError(f"Invalid CoinGecko historical response for {date}: {data}")

    price = float(data["market_data"]["current_price"]["usd"])
    logger.debug(f"CoinGecko historical ({date_str}) returned price: ${price:,.2f}")
    return price


def fetch_historical_price_binance(date: datetime) -> float:
    """Fetch historical BTC price from Binance API for a specific date.

    Uses the 1d candlestick data.

    Args:
        date: datetime for the date to fetch

    Returns:
        float: BTC price in USD
    """
    # Start of the date in milliseconds
    start_time = int(date.timestamp() * 1000)
    # 1 day later
    end_time = start_time + (24 * 60 * 60 * 1000)

    response = requests.get(
        "https://api.binance.com/api/v3/klines",
        params={
            "symbol": "BTCUSDT",
            "interval": "1d",
            "startTime": start_time,
            "endTime": end_time,
            "limit": 1,
        },
        timeout=10,
    )
    response.raise_for_status()
    data = response.json()

    if not data or not isinstance(data, list) or len(data) == 0:
        raise ValueError(f"No Binance historical data for {date}")

    # Index 4 is the close price
    price = float(data[0][4])
    logger.debug(f"Binance historical ({date.date()}) returned price: ${price:,.2f}")
    return price


def fetch_btc_price_historical(
    date: datetime,
    previous_price: Optional[float] = None,
) -> Optional[float]:
    """Fetch historical BTC price with retry logic and fallback sources.

    Uses public API sources with retry/fallback behavior.

    Args:
        date: datetime for the date to fetch
        previous_price: Optional previous price for validation

    Returns:
        float: BTC price in USD, or None if all sources fail
    """
    sources = [
        (lambda: fetch_historical_price_coingecko(date), "CoinGecko Historical"),
        (lambda: fetch_historical_price_binance(date), "Binance Historical"),
    ]

    for fetch_func, source_name in sources:
        try:
            # We use a longer wait for historical as rate limits are tighter
            price = fetch_func()
            if validate_price(price, previous_price):
                logger.debug(
                    f"Successfully fetched historical BTC price for {date.date()} from {source_name}: ${price:,.2f}"
                )
                return price
            else:
                logger.warning(
                    f"{source_name} returned invalid historical price for {date.date()}: ${price:,.2f}"
                )
        except Exception as e:
            logger.warning(
                f"Failed to fetch historical BTC price for {date.date()} from {source_name}: {e}"
            )

    return None


def validate_price(price: float, previous_price: Optional[float] = None) -> bool:
    """Validate that a BTC price is reasonable.

    Args:
        price: The price to validate
        previous_price: Optional previous price for sanity check

    Returns:
        bool: True if price is valid, False otherwise
    """
    # Basic range check
    if not isinstance(price, (int, float)) or price <= 0:
        logger.warning(f"Invalid price type or non-positive: {price}")
        return False

    if price < MIN_BTC_PRICE or price > MAX_BTC_PRICE:
        logger.warning(
            f"Price {price:,.2f} outside reasonable range [{MIN_BTC_PRICE}, {MAX_BTC_PRICE}]"
        )
        return False

    # Sanity check against previous price if provided
    if previous_price is not None and previous_price > 0:
        change_pct = abs((price - previous_price) / previous_price) * 100
        if change_pct > MAX_PRICE_CHANGE_PCT:
            logger.warning(
                f"Price change {change_pct:.2f}% exceeds threshold {MAX_PRICE_CHANGE_PCT}% "
                f"(previous: ${previous_price:,.2f}, current: ${price:,.2f})"
            )
            # Don't reject, just warn - could be legitimate volatility

    return True


def _fetch_with_retry(
    fetch_func,
    source_name: str,
    *,
    retry_policy: Retrying | None = None,
) -> float:
    """Fetch price from a single source with retry logic.

    Args:
        fetch_func: Function to call for fetching price
        source_name: Name of the source for logging

    Returns:
        float: BTC price in USD

    Raises:
        requests.RequestException: If all retries fail
    """
    policy = retry_policy or DEFAULT_RETRY_POLICY
    for attempt in policy:
        with attempt:
            try:
                price = fetch_func()
                logger.debug(f"Successfully fetched from {source_name}: ${price:,.2f}")
                return price
            except requests.RequestException as e:
                logger.debug(f"Request failed for {source_name}: {e}")
                raise
            except (ValueError, KeyError) as e:
                logger.warning(f"Invalid response format from {source_name}: {e}")
                raise ValueError(f"{source_name} returned invalid data: {e}") from e
    raise RuntimeError("Retry policy exhausted unexpectedly without returning.")


def fetch_btc_price_robust(
    previous_price: Optional[float] = None,
    sources: Optional[list] = None,
    retry_policy: Retrying | None = None,
) -> Optional[float]:
    """Fetch BTC price with retry logic and multiple fallback sources.

    This function tries multiple API sources in order, with retry logic for each.
    If all sources fail, returns None.

    Args:
        previous_price: Optional previous price for validation/sanity checks
        sources: Optional list of (fetch_func, source_name) tuples.
                 Defaults to [CoinGecko, Coinbase, Bitstamp]

    Returns:
        float: Current BTC price in USD, or None if all sources fail
    """
    if sources is None:
        sources = [
            (fetch_price_coingecko, "CoinGecko"),
            (fetch_price_coinbase, "Coinbase"),
            (fetch_price_bitstamp, "Bitstamp"),
            (fetch_price_kraken, "Kraken"),
            (fetch_price_binance, "Binance"),
        ]

    errors = []

    for fetch_func, source_name in sources:
        try:
            price = _fetch_with_retry(
                fetch_func,
                source_name,
                retry_policy=retry_policy,
            )

            # Validate the price
            if validate_price(price, previous_price):
                logger.info(
                    f"Successfully fetched BTC price from {source_name}: ${price:,.2f}"
                )
                return price
            else:
                error_msg = f"{source_name} returned invalid price: ${price:,.2f}"
                logger.warning(error_msg)
                errors.append(error_msg)
        except requests.RequestException as e:
            error_msg = f"{source_name} request failed: {str(e)}"
            logger.warning(error_msg)
            errors.append(error_msg)
        except (ValueError, KeyError) as e:
            error_msg = f"{source_name} invalid response: {str(e)}"
            logger.warning(error_msg)
            errors.append(error_msg)
        except Exception as e:
            error_msg = f"{source_name} unexpected error: {str(e)}"
            logger.error(error_msg, exc_info=True)
            errors.append(error_msg)

    # All sources failed
    logger.error(f"All BTC price sources failed. Errors: {'; '.join(errors)}")
    return None


if __name__ == "__main__":
    # Configure logging for direct execution
    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")

    print("Checking Bitcoin price sources...")
    print("-" * 40)

    test_sources = [
        (fetch_price_coingecko, "CoinGecko"),
        (fetch_price_coinbase, "Coinbase"),
        (fetch_price_bitstamp, "Bitstamp"),
        (fetch_price_kraken, "Kraken"),
        (fetch_price_binance, "Binance"),
    ]

    success_count = 0
    for fetch_func, name in test_sources:
        try:
            price = fetch_func()
            print(f"✓ {name:<12}: ${price:,.2f}")
            success_count += 1
        except Exception as e:
            print(f"✗ {name:<12}: Error - {e}")

    print("-" * 40)
    robust_price = fetch_btc_price_robust()
    if robust_price:
        print(f"FINAL RESULT  : ${robust_price:,.2f} ({success_count}/{len(test_sources)} sources up)")
    else:
        print("FINAL RESULT  : FAILED (All sources down)")
