"""
Main Reno API client implementation.
"""

import json
from typing import Dict, List, Optional, Union, Generator, Any

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from .exceptions import RenoError, RenoConnectionError, RenoTimeoutError
from .models import Completion, StreamChunk


class Reno:
    """
    Reno AI Client - handles chat completions with error-code based exceptions.
    
    Args:
        api_key: Your Reno API key
        base_url: Base URL for the API (default: http://127.0.0.1:8000/api/v1)
        timeout: Request timeout in seconds (default: 30)
        max_retries: Maximum number of retries for failed requests (default: 3)
    
    Example:
        >>> client = Reno(api_key="reno_sk_xxx")
        >>> response = client.ask("What is Python?")
        >>> print(response)
    """
    
    DEFAULT_BASE_URL = "http://127.0.0.1:8000/api/v1"
    DEFAULT_TIMEOUT = 30
    DEFAULT_MODEL = "gemma2:2b-instruct"
    
    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: int = DEFAULT_TIMEOUT,
        max_retries: int = 3
    ):
        if not api_key:
            raise ValueError("API key is required")
        
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
        self.session = self._create_session(max_retries)
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": "renoai-python/0.1.0"
        })

    def _create_session(self, max_retries: int) -> requests.Session:
        """Create a session with retry logic."""
        session = requests.Session()
        
        # Configure retry strategy
        retry_strategy = Retry(
            total=max_retries,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["POST"],
            backoff_factor=1
        )
        
        adapter = HTTPAdapter(max_retries=retry_strategy)
        session.mount("http://", adapter)
        session.mount("https://", adapter)
        
        return session

    def _request(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        """
        POST to /chat/completions/ and handle Reno error codes.
        
        Raises:
            RenoError: If the API returns an error
            RenoConnectionError: If connection fails
            RenoTimeoutError: If request times out
        """
        try:
            resp = self.session.post(
                f"{self.base_url}/chat/completions/",
                json=payload,
                timeout=self.timeout
            )
            
            # Check HTTP status code
            if resp.status_code >= 500:
                raise RenoError(
                    code=6001,
                    message=f"Server error: {resp.status_code}",
                    details=resp.text
                )
            elif resp.status_code >= 400:
                try:
                    error_data = resp.json()
                    if "error" in error_data:
                        err = error_data["error"]
                        raise RenoError(
                            code=err.get("code", 9001),
                            message=err.get("message", "Unknown error"),
                            details=err.get("details") or err.get("suggestion")
                        )
                except (json.JSONDecodeError, KeyError):
                    raise RenoError(
                        code=9001,
                        message=f"HTTP {resp.status_code}: {resp.text}",
                        details=None
                    )
            
            # Parse successful response
            try:
                data = resp.json()
            except json.JSONDecodeError as e:
                raise RenoError(
                    code=9001,
                    message="Invalid JSON response from server",
                    details=str(e)
                )
            
            # Check for error in response body (even with 200 status)
            if "error" in data:
                err = data["error"]
                raise RenoError(
                    code=err.get("code", 9001),
                    message=err.get("message", "Unknown error"),
                    details=err.get("details") or err.get("suggestion")
                )
            
            # Return the data field, or the whole response if no data field
            return data.get("data", data)
            
        except requests.exceptions.Timeout:
            raise RenoTimeoutError(
                f"Request timed out after {self.timeout} seconds"
            )
        except requests.exceptions.ConnectionError as e:
            raise RenoConnectionError(
                f"Failed to connect to {self.base_url}: {str(e)}"
            )
        except requests.exceptions.RequestException as e:
            raise RenoError(
                code=9001,
                message=f"Request failed: {str(e)}",
                details=None
            )

    def chat(
        self,
        messages: List[Dict[str, str]],
        model: str = DEFAULT_MODEL,
        temperature: float = 0.7,
        max_tokens: Optional[int] = None,
        stream: bool = False,
        **kwargs
    ) -> Union[Completion, Generator[StreamChunk, None, None]]:
        """
        Complete chat completion with full control.
        
        Args:
            messages: List of message dicts with 'role' and 'content'
            model: Model to use (default: gemma2:2b-instruct)
            temperature: Sampling temperature 0-2 (default: 0.7)
            max_tokens: Maximum tokens to generate
            stream: Enable streaming responses
            **kwargs: Additional parameters to pass to the API
        
        Returns:
            Completion object or generator of StreamChunk objects if streaming
        
        Example:
            >>> messages = [
            ...     {"role": "system", "content": "You are helpful."},
            ...     {"role": "user", "content": "Hello!"}
            ... ]
            >>> response = client.chat(messages)
            >>> print(response.text)
        """
        payload = {
            "model": model,
            "messages": messages,
            "temperature": temperature,
            "stream": stream,
            **kwargs
        }
        
        if max_tokens is not None:
            payload["max_tokens"] = max_tokens
        
        if stream:
            return self._stream(payload)
        
        return Completion(self._request(payload))

    def ask(
        self,
        prompt: str,
        system: Optional[str] = None,
        model: str = DEFAULT_MODEL,
        temperature: float = 0.7,
        max_tokens: Optional[int] = None
    ) -> str:
        """
        Simplified single-turn request - returns just the answer string.
        
        Args:
            prompt: The user's question or prompt
            system: Optional system message
            model: Model to use (default: gemma2:2b-instruct)
            temperature: Sampling temperature 0-2 (default: 0.7)
            max_tokens: Maximum tokens to generate
        
        Returns:
            The generated text response
        
        Example:
            >>> answer = client.ask("What is 2+2?")
            >>> print(answer)
            "2+2 equals 4."
        """
        messages = []
        if system:
            messages.append({"role": "system", "content": system})
        messages.append({"role": "user", "content": prompt})
        
        return self.chat(
            messages,
            model=model,
            temperature=temperature,
            max_tokens=max_tokens
        ).text

    def _stream(
        self, 
        payload: Dict[str, Any]
    ) -> Generator[StreamChunk, None, None]:
        """
        Handle streaming responses.
        
        Yields:
            StreamChunk objects containing delta content
        
        Raises:
            RenoError: If the stream contains an error
            RenoConnectionError: If connection fails
            RenoTimeoutError: If request times out
        """
        try:
            with self.session.post(
                f"{self.base_url}/chat/completions/",
                json=payload,
                stream=True,
                timeout=self.timeout
            ) as resp:
                # Check status code before processing stream
                if resp.status_code >= 400:
                    try:
                        error_data = resp.json()
                        if "error" in error_data:
                            err = error_data["error"]
                            raise RenoError(
                                code=err.get("code", 9001),
                                message=err.get("message", "Unknown error"),
                                details=err.get("details") or err.get("suggestion")
                            )
                    except json.JSONDecodeError:
                        pass
                    raise RenoError(
                        code=9001,
                        message=f"HTTP {resp.status_code}: {resp.text}",
                        details=None
                    )
                
                # Process stream
                for line in resp.iter_lines():
                    if not line:
                        continue
                    
                    line = line.decode('utf-8')
                    
                    if line.startswith("data: "):
                        data_str = line[6:]  # Remove "data: " prefix
                        
                        if data_str == "[DONE]":
                            break
                        
                        try:
                            chunk_data = json.loads(data_str)
                            
                            # Check for error in chunk
                            if "error" in chunk_data:
                                err = chunk_data["error"]
                                raise RenoError(
                                    code=err.get("code", 9001),
                                    message=err.get("message", "Unknown error"),
                                    details=err.get("details") or err.get("suggestion")
                                )
                            
                            yield StreamChunk(chunk_data)
                        except json.JSONDecodeError:
                            # Skip malformed chunks
                            continue
                            
        except requests.exceptions.Timeout:
            raise RenoTimeoutError(
                f"Stream request timed out after {self.timeout} seconds"
            )
        except requests.exceptions.ConnectionError as e:
            raise RenoConnectionError(
                f"Failed to connect to {self.base_url}: {str(e)}"
            )
        except requests.exceptions.RequestException as e:
            if not isinstance(e, (RenoError, RenoConnectionError, RenoTimeoutError)):
                raise RenoError(
                    code=9001,
                    message=f"Stream request failed: {str(e)}",
                    details=None
                )
            raise

    def close(self):
        """Close the HTTP session."""
        self.session.close()

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, *args):
        """Context manager exit."""
        self.close()

    def __repr__(self) -> str:
        return f"Reno(base_url='{self.base_url}')"
