"""Main DiceAgent client for the Rollhub Dice API."""

import secrets
import time
from typing import List, Optional

import requests

from .exceptions import (
    AuthenticationError,
    InsufficientBalanceError,
    InvalidBetError,
    RateLimitError,
    RollhubError,
    ServerError,
)
from .models import Balance, Bet, BetResult, Proof, Registration, VerifyResult

DEFAULT_BASE_URL = "https://agent.rollhub.com/api/v1"
_MAX_RETRIES = 3
_RETRY_BACKOFF = 1.0


class DiceAgent:
    """Client for the Rollhub Dice Agent API.

    Usage::

        agent = DiceAgent(api_key="rh_sk_...")
        result = agent.bet(target=0.5, direction="over", amount=100)
        print(result)
    """

    def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self._session = requests.Session()
        self._session.headers.update({
            "X-API-Key": self.api_key,
            "Content-Type": "application/json",
            "User-Agent": "rollhub-dice-python/0.1.0",
        })

    @classmethod
    def register(cls, wallet_address: str, base_url: str = DEFAULT_BASE_URL) -> "DiceAgent":
        """Register a new agent and return an initialized client.

        Args:
            wallet_address: The wallet address to register.
            base_url: API base URL.

        Returns:
            An authenticated DiceAgent instance.
        """
        url = f"{base_url.rstrip('/')}/register"
        resp = requests.post(url, json={"wallet_address": wallet_address})
        _raise_for_status(resp)
        data = resp.json()
        return cls(api_key=data["api_key"], base_url=base_url)

    def balance(self) -> Balance:
        """Get current account balance."""
        data = self._get("/balance")
        return Balance(
            balance_usd=data["balance_usd"],
            balance_cents=data["balance_cents"],
            currency=data["currency"],
        )

    def bet(
        self,
        target: float,
        direction: str = "over",
        amount: int = 100,
        client_secret: Optional[str] = None,
    ) -> BetResult:
        """Place a dice bet.

        Args:
            target: Target threshold (0.0–1.0).
            direction: ``"over"`` or ``"under"``.
            amount: Wager in cents.
            client_secret: Optional client secret for provably fair verification.
                Generated automatically if not provided.

        Returns:
            A :class:`BetResult` with the outcome and proof.
        """
        if client_secret is None:
            client_secret = secrets.token_hex(16)

        data = self._post("/dice", json={
            "target": target,
            "direction": direction,
            "amount": amount,
            "client_secret": client_secret,
        })

        proof_data = data.get("proof", {})
        proof = Proof(
            server_secret=proof_data.get("server_secret", ""),
            client_secret=proof_data.get("client_secret", client_secret),
            nonce=proof_data.get("nonce", 0),
            server_seed_hash=proof_data.get("server_seed_hash", ""),
        )

        return BetResult(
            bet_id=data["bet_id"],
            roll=data["roll"],
            win=data["win"],
            payout=data["payout"],
            multiplier=data["multiplier"],
            balance=data["balance"],
            proof=proof,
        )

    def bets(self) -> List[Bet]:
        """Get bet history."""
        data = self._get("/bets")
        return [
            Bet(
                bet_id=b["bet_id"],
                roll=b["roll"],
                win=b["win"],
                payout=b["payout"],
                multiplier=b["multiplier"],
                target=b["target"],
                direction=b["direction"],
                amount=b["amount"],
                client_secret=b.get("client_secret", ""),
                nonce=b.get("nonce", 0),
                created_at=b.get("created_at"),
            )
            for b in data.get("bets", [])
        ]

    def strategies(self) -> list:
        """List available play strategies.

        Returns:
            A list of strategy dicts with name, description, risk info.
        """
        data = self._get("/strategies")
        return data.get("strategies", [])

    def play(
        self,
        strategy: str = "grinder",
        budget: int = 1000,
        rounds: Optional[int] = None,
        personality: str = "default",
        auto: bool = True,
        custom: Optional[dict] = None,
    ) -> dict:
        """Start a play session with a strategy and narration.

        Args:
            strategy: Strategy preset (grinder/sniper/martingale/degen/kelly/custom).
            budget: Budget in cents to allocate from balance.
            rounds: Max rounds (defaults to strategy max).
            personality: Narration style (default/hype/analyst/degen).
            auto: If True, runs all rounds at once. If False, step mode.
            custom: Custom strategy params (only if strategy="custom").

        Returns:
            Session result dict with events, narration, and stats.
        """
        payload: dict = {
            "strategy": strategy,
            "budget": budget,
            "personality": personality,
            "auto": auto,
        }
        if rounds is not None:
            payload["rounds"] = rounds
        if custom is not None:
            payload["custom"] = custom
        return self._post("/play", json=payload)

    def play_next(self, session_id: str, action: str = "continue") -> dict:
        """Advance a step-mode play session.

        Args:
            session_id: The play session ID.
            action: "continue", "stop", or "change_strategy".

        Returns:
            Next event(s) from the session.
        """
        return self._post(f"/play/{session_id}/next", json={"action": action})

    def play_status(self, session_id: str) -> dict:
        """Get current status of a play session."""
        return self._get(f"/play/{session_id}")

    def withdraw(
        self,
        amount_usd: float,
        currency: str,
        chain: str,
        address: str,
        memo: Optional[str] = None,
    ) -> dict:
        """Request a withdrawal.

        Args:
            amount_usd: Amount in USD (e.g. 5.50).
            currency: Token symbol (e.g. "USDC", "ETH", "SOL").
            chain: Blockchain network (e.g. "SOL", "ETH", "TRX").
            address: Destination wallet address.
            memo: Optional memo/tag (for XRP, etc.).

        Returns:
            Withdrawal details dict with status.
        """
        payload: dict = {
            "amount_usd": amount_usd,
            "currency": currency,
            "chain": chain,
            "address": address,
        }
        if memo is not None:
            payload["memo"] = memo
        return self._post("/withdraw", json=payload)

    def supported_withdrawals(self) -> list:
        """List supported withdrawal currency/chain combos."""
        data = self._get("/supported-withdrawals")
        return data.get("withdrawals", [])

    def withdrawals(self) -> list:
        """List withdrawal history."""
        data = self._get("/withdrawals")
        return data.get("withdrawals", [])

    def deposit_address(self, chain: str) -> dict:
        """Get a permanent deposit address for a chain.

        Args:
            chain: Blockchain network (e.g. "SOL", "ETH", "BTC").

        Returns:
            Dict with address, chain, memo, qr_image.
        """
        return self._post("/deposit/address", json={"chain": chain})

    def deposits(self) -> list:
        """List deposit history."""
        data = self._get("/deposits")
        return data.get("deposits", [])

    def verify(self, bet_id: int) -> VerifyResult:
        """Verify that a bet was provably fair.

        Args:
            bet_id: The ID of the bet to verify.

        Returns:
            A :class:`VerifyResult` indicating whether the bet is verified.
        """
        data = self._get(f"/verify/{bet_id}")
        return VerifyResult(
            bet_id=data["bet_id"],
            verified=data["verified"],
            roll=data.get("roll"),
            server_secret=data.get("server_secret"),
            client_secret=data.get("client_secret"),
            nonce=data.get("nonce"),
            server_seed_hash=data.get("server_seed_hash"),
        )

    # -- internal helpers --

    def _get(self, path: str) -> dict:
        return self._request("GET", path)

    def _post(self, path: str, json: dict = None) -> dict:
        return self._request("POST", path, json=json)

    def _request(self, method: str, path: str, **kwargs) -> dict:
        url = f"{self.base_url}{path}"
        last_exc: Optional[Exception] = None

        for attempt in range(_MAX_RETRIES):
            try:
                resp = self._session.request(method, url, **kwargs)
                if resp.status_code >= 500 and attempt < _MAX_RETRIES - 1:
                    time.sleep(_RETRY_BACKOFF * (attempt + 1))
                    continue
                _raise_for_status(resp)
                return resp.json()
            except (requests.ConnectionError, requests.Timeout) as exc:
                last_exc = exc
                if attempt < _MAX_RETRIES - 1:
                    time.sleep(_RETRY_BACKOFF * (attempt + 1))
                    continue
                raise RollhubError(f"Connection failed after {_MAX_RETRIES} retries: {exc}")

        raise last_exc  # type: ignore[misc]


def _raise_for_status(resp: requests.Response) -> None:
    """Raise a typed exception for HTTP error responses."""
    if resp.status_code < 400:
        return

    try:
        body = resp.json()
        message = body.get("error", body.get("message", resp.text))
    except ValueError:
        body = None
        message = resp.text or f"HTTP {resp.status_code}"

    code = resp.status_code
    if code == 401:
        raise AuthenticationError(message, status_code=code, response=body)
    if code == 402:
        raise InsufficientBalanceError(message, status_code=code, response=body)
    if code == 422:
        raise InvalidBetError(message, status_code=code, response=body)
    if code == 429:
        raise RateLimitError(message, status_code=code, response=body)
    if code >= 500:
        raise ServerError(message, status_code=code, response=body)
    raise RollhubError(message, status_code=code, response=body)
