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

import hashlib
import hmac
import secrets
import struct
import time
from typing import List, Optional

import requests

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

from .exceptions import (
    AuthenticationError,
    InsufficientBalanceError,
    InvalidBetError,
    RateLimitError,
    RollhubError,
    ServerError,
    VerificationError,
)
from .models import Balance, Bet, BetResult, CoinflipResult, Proof, Registration, RouletteResult, 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, referral_code: Optional[str] = None) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.referral_code = referral_code
        self._session = requests.Session()
        self._session.headers.update({
            "X-API-Key": self.api_key,
            "Content-Type": "application/json",
            "User-Agent": "rollhub-dice-python/0.5.0",
        })

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

        Args:
            wallet_address: The wallet address to register.
            base_url: API base URL.
            ref: Optional referral code of the referring agent.

        Returns:
            An authenticated DiceAgent instance.
        """
        url = f"{base_url.rstrip('/')}/register"
        payload: dict = {"wallet_address": wallet_address}
        if ref is not None:
            payload["ref"] = ref
        resp = requests.post(url, json=payload)
        _raise_for_status(resp)
        data = resp.json()
        return cls(api_key=data["api_key"], base_url=base_url, referral_code=data.get("referral_code"))

    def affiliate_stats(self) -> dict:
        """Get affiliate/referral statistics.

        Returns:
            Dict with referral_code, referral_link, total_referred,
            total_earned_cents, today_earned_cents, referred_agents.
        """
        return self._get("/affiliate/stats")

    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_seed", proof_data.get("server_secret", "")),
            client_secret=proof_data.get("client_seed", client_secret),
            nonce=proof_data.get("nonce", 0),
            server_seed_hash=proof_data.get("server_seed_hash", ""),
        )

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

        # Auto-verify if server_seed is available
        if proof.server_secret:
            verification = self._verify_bet({
                "server_seed": proof.server_secret,
                "server_seed_hash": proof.server_seed_hash,
                "client_seed": proof.client_secret,
                "nonce": proof.nonce,
                "roll": data["roll"],
            })
            result = BetResult(
                bet_id=result.bet_id,
                roll=result.roll,
                win=result.win,
                payout=result.payout,
                multiplier=result.multiplier,
                balance=result.balance,
                proof=proof,
                verification=verification,
            )

        return result

    def coinflip(
        self,
        side: str,
        amount: int,
        client_seed: Optional[str] = None,
    ) -> CoinflipResult:
        """Place a coinflip bet.

        Args:
            side: ``"heads"`` or ``"tails"``.
            amount: Wager in cents.
            client_seed: Optional client seed for provably fair verification.
                Generated automatically if not provided.

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

        data = self._post("/coinflip/bet", json={
            "side": side,
            "amount": amount,
            "client_seed": client_seed,
        })

        proof_data = data.get("proof", {})
        proof = Proof(
            server_secret=proof_data.get("server_seed", proof_data.get("server_secret", "")),
            client_secret=proof_data.get("client_seed", client_seed),
            nonce=proof_data.get("nonce", 0),
            server_seed_hash=proof_data.get("server_seed_hash", ""),
        ) if proof_data else None

        result = CoinflipResult(
            bet_id=data["bet_id"],
            side=data.get("side", side),
            result=data.get("result", ""),
            win=data["win"],
            payout=data["payout"],
            multiplier=data["multiplier"],
            balance=data["balance"],
            proof=proof,
        )

        # Auto-verify if server_seed is available
        if proof and proof.server_secret:
            verification = self._verify_bet({
                "server_seed": proof.server_secret,
                "server_seed_hash": proof.server_seed_hash,
                "client_seed": proof.client_secret,
                "nonce": proof.nonce,
                "roll": data.get("roll", 0),
            })
            result = CoinflipResult(
                bet_id=result.bet_id,
                side=result.side,
                result=result.result,
                win=result.win,
                payout=result.payout,
                multiplier=result.multiplier,
                balance=result.balance,
                proof=proof,
                verification=verification,
            )

        return result

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

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

        Returns:
            A :class:`VerifyResult` indicating whether the bet is verified.
        """
        data = self._get(f"/coinflip/verify/{bet_id}")
        proof = data.get("proof", {})

        server_secret = proof.get("server_secret", "")
        client_secret = proof.get("client_seed", "")
        nonce = proof.get("nonce", 0)
        server_seed_hash = proof.get("server_seed_hash", "")
        roll = data.get("roll", 0)

        verification = self._verify_bet({
            "server_seed": server_secret,
            "server_seed_hash": server_seed_hash,
            "client_seed": client_secret,
            "nonce": nonce,
            "roll": roll,
        })

        return VerifyResult(
            bet_id=data["bet_id"],
            verified=verification["verified"],
            roll=roll,
            server_secret=server_secret,
            client_secret=client_secret,
            nonce=nonce,
            server_seed_hash=server_seed_hash,
        )

    def roulette(
        self,
        bet_type: str,
        bet_value,
        amount: int,
        client_seed: Optional[str] = None,
    ) -> RouletteResult:
        """Place a roulette bet.

        Args:
            bet_type: Bet type (straight, split, street, corner, six_line,
                dozen, column, red, black, odd, even, high, low).
            bet_value: Bet value — depends on bet_type (number, list, or string).
            amount: Wager in cents.
            client_seed: Optional client seed for provably fair verification.

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

        data = self._post("/roulette/bet", json={
            "bet_type": bet_type,
            "bet_value": bet_value,
            "amount": amount,
            "client_seed": client_seed,
        })

        proof_data = data.get("proof", {})
        proof = Proof(
            server_secret=proof_data.get("server_seed", proof_data.get("server_secret", "")),
            client_secret=proof_data.get("client_seed", client_seed),
            nonce=proof_data.get("nonce", 0),
            server_seed_hash=proof_data.get("server_seed_hash", ""),
        ) if proof_data else None

        result_data = data.get("result", {})
        return RouletteResult(
            bet_id=data["id"],
            number=result_data.get("number", 0),
            color=result_data.get("color", ""),
            won=data["won"],
            payout=data["payout"],
            balance=data["balance"],
            bet_type=bet_type,
            bet_value=bet_value,
            proof=proof,
        )

    def roulette_verify(self, bet_id: int) -> VerifyResult:
        """Verify a roulette bet was provably fair."""
        data = self._get(f"/roulette/verify/{bet_id}")
        return VerifyResult(
            bet_id=data["id"],
            verified=data.get("verified", False),
            roll=data.get("proof", {}).get("raw_roll"),
            server_secret=data.get("proof", {}).get("server_seed"),
            client_secret=data.get("proof", {}).get("client_seed"),
            nonce=data.get("proof", {}).get("nonce"),
            server_seed_hash=data.get("proof", {}).get("server_seed_hash"),
        )

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

        Returns:
            A list of available game dicts.
        """
        data = self._get("/games")
        return data.get("games", data) if isinstance(data, dict) else data

    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 (client-side).

        Fetches the bet data from the server and independently recomputes
        the roll to verify it matches.

        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}")
        proof = data.get("proof", {})

        server_secret = proof.get("server_secret", "")
        client_secret = proof.get("client_seed", "")
        nonce = proof.get("nonce", 0)
        server_seed_hash = proof.get("server_seed_hash", "")
        roll = data.get("roll", 0)

        # Client-side verification
        verification = self._verify_bet({
            "server_seed": server_secret,
            "server_seed_hash": server_seed_hash,
            "client_seed": client_secret,
            "nonce": nonce,
            "roll": roll,
        })

        return VerifyResult(
            bet_id=data["bet_id"],
            verified=verification["verified"],
            roll=roll,
            server_secret=server_secret,
            client_secret=client_secret,
            nonce=nonce,
            server_seed_hash=server_seed_hash,
        )

    # -- provably fair verification --

    @staticmethod
    def _compute_roll(server_seed_hex: str, client_seed_hex: str, nonce: int) -> float:
        """Reproduce the server's provably fair roll.

        Algorithm (mirrors @blackjackfun/rng):
        1. SHA3-384(server_seed_bytes || client_seed_bytes || nonce_bytes) → 48 bytes
        2. key = first 32 bytes, iv = next 16 bytes
        3. AES-256-CTR encrypt 16 zero-bytes → 16 bytes ciphertext
        4. XOR all 4 little-endian uint32s from the ciphertext
        5. result = 0.5 + xor_value / 4294967295
        """
        server_bytes = bytes.fromhex(server_seed_hex)
        client_bytes = bytes.fromhex(client_seed_hex)
        nonce_bytes = struct.pack("<Q", nonce)  # 8-byte LE uint64

        h = hashlib.sha3_384()
        h.update(server_bytes)
        h.update(client_bytes)
        h.update(nonce_bytes)
        digest = h.digest()  # 48 bytes

        key = digest[:32]
        iv = digest[32:48]

        cipher = Cipher(algorithms.AES(key), modes.CTR(iv))
        encryptor = cipher.encryptor()
        ct = encryptor.update(b"\x00" * 16) + encryptor.finalize()

        xor_val = 0
        for i in range(0, 16, 4):
            xor_val ^= struct.unpack_from("<I", ct, i)[0]

        # Match JS behavior: XOR result is treated as signed 32-bit int
        if xor_val >= 0x80000000:
            xor_val -= 0x100000000

        return 0.5 + xor_val / 4294967295

    @staticmethod
    def _hash_server_seed(server_seed_hex: str) -> str:
        """SHA3-384 hash of server seed bytes, returned as hex."""
        server_bytes = bytes.fromhex(server_seed_hex)
        return hashlib.sha3_384(server_bytes).hexdigest()

    @staticmethod
    def _verify_bet(proof_data: dict) -> dict:
        """Verify a bet's provably fair proof.

        Returns a dict with verification details.
        Raises VerificationError if verification fails.
        """
        server_seed = proof_data["server_seed"]
        server_seed_hash = proof_data["server_seed_hash"]
        client_seed = proof_data["client_seed"]
        nonce = proof_data["nonce"]
        roll_reported = proof_data["roll"]

        # 1. Verify hash commitment
        computed_hash = DiceAgent._hash_server_seed(server_seed)
        hash_ok = computed_hash == server_seed_hash

        # 2. Recompute roll
        roll_recalculated = round(
            DiceAgent._compute_roll(server_seed, client_seed, nonce), 6
        )
        roll_ok = roll_recalculated == round(roll_reported, 6)

        verified = hash_ok and roll_ok

        result = {
            "verified": verified,
            "server_seed": server_seed,
            "server_seed_hash": server_seed_hash,
            "client_seed": client_seed,
            "nonce": nonce,
            "roll_reported": round(roll_reported, 6),
            "roll_recalculated": roll_recalculated,
            "hash_verified": hash_ok,
            "roll_verified": roll_ok,
        }

        if not verified:
            raise VerificationError(
                f"Bet verification failed: hash_ok={hash_ok}, roll_ok={roll_ok}",
            )

        return result

    # -- 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)
