"""Client principal du SDK STRIGIVAL."""

import time
import hashlib
import hmac
from datetime import datetime
from typing import Optional, List, Dict, Any

import httpx
from eth_account import Account
from eth_account.messages import encode_defunct

from .types import (
    Quote,
    Trade,
    VaultStatus,
    KPIs,
    Token,
    TradeHistory,
    Protocol,
    TradeAction,
)
from .exceptions import (
    StrigivalError,
    AuthenticationError,
    QuoteError,
    TradeError,
    RateLimitError,
    NetworkError,
    ValidationError,
)
from .security import (
    get_validator,
    get_divergence_detector,
    get_race_detector,
    CalldataValidator,
    ProtocolDivergenceDetector,
    RaceConditionDetector,
)


class StrigivalClient:
    """
    Client pour interagir avec l'API STRIGIVAL.
    
    Permet aux traders algorithmiques d'exécuter des trades,
    récupérer des quotes, et accéder aux analytics.
    
    Attributes:
        api_url: URL de base de l'API
        address: Adresse Ethereum du trader
        
    Example:
        >>> client = StrigivalClient(
        ...     api_url="https://api.strigival.io",
        ...     private_key="0x..."
        ... )
        >>> quote = client.get_quote(
        ...     vault_address="0x...",
        ...     token_in="USDC",
        ...     token_out="WETH",
        ...     amount=1000_000000  # 1000 USDC
        ... )
        >>> trade = client.execute_trade(quote)
    """
    
    DEFAULT_TIMEOUT = 30.0
    DEFAULT_SLIPPAGE_BPS = 50  # 0.5%
    
    def __init__(
        self,
        api_url: str,
        private_key: Optional[str] = None,
        address: Optional[str] = None,
        timeout: float = DEFAULT_TIMEOUT,
    ):
        """
        Initialise le client STRIGIVAL.
        
        Args:
            api_url: URL de base de l'API (ex: https://api.strigival.io)
            private_key: Clé privée Ethereum pour signer les transactions
            address: Adresse Ethereum (optionnel si private_key fournie)
            timeout: Timeout pour les requêtes HTTP
        """
        self.api_url = api_url.rstrip("/")
        self.timeout = timeout
        self._session_token: Optional[str] = None
        self._session_expires: Optional[datetime] = None
        
        # Setup account
        if private_key:
            self._account = Account.from_key(private_key)
            self.address = self._account.address
        elif address:
            self._account = None
            self.address = address
        else:
            raise ValueError("Either private_key or address must be provided")
        
        # HTTP client
        self._client = httpx.Client(
            base_url=self.api_url,
            timeout=timeout,
            headers={
                "Content-Type": "application/json",
                "User-Agent": f"strigival-python-sdk/1.0.0",
            },
        )
        
        # SECURITY: Initialize security modules
        self._validator = get_validator(strict_mode=True)
        self._divergence_detector = get_divergence_detector()
        self._race_detector = get_race_detector()
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
    
    def close(self):
        """Ferme le client HTTP."""
        self._client.close()
    
    # =========================================================================
    # AUTHENTICATION
    # =========================================================================
    
    def authenticate(self) -> str:
        """
        Authentifie le client avec l'API.
        
        Returns:
            Token de session
            
        Raises:
            AuthenticationError: Si l'authentification échoue
        """
        if not self._account:
            raise AuthenticationError("Private key required for authentication")
        
        # Get nonce
        try:
            resp = self._request("GET", f"/api/v1/auth/nonce/{self.address}")
            nonce = resp["nonce"]
            message = resp["message"]
        except StrigivalError:
            raise
        except Exception as e:
            raise AuthenticationError(f"Failed to get nonce: {e}")
        
        # Sign message
        message_hash = encode_defunct(text=message)
        signed = self._account.sign_message(message_hash)
        signature = signed.signature.hex()
        
        # Verify signature
        try:
            resp = self._request("POST", "/api/v1/auth/verify", json={
                "address": self.address,
                "message": message,
                "signature": f"0x{signature}",
            })
            self._session_token = resp["token"]
            self._session_expires = datetime.fromisoformat(
                resp["expires_at"].replace("Z", "+00:00")
            )
            return self._session_token
        except StrigivalError:
            raise
        except Exception as e:
            raise AuthenticationError(f"Failed to verify signature: {e}")
    
    def _ensure_authenticated(self):
        """S'assure que le client est authentifié."""
        if not self._session_token:
            self.authenticate()
        elif self._session_expires and datetime.utcnow() > self._session_expires:
            self.authenticate()
    
    # =========================================================================
    # QUOTES
    # =========================================================================
    
    def get_quote(
        self,
        vault_address: str,
        token_in: str,
        token_out: str,
        amount: int,
        protocol: Protocol = Protocol.UNISWAP_V3,
        action: TradeAction = TradeAction.SWAP,
        slippage_bps: int = DEFAULT_SLIPPAGE_BPS,
    ) -> Quote:
        """
        Récupère un quote pour un trade.
        
        Args:
            vault_address: Adresse du vault
            token_in: Adresse ou symbole du token d'entrée
            token_out: Adresse ou symbole du token de sortie
            amount: Montant en wei
            protocol: Protocole DeFi à utiliser
            action: Action de trading
            slippage_bps: Slippage maximum en basis points
            
        Returns:
            Quote avec prix et route
            
        Raises:
            QuoteError: Si le quote échoue
        """
        self._ensure_authenticated()
        
        try:
            resp = self._request("POST", "/api/v1/quote", json={
                "vault_address": vault_address,
                "token_in": token_in,
                "token_out": token_out,
                "amount": str(amount),
                "protocol": protocol.value,
                "action": action.value,
                "slippage_bps": slippage_bps,
            })
            return Quote.from_dict(resp)
        except StrigivalError:
            raise
        except Exception as e:
            raise QuoteError(f"Failed to get quote: {e}")
    
    def get_protocols(self) -> List[Dict[str, Any]]:
        """
        Liste les protocoles supportés.
        
        Returns:
            Liste des protocoles avec leurs actions
        """
        resp = self._request("GET", "/api/v1/quote/protocols")
        return resp["protocols"]
    
    # =========================================================================
    # CALLDATA
    # =========================================================================
    
    def build_calldata(
        self,
        vault_address: str,
        protocol: Protocol,
        action: TradeAction,
        params: Dict[str, Any],
    ) -> Dict[str, Any]:
        """
        Construit le calldata pour un trade.
        
        Args:
            vault_address: Adresse du vault
            protocol: Protocole DeFi
            action: Action de trading
            params: Paramètres spécifiques au protocole
            
        Returns:
            Dict avec target, calldata, value, estimated_gas
            
        Raises:
            ValidationError: Si les paramètres sont invalides ou malveillants
        """
        self._ensure_authenticated()
        
        # SECURITY: Validate all parameters before sending to API
        self._validator.validate_params(protocol.value, action.value, params)
        
        # SECURITY: Check for race conditions
        conflict_id = self._race_detector.check_conflict(
            vault_address, action.value, params
        )
        if conflict_id:
            raise ValidationError(
                f"Potential race condition with pending order: {conflict_id}",
                field="action"
            )
        
        resp = self._request("POST", "/api/v1/calldata/build", json={
            "vault_address": vault_address,
            "protocol": protocol.value,
            "action": action.value,
            "params": params,
        })
        return resp
    
    def decode_calldata(self, target: str, calldata: str) -> Dict[str, Any]:
        """
        Décode un calldata pour inspection.
        
        Args:
            target: Adresse du contrat cible
            calldata: Calldata encodé (hex)
            
        Returns:
            Dict avec protocol, function, params
        """
        resp = self._request("POST", "/api/v1/calldata/decode", json={
            "target": target,
            "calldata": calldata,
        })
        return resp
    
    # =========================================================================
    # VAULT
    # =========================================================================
    
    def get_vault_status(self, vault_address: str) -> VaultStatus:
        """
        Récupère le statut d'un vault.
        
        Args:
            vault_address: Adresse du vault
            
        Returns:
            VaultStatus avec état et permissions
        """
        self._ensure_authenticated()
        
        resp = self._request("GET", f"/api/v1/vault/{vault_address}/status")
        return VaultStatus.from_dict(resp)
    
    def get_authorized_tokens(self, vault_address: str) -> List[Token]:
        """
        Liste les tokens autorisés pour un vault.
        
        Args:
            vault_address: Adresse du vault
            
        Returns:
            Liste des tokens autorisés
        """
        self._ensure_authenticated()
        
        resp = self._request("GET", f"/api/v1/vault/{vault_address}/tokens")
        return [Token.from_dict(t) for t in resp["tokens"]]
    
    # =========================================================================
    # TRADES
    # =========================================================================
    
    def execute_trade(
        self,
        quote: Quote,
        vault_address: Optional[str] = None,
    ) -> Trade:
        """
        Exécute un trade basé sur un quote.
        
        Args:
            quote: Quote obtenu via get_quote()
            vault_address: Adresse du vault (optionnel si dans quote)
            
        Returns:
            Trade avec résultat de l'exécution
            
        Raises:
            TradeError: Si l'exécution échoue
        """
        if quote.is_expired():
            raise TradeError("Quote has expired")
        
        self._ensure_authenticated()
        
        # Build calldata from quote
        calldata = self.build_calldata(
            vault_address=vault_address or "",
            protocol=Protocol(quote.protocol),
            action=TradeAction.SWAP,
            params={
                "amount_in": quote.amount_in,
                "amount_out_min": quote.amount_out_min,
                "route": quote.route,
            },
        )
        
        try:
            resp = self._request("POST", "/api/v1/trade/execute", json={
                "vault_address": vault_address,
                "target": calldata["target"],
                "calldata": calldata["calldata"],
                "value": calldata.get("value", "0"),
                "quote_id": quote.quote_id,
            })
            return Trade.from_dict(resp)
        except StrigivalError:
            raise
        except Exception as e:
            raise TradeError(f"Trade execution failed: {e}")
    
    def get_trade_history(
        self,
        vault_address: str,
        limit: int = 50,
        offset: int = 0,
    ) -> List[TradeHistory]:
        """
        Récupère l'historique des trades.
        
        Args:
            vault_address: Adresse du vault
            limit: Nombre maximum de trades
            offset: Offset pour pagination
            
        Returns:
            Liste des trades
        """
        self._ensure_authenticated()
        
        resp = self._request(
            "GET",
            f"/api/v1/vault/{vault_address}/trades",
            params={"limit": limit, "offset": offset},
        )
        return [TradeHistory.from_dict(t) for t in resp["trades"]]
    
    def get_trade(self, vault_address: str, tx_hash: str) -> TradeHistory:
        """
        Récupère les détails d'un trade.
        
        Args:
            vault_address: Adresse du vault
            tx_hash: Hash de la transaction
            
        Returns:
            Détails du trade
        """
        self._ensure_authenticated()
        
        resp = self._request("GET", f"/api/v1/vault/{vault_address}/trades/{tx_hash}")
        return TradeHistory.from_dict(resp)
    
    # =========================================================================
    # ANALYTICS
    # =========================================================================
    
    def get_kpis(self, vault_address: str) -> KPIs:
        """
        Récupère les KPIs d'un vault.
        
        Args:
            vault_address: Adresse du vault
            
        Returns:
            KPIs institutionnelles
        """
        self._ensure_authenticated()
        
        resp = self._request("GET", f"/api/v1/vault/{vault_address}/analytics")
        return KPIs.from_dict(resp["kpis"])
    
    def get_sharpe_ratio(
        self,
        vault_address: str,
        period_days: int = 365,
    ) -> float:
        """
        Récupère le Sharpe Ratio d'un vault.
        
        Args:
            vault_address: Adresse du vault
            period_days: Période de calcul
            
        Returns:
            Sharpe Ratio
        """
        resp = self._request(
            "GET",
            f"/api/v1/strategies/0/kpis/sharpe",  # TODO: Use vault_address lookup
            params={"period_days": period_days},
        )
        return resp["sharpe_ratio"]
    
    # =========================================================================
    # STRATEGIES (Public)
    # =========================================================================
    
    def list_strategies(
        self,
        sort_by: str = "tvl",
        limit: int = 20,
        offset: int = 0,
    ) -> List[Dict[str, Any]]:
        """
        Liste les stratégies disponibles.
        
        Args:
            sort_by: Critère de tri (tvl, sharpe, return)
            limit: Nombre maximum
            offset: Offset pour pagination
            
        Returns:
            Liste des stratégies
        """
        resp = self._request(
            "GET",
            "/api/v1/strategies",
            params={"sort_by": sort_by, "limit": limit, "offset": offset},
        )
        return resp["strategies"]
    
    def get_strategy(self, strategy_id: int) -> Dict[str, Any]:
        """
        Récupère les détails d'une stratégie.
        
        Args:
            strategy_id: ID de la stratégie
            
        Returns:
            Détails de la stratégie
        """
        return self._request("GET", f"/api/v1/strategies/{strategy_id}")
    
    # =========================================================================
    # INTERNAL
    # =========================================================================
    
    def _request(
        self,
        method: str,
        path: str,
        json: Dict[str, Any] = None,
        params: Dict[str, Any] = None,
    ) -> Dict[str, Any]:
        """Effectue une requête HTTP."""
        headers = {}
        if self._session_token:
            headers["Authorization"] = f"Bearer {self._session_token}"
        
        try:
            response = self._client.request(
                method=method,
                url=path,
                json=json,
                params=params,
                headers=headers,
            )
        except httpx.TimeoutException:
            raise NetworkError("Request timed out")
        except httpx.RequestError as e:
            raise NetworkError(f"Request failed: {e}")
        
        # Handle rate limiting
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            raise RateLimitError(
                retry_after=int(retry_after) if retry_after else None
            )
        
        # Handle errors
        if response.status_code >= 400:
            try:
                error_data = response.json()
                detail = error_data.get("detail", "Unknown error")
            except Exception:
                detail = response.text or "Unknown error"
            
            if response.status_code == 401:
                raise AuthenticationError(detail)
            elif response.status_code == 400:
                raise ValidationError(detail)
            else:
                raise StrigivalError(detail, code=f"HTTP_{response.status_code}")
        
        return response.json()
