"""
LitcoinAgent — High-level interface for the LITCOIN protocol.

    from litcoin import Agent

    agent = Agent(bankr_key="bk_YOUR_KEY")
    agent.mine(rounds=5)
    agent.claim()
"""

import uuid
import time
import logging
import threading
from dataclasses import dataclass, field

from litcoin.api import CoordinatorAPI, APIError
from litcoin.auth import BankrAuth, AuthSession
from litcoin.solver import solve

log = logging.getLogger("litcoin")


@dataclass
class MiningStats:
    """Tracks mining session statistics."""
    rounds: int = 0
    passes: int = 0
    fails: int = 0
    total_earned: int = 0
    start_time: float = field(default_factory=time.time)

    @property
    def elapsed(self):
        return time.time() - self.start_time

    @property
    def pass_rate(self):
        return self.passes / max(1, self.rounds) * 100

    def summary(self):
        return {
            "rounds": self.rounds,
            "passes": self.passes,
            "fails": self.fails,
            "passRate": round(self.pass_rate, 1),
            "totalEarned": self.total_earned,
            "elapsedSeconds": round(self.elapsed, 1),
        }


class Agent:
    """
    Full-protocol LITCOIN agent.

    Handles mining, claiming, staking reads, compute requests, and stats —
    all through a single clean interface.

    Args:
        bankr_key:       Bankr API key (bk_...). Required for mining/claiming.
        coordinator_url: Coordinator base URL (default: https://api.litcoiin.xyz)
        name:            Human-readable agent name (for logging/demos)
        log_level:       Logging level (default: INFO). Set to WARNING for quiet mode.
    """

    def __init__(self, bankr_key, coordinator_url=None, name=None, log_level=logging.INFO):
        self.name = name or "Agent"
        self.bankr = BankrAuth(bankr_key)
        self.api = CoordinatorAPI(coordinator_url)
        self.auth = AuthSession(self.bankr, self.api)
        self.stats = MiningStats()
        self._mining = False
        self._stop_event = threading.Event()

        logging.basicConfig(
            level=log_level,
            format=f"  [%(asctime)s] [{self.name}] %(message)s",
            datefmt="%H:%M:%S",
        )

    @property
    def wallet(self):
        """The agent's wallet address (resolved from Bankr)."""
        return self.bankr.wallet

    # ─── Mining ───────────────────────────────────────────────────────────────

    def mine(self, rounds=0, max_failures=5, delay=2, on_solve=None):
        """
        Start mining. Blocks until complete.

        Args:
            rounds:       Number of rounds (0 = mine forever)
            max_failures: Stop after N consecutive failures
            delay:        Seconds between rounds
            on_solve:     Callback(result_dict) called after each solve attempt

        Returns:
            MiningStats for this session
        """
        self._mining = True
        self._stop_event.clear()
        consecutive_fails = 0
        infra_fails = 0

        log.info(f"Mining started | wallet={self.wallet[:10]}...")

        while self._mining and not self._stop_event.is_set():
            if rounds > 0 and self.stats.rounds >= rounds:
                log.info(f"Completed {rounds} rounds")
                break
            if consecutive_fails >= max_failures:
                log.error(f"{max_failures} consecutive failures — stopping")
                break

            result = self._mine_one_round()

            if result is None:
                # Infrastructure failure
                infra_fails += 1
                wait = min(30 * infra_fails, 300)
                log.warning(f"Infra failure, retry in {wait}s")
                self._stop_event.wait(wait)
                continue

            infra_fails = 0
            self.stats.rounds += 1

            if result.get("pass"):
                consecutive_fails = 0
                self.stats.passes += 1
                reward = result.get("reward", 0)
                self.stats.total_earned += reward
                log.info(f"✓ PASS +{reward:,} LITCOIN | session: {self.stats.total_earned:,}")
            else:
                consecutive_fails += 1
                self.stats.fails += 1
                failed = result.get("failedConstraintIndices", [])
                log.warning(f"✗ FAIL constraints={failed}")

            if on_solve:
                on_solve({**result, "stats": self.stats.summary()})

            self._stop_event.wait(delay)

        self._mining = False
        log.info(f"Mining stopped | {self.stats.passes}/{self.stats.rounds} passed | {self.stats.total_earned:,} LITCOIN")
        return self.stats

    def mine_async(self, **kwargs):
        """Start mining in a background thread. Returns the thread."""
        t = threading.Thread(target=self.mine, kwargs=kwargs, daemon=True)
        t.start()
        return t

    def stop(self):
        """Signal the mining loop to stop."""
        self._mining = False
        self._stop_event.set()

    def _mine_one_round(self):
        """Execute a single mine-solve-submit cycle. Returns result dict or None."""
        nonce = uuid.uuid4().hex[:32]
        try:
            # Get challenge
            challenge = self.api.get_challenge(nonce, self.auth.token)

            # Solve
            artifact = solve(challenge)
            if not artifact:
                return {"pass": False, "error": "solve_failed"}

            # Submit
            result = self.api.submit_solution(
                challenge["challengeId"], artifact, nonce, self.auth.token
            )
            return result

        except APIError as e:
            if e.status == 401:
                # Auth expired, refresh and retry
                try:
                    self.auth.refresh()
                    challenge = self.api.get_challenge(nonce, self.auth.token)
                    artifact = solve(challenge)
                    if not artifact:
                        return {"pass": False, "error": "solve_failed"}
                    return self.api.submit_solution(
                        challenge["challengeId"], artifact, nonce, self.auth.token
                    )
                except Exception:
                    return None
            elif e.status == 403:
                log.warning(f"Forbidden: {e.message}")
                return None
            elif e.status >= 500:
                return None
            else:
                log.error(f"API error: {e}")
                return {"pass": False, "error": str(e)}
        except Exception as e:
            log.error(f"Unexpected error: {e}")
            return None

    # ─── Claims ───────────────────────────────────────────────────────────────

    def status(self):
        """Get this agent's claim status (earned, claimed, claimable)."""
        return self.api.claims_status(self.wallet)

    def claim(self):
        """
        Claim accumulated mining rewards via Bankr.

        Returns dict with txHash on success, or None.
        """
        log.info("Claiming rewards...")

        # Use coordinator-assisted Bankr claim
        try:
            result = self.api.claims_bankr(self.wallet)
            if result.get("txHash"):
                log.info(f"✓ Claimed! tx: {result['txHash']}")
            return result
        except APIError as e:
            # Fallback to manual claim
            log.warning(f"Bankr claim route failed ({e}), trying manual...")

        status = self.api.claims_status(self.wallet)
        if not status.get("claimableWei") or status["claimableWei"] == "0":
            log.info("Nothing to claim")
            return None

        # Sign claim
        sig_data = self.api.claims_sign(self.wallet)
        total_earned_wei = sig_data.get("totalEarnedWei")
        signature = sig_data.get("signature")

        if not total_earned_wei or not signature:
            log.error("Could not get claim signature")
            return None

        # Build calldata and submit via Bankr
        calldata = self._encode_claim(total_earned_wei, signature)
        result = self.bankr.submit_tx(
            "0xF703DcF2E88C0673F776870fdb12A453927C6A5e",
            calldata
        )
        if result.get("hash"):
            log.info(f"✓ Claim tx: {result['hash']}")
        return result

    def _encode_claim(self, total_earned_wei, signature_hex):
        """ABI-encode claim(uint256, bytes) calldata."""
        selector = "38926b6d"
        sig_bytes = bytes.fromhex(signature_hex.replace("0x", ""))
        total_earned = int(total_earned_wei)

        total_earned_hex = format(total_earned, '064x')
        offset_hex = format(64, '064x')
        sig_len_hex = format(len(sig_bytes), '064x')
        sig_hex = sig_bytes.hex()
        pad_len = (32 - len(sig_bytes) % 32) % 32
        sig_padded = sig_hex + "00" * pad_len

        return "0x" + selector + total_earned_hex + offset_hex + sig_len_hex + sig_padded

    # ─── Stats ────────────────────────────────────────────────────────────────

    def network_stats(self):
        """Get global network statistics."""
        return self.api.network_stats()

    def leaderboard(self, limit=20):
        """Get mining leaderboard."""
        return self.api.leaderboard(limit)

    def health(self):
        """Check coordinator health."""
        return self.api.health()

    # ─── Staking ──────────────────────────────────────────────────────────────

    def boost(self):
        """Get this agent's staking tier and mining boost."""
        return self.api.get_boost(self.wallet)

    def staking_yield(self):
        """Get this agent's staking yield info."""
        return self.api.staking_yield(self.wallet)

    def register_staker(self):
        """Register this wallet as a known staker for yield distribution."""
        return self.api.register_staker(self.wallet)

    # ─── Compute ──────────────────────────────────────────────────────────────

    def compute(self, prompt, model=None, max_tokens=4096):
        """
        Submit a compute request to the relay network.
        Requires LITCREDIT balance for paid requests.

        Args:
            prompt:     The inference prompt
            model:      Model to use (optional, uses relay's default)
            max_tokens: Maximum output tokens

        Returns:
            Response dict with 'response', 'tokensUsed', etc.
        """
        return self.api.compute_request(prompt, model=model,
                                        max_tokens=max_tokens, wallet=self.wallet)

    def compute_status(self):
        """Get compute network health and stats."""
        return {
            "health": self.api.compute_health(),
            "providers": self.api.compute_providers(),
            "stats": self.api.compute_stats(),
        }
