"""
LockStock Core Client
---------------------
Thin HTTP client for LockStock API interactions with idempotency and retry.

NO LOCAL CRYPTO. All verification is done server-side.
This client just makes HTTP calls to the Rust server.

Features:
- Automatic idempotency keys for safe retries
- Exponential backoff on transient failures
- Expected sequence tracking for staleness detection
- Lazy sync on first stamp
"""

import asyncio
import uuid
from typing import Optional, Dict, Any
import httpx

from .types import VerifyResult, VerifyStatus, AgentCard, map_tool_to_capability
from .fingerprint import generate_fingerprint


class LockStockClient:
    """
    Thin HTTP client for LockStock API with idempotency and retry.

    NO LOCAL CRYPTO. NO STATE TRACKING (except sequence for diagnostics).
    All verification is server-side. This client calls /v1/stamp for authorization.

    Idempotency:
        Every stamp() call generates a unique idempotency key. If the network
        fails after the server processes but before the client receives the
        response, retrying with the same key returns the cached response
        without double-advancing the chain.
    """

    DEFAULT_ENDPOINT = "https://lockstock-api-i9kp.onrender.com"

    # Retry configuration
    MAX_RETRIES = 3
    RETRY_DELAYS = [0.5, 1.0, 2.0]  # Exponential backoff in seconds
    RETRYABLE_STATUS_CODES = {502, 503, 504}  # Gateway errors

    def __init__(
        self,
        agent_id: str,
        api_key: Optional[str] = None,
        endpoint: str = DEFAULT_ENDPOINT,
        timeout: float = 30.0
    ):
        """
        Initialize LockStock client.

        Args:
            agent_id: The agent's unique identifier
            api_key: API key for authentication (Bearer token)
            endpoint: LockStock API endpoint
            timeout: Request timeout in seconds
        """
        self.agent_id = agent_id
        self.api_key = api_key
        self.endpoint = endpoint.rstrip("/")
        self.timeout = timeout
        self._client = httpx.AsyncClient(timeout=timeout)

        # Track last known sequence for expected_sequence (diagnostic)
        self._last_known_sequence: Optional[int] = None

    async def activate(self, genesis_token: str) -> Dict[str, Any]:
        """One-time activation via /v1/agents/claim with auto-generated fingerprint.

        Replaces daemon bind step for SaaS thin clients. Call once per agent
        after provisioning on dashboard.

        Args:
            genesis_token: One-time token from dashboard provisioning

        Returns:
            Dict with agent_id and genesis block (root_hash, state_matrix, etc.)

        Raises:
            httpx.HTTPStatusError: If activation fails
        """
        fingerprint = generate_fingerprint()
        response = await self._client.post(
            f"{self.endpoint}/v1/agents/claim",
            json={
                "genesis_token": genesis_token,
                "hardware_fingerprint": fingerprint,
            },
        )
        response.raise_for_status()
        return response.json()

    async def stamp(self, task: str) -> VerifyResult:
        """
        Stamp a task with automatic retry and idempotency.

        Each call generates a unique idempotency key. If the request
        succeeds server-side but the response is lost, retries will
        return the cached response without double-advancing the chain.

        Args:
            task: The task type (e.g., "chatcompletion", "network", "heartbeat")

        Returns:
            VerifyResult with authorization status and chain state
        """
        # Lazy sync on first call to initialize sequence tracking
        if self._last_known_sequence is None:
            await self.sync()

        # Generate idempotency key for this logical request
        idempotency_key = str(uuid.uuid4())

        # Build request payload
        payload: Dict[str, Any] = {
            "agent_id": self.agent_id,
            "task": task.lower(),
            "idempotency_key": idempotency_key,
        }

        # Include expected_sequence if we know it (diagnostic)
        if self._last_known_sequence is not None:
            payload["expected_sequence"] = self._last_known_sequence + 1

        last_error: Optional[Exception] = None

        for attempt in range(self.MAX_RETRIES + 1):
            try:
                response = await self._client.post(
                    f"{self.endpoint}/v1/stamp",
                    json=payload,
                    headers=self._headers(),
                )

                data = response.json() if response.content else {}

                # Success (200) - includes both fresh and idempotency-cached responses
                if response.status_code == 200:
                    # Update sequence tracking
                    if "sequence" in data:
                        self._last_known_sequence = data["sequence"]

                    if data.get("authorized"):
                        return VerifyResult(
                            status=VerifyStatus.ACCEPTED,
                            state_hash=data.get("state_hash"),
                            sequence=data.get("sequence"),
                            server_timestamp=data.get("timestamp"),
                        )
                    else:
                        # 200 but not authorized (shouldn't happen, but handle gracefully)
                        return VerifyResult(
                            status=VerifyStatus.REJECTED,
                            reason=data.get("reason", "Authorized=false in 200 response"),
                        )

                # Accepted (202) - request is still processing, retry
                if response.status_code == 202:
                    retry_after = data.get("retry_after_ms", 1000) / 1000.0
                    if attempt < self.MAX_RETRIES:
                        await asyncio.sleep(retry_after)
                        # Keep same idempotency key - we're waiting for the same request
                        continue

                # Conflict (409) - sequence mismatch or concurrent request
                if response.status_code == 409:
                    # Update our sequence knowledge from server
                    if "current_sequence" in data:
                        self._last_known_sequence = data["current_sequence"]

                    # Check if this is a concurrent conflict (retry with new key)
                    # or a sequence mismatch (need to resync)
                    reason = data.get("reason", "")
                    if "Concurrent" in reason:
                        # Concurrent request conflict - generate new idempotency key and retry
                        idempotency_key = str(uuid.uuid4())
                        payload["idempotency_key"] = idempotency_key
                        if self._last_known_sequence is not None:
                            payload["expected_sequence"] = self._last_known_sequence + 1
                        # Retry immediately (no backoff for conflicts)
                        continue
                    else:
                        # Sequence mismatch - return error so caller can handle
                        return VerifyResult(
                            status=VerifyStatus.REJECTED,
                            reason=data.get("reason", f"Conflict: {response.status_code}"),
                            sequence=data.get("current_sequence"),
                        )

                # Retryable server errors (502, 503, 504)
                if response.status_code in self.RETRYABLE_STATUS_CODES:
                    if attempt < self.MAX_RETRIES:
                        await asyncio.sleep(self.RETRY_DELAYS[attempt])
                        # Keep same idempotency key - server might have processed
                        continue

                # Non-retryable error
                return VerifyResult(
                    status=VerifyStatus.REJECTED,
                    reason=data.get("reason", f"HTTP {response.status_code}"),
                )

            except httpx.TimeoutException as e:
                last_error = e
                if attempt < self.MAX_RETRIES:
                    await asyncio.sleep(self.RETRY_DELAYS[attempt])
                    # Keep same idempotency key - request might have succeeded
                    continue

            except httpx.HTTPError as e:
                last_error = e
                if attempt < self.MAX_RETRIES:
                    await asyncio.sleep(self.RETRY_DELAYS[attempt])
                    continue

            except Exception as e:
                last_error = e
                if attempt < self.MAX_RETRIES:
                    await asyncio.sleep(self.RETRY_DELAYS[attempt])
                    continue

        # All retries exhausted
        return VerifyResult(
            status=VerifyStatus.REJECTED,
            reason=f"Request failed after {self.MAX_RETRIES + 1} attempts: {last_error}"
        )

    async def stamp_tool(self, tool_name: str) -> VerifyResult:
        """
        Stamp authorization for a specific tool call.

        Maps tool name to task type and calls stamp().

        Args:
            tool_name: Name of the tool being called

        Returns:
            VerifyResult with authorization status
        """
        task = map_tool_to_capability(tool_name)
        return await self.stamp(task=task)

    async def get_agent_card(self) -> Optional[AgentCard]:
        """
        Retrieve the agent's card from the server.

        Returns:
            AgentCard if found, None otherwise
        """
        try:
            response = await self._client.get(
                f"{self.endpoint}/api/guard/agents/{self.agent_id}",
                headers=self._headers()
            )

            if response.status_code == 200:
                data = response.json()
                return AgentCard(
                    agent_id=data.get("agent_id", self.agent_id),
                    display_name=data.get("display_name", ""),
                    capabilities=data.get("capabilities", []),
                    status=data.get("status", "unknown"),
                    fingerprint=data.get("fingerprint"),
                    owner_id=data.get("owner_id"),
                    created_at=data.get("created_at")
                )
            return None

        except Exception:
            return None

    async def get_chain_state(self) -> Optional[Dict[str, Any]]:
        """
        Get current chain state from the server.

        Returns:
            Dict with last_hash, last_sequence, state_matrix, or None on error
        """
        try:
            response = await self._client.get(
                f"{self.endpoint}/api/guard/agent/{self.agent_id}/state",
                headers=self._headers()
            )

            if response.status_code == 200:
                data = response.json()
                # Update sequence tracking
                if "last_sequence" in data:
                    self._last_known_sequence = data["last_sequence"]
                return data
            return None

        except Exception:
            return None

    async def sync(self) -> bool:
        """
        Sync local sequence tracking with server state.

        Call this on startup or after errors to ensure
        expected_sequence is accurate.

        Returns:
            True if sync succeeded, False otherwise
        """
        state = await self.get_chain_state()
        return state is not None

    def _headers(self) -> Dict[str, str]:
        """Build request headers."""
        headers = {"Content-Type": "application/json"}
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
        return headers

    async def close(self):
        """Close the HTTP client."""
        await self._client.aclose()

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()
