from google import genai
from .memory import Memory
from .stats import Stats
from ._version import __version__
from .version_checker import check_latest_version
from .personas import PERSONAS
from .exceptions import InvalidAPIKeyException, ChatException, PersonaException
from .validators import (
    validate_api_key,
    validate_filepath,
    validate_max_output_tokens,
    validate_message,
    validate_prompt,
    validate_temperature,
    validate_max_messages,
    validate_language,
)


class Dracula:
    """
    The main class for interacting with Google Gemini using the Dracula library.

    Dracula provides a simple and elegant interface for chatting with Gemini,
    managing conversation memory, controlling AI behavior, and tracking usage stats.

    Args:
        api_key (str): Your Google Gemini API key.
        model (str): The Gemini model to use. Defaults to 'gemini-2.0-flash'.
        max_messages (int): Maximum number of messages to keep in memory. Defaults to 10.
        prompt (str): System prompt that defines the AI's personality. Defaults to 'You are a helpful assistant.'
        temperature (float): Controls response creativity between 0.0 and 2.0. Defaults to 1.0.
        max_output_tokens (int): Maximum length of responses in tokens. Defaults to 8192.
        stats_filepath (str): Path to save usage stats. Defaults to 'dracula_stats.json'.
        language (str): Language for responses. Defaults to 'English'.

    Example:
        >>> from dracula import Dracula
        >>> ai = Dracula(api_key="your-api-key", language="Turkish")
        >>> print(ai.chat("Hello!"))
    """

    def __init__(
        self,
        api_key: str,
        model: str = "gemini-3-flash-preview",
        max_messages: int = 10,
        prompt: str = "You are a helpful asisstant.",
        temperature: float = 1.0,
        max_output_tokens: int = 8192,
        stats_filepath: str = "dracula_stats.json",
        language: str = "English",
    ):
        validate_api_key(api_key)
        validate_temperature(temperature)
        validate_max_output_tokens(max_output_tokens)
        validate_max_messages(max_messages)
        validate_prompt(prompt)
        validate_filepath(stats_filepath)

        try:
            self.client = genai.Client(api_key=api_key)
            self.model_name = model

        except Exception:
            raise InvalidAPIKeyException("Invalid API key or model name.")

        self.prompt = prompt
        self.language = language
        self.temperature = temperature
        self.max_output_tokens = max_output_tokens
        self.memory = Memory(max_messages=max_messages)
        self.stats = Stats(filepath=stats_filepath)

        check_latest_version(__version__)

    def __enter__(self):
        """
        Enter the context manager.

        Returns:
            Dracula: The current instance.

        Example:
            >>> with Dracula(api_key="your-api-key") as ai:
            ...     ai.chat("Hello!")
        """

        return self

    def __exit__(self, exc_type, exc, tb):
        """
        Exit the context manager and clean up.

        Automatically clears memory and resets stats when the with block ends.

        Returns:
            bool: False so exceptions are not suppressed.
        """

        self.clear_memory()
        self.reset_stats()
        return False

    def chat(self, message: str) -> str:
        """
        Send a message to Gemini and get a response.

        The message and response are automatically stored in memory
        so Gemini can refer back to previous messages in the conversation.

        Args:
            message (str): The message to send to Gemini.

        Returns:
            str: Gemini's response.

        Raises:
            ValidationException: If the message is empty.
            ChatException: If something goes wrong during the request.

        Example:
            >>> response = ai.chat("What is Python?")
            >>> print(response)
        """

        validate_message(message)

        try:
            self.memory.add_message("user", message)
            self.stats.record_message(message)

            history = [
                genai.types.Content(
                    role=msg["role"], parts=[genai.types.Part(text=msg["content"])]
                )
                for msg in self.memory.get_history()
            ]

            system_instruction = self.prompt
            if self.language.lower() != "auto":
                system_instruction = f"{self.prompt} Always respond in {self.language}."

            response = self.client.models.generate_content(
                model=self.model_name,
                contents=history,
                config=genai.types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    temperature=self.temperature,
                    max_output_tokens=self.max_output_tokens,
                ),
            )

            reply = response.text

            self.memory.add_message("model", reply)
            self.stats.record_response(reply)
            return reply

        except Exception as e:
            raise ChatException(f"Something went wrong: {str(e)}")

    def stream(self, message: str):
        """
        Send a message and receive the response word by word as it is generated.

        Instead of waiting for the full response, this method yields small chunks
        of text as they arrive, creating a real-time streaming effect similar to ChatGPT.

        Args:
            message (str): The message to send to Gemini.

        Yields:
            str: Small chunks of the response text.

        Raises:
            ValidationException: If the message is empty.
            ChatException: If something goes wrong during the request.

        Example:
            >>> for chunk in ai.stream("Tell me a long story."):
            ...     print(chunk, end="", flush=True)
        """

        validate_message(message)

        try:
            self.memory.add_message("user", message)
            self.stats.record_message(message)

            history = [
                genai.types.Content(
                    role=msg["role"], parts=[genai.types.Part(text=msg["content"])]
                )
                for msg in self.memory.get_history()
            ]

            system_instruction = self.prompt
            if self.language.lower() != "auto":
                system_instruction = f"{self.prompt} Always respond in {self.language}."

            response = self.client.models.generate_content_stream(
                model=self.model_name,
                contents=history,
                config=genai.types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    temperature=self.temperature,
                    max_output_tokens=self.max_output_tokens,
                ),
            )

            full_reply = ""
            for chunk in response:
                text = chunk.text
                full_reply += text
                yield text

            self.memory.add_message("model", full_reply)
            self.stats.record_response(full_reply)

        except Exception as e:
            raise ChatException(f"Something went wrong: {str(e)}")

    async def async_chat(self, message: str) -> str:
        """
        Asynchronously send a message to Gemini and get a response.

        This is the async version of chat(). Use this inside async
        applications like FastAPI, aiohttp, or Discord bots to avoid
        blocking the event loop while waiting for a response.

        Args:
            message (str): The message to send to Gemini.

        Returns:
            str: Gemini's response.

        Raises:
            ValidationException: If the message is empty.
            ChatException: If something goes wrong during the request.

        Example:
            >>> import asyncio
            >>> async def main():
            ...     response = await ai.async_chat("Hello!")
            ...     print(response)
            >>> asyncio.run(main())
        """

        validate_message(message)

        try:
            self.memory.add_message("user", message)
            self.stats.record_message(message)

            history = [
                genai.types.Content(
                    role=msg["role"], parts=[genai.types.Part(text=msg["content"])]
                )
                for msg in self.memory.get_history()
            ]

            system_instruction = self.prompt
            if self.language.lower() != "auto":
                system_instruction = f"{self.prompt} Always respond in {self.language}."

            response = await self.client.aio.models.generate_content(
                model=self.model_name,
                contents=history,
                config=genai.types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    temperature=self.temperature,
                    max_output_tokens=self.max_output_tokens,
                ),
            )

            reply = response.text
            self.memory.add_message("model", reply)
            self.stats.record_response(reply)
            return reply

        except Exception as e:
            raise ChatException(f"Something went wrong: {str(e)}")

    async def async_stream(self, message: str):
        """
        Asynchronously send a message and stream the response word by word.

        This is the async version of stream(). Use this inside async
        applications to stream responses without blocking the event loop.

        Args:
            message (str): The message to send to Gemini.

        Yields:
            str: Small chunks of the response text.

        Raises:
            ValidationException: If the message is empty.
            ChatException: If something goes wrong during the request.

        Example:
            >>> import asyncio
            >>> async def main():
            ...     async for chunk in ai.async_stream("Tell me a story."):
            ...         print(chunk, end="", flush=True)
            >>> asyncio.run(main())
        """

        validate_message(message)

        try:
            self.memory.add_message("user", message)
            self.stats.record_message(message)

            history = [
                genai.types.Content(
                    role=msg["role"], parts=[genai.types.Part(text=msg["content"])]
                )
                for msg in self.memory.get_history()
            ]

            full_reply = ""

            system_instruction = self.prompt
            if self.language.lower() != "auto":
                system_instruction = f"{self.prompt} Always respond in {self.language}."

            async for chunk in await self.client.aio.models.generate_content_stream(
                model=self.model_name,
                contents=history,
                config=genai.types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    temperature=self.temperature,
                    max_output_tokens=self.max_output_tokens,
                ),
            ):
                text = chunk.text
                full_reply += text
                yield text

            self.memory.add_message("model", full_reply)
            self.stats.record_response(full_reply)

        except Exception as e:
            raise ChatException(f"Something went wrong: {str(e)}")

    def get_stats(self) -> dict:
        """
        Return the current usage statistics.

        Returns:
            dict: A dictionary containing total_messages, total_responses,
                total_characters_sent, and total_characters_received.

        Example:
            >>> print(ai.get_stats())
        """

        return self.stats.get_stats()

    def reset_stats(self):
        """
        Reset all usage statistics back to zero.

        Example:
            >>> ai.reset_stats()
        """

        self.stats.reset()

    def print_history(self):
        """
        Print the conversation history in a clean, human-readable format.

        Example:
            >>> ai.print_history()
        """

        history = self.memory.get_history()

        if not history:
            print("No conversaiton history yet.")
            return

        print("\n" + "=" * 150)
        print("         🧛 DRACULA CONVERSATION HISTORY")
        print("=" * 150)

        for i, msg in enumerate(history, 1):
            if msg["role"] == "user":
                print(f"\n[{i}] 👤 You:")
            else:
                print(f"\n[{i}] 🤖 Gemini:")

            print(f"    {msg['content']}")

        print("\n" + "═" * 150 + "\n")

    def save_history(self, filepath: str):
        """
        Save the conversation history to a JSON file.

        Args:
            filepath (str): Path to the file where history will be saved.

        Raises:
            ValidationException: If the filepath is empty.
            ChatException: If the file cannot be saved.

        Example:
            >>> ai.save_history("conversation.json")
        """

        validate_filepath(filepath)
        self.memory.save(filepath)

    def load_history(self, filepath: str):
        """
        Load conversation history from a JSON file.

        Args:
            filepath (str): Path to the file to load history from.

        Raises:
            ValidationException: If the filepath is empty.
            ChatException: If the file cannot be loaded or does not exist.

        Example:
            >>> ai.load_history("conversation.json")
        """

        validate_filepath(filepath)
        self.memory.load(filepath)

    def clear_memory(self):
        """
        Clear the conversation history.

        After calling this, Gemini will have no memory of previous messages.

        Example:
            >>> ai.clear_memory()
        """

        self.memory.clear()

    def set_prompt(self, prompt: str):
        """
        Change the system prompt and clear conversation memory.

        Args:
            prompt (str): The new system prompt.

        Returns:
            Dracula: The current instance for method chaining.

        Example:
            >>> ai.set_prompt("You are a pirate.")
        """

        validate_prompt(prompt)
        self.prompt = prompt
        self.memory.clear()
        return self

    def set_temperature(self, temperature: float):
        """
        Change the temperature setting.

        Temperature controls how creative and random responses are.
        Lower values (close to 0.0) produce focused, predictable responses.
        Higher values (close to 2.0) produce creative, varied responses.

        Args:
            temperature (float): A value between 0.0 and 2.0.

        Returns:
            Dracula: The current instance for method chaining.

        Example:
            >>> ai.set_temperature(0.5)
        """

        validate_temperature(temperature)
        self.temperature = temperature
        return self

    def set_max_output_tokens(self, max_output_tokens: int):
        """
        Change the maximum response length in tokens.

        Args:
            max_output_tokens (int): Maximum number of tokens in the response.

        Returns:
            Dracula: The current instance for method chaining.

        Example:
            >>> ai.set_max_output_tokens(512)
        """

        validate_max_output_tokens(max_output_tokens)
        self.max_output_tokens = max_output_tokens
        return self

    def set_language(self, language: str):
        """
        Change the response language and clear conversation memory.

        Forces Gemini to always respond in the specified language
        regardless of what language the user writes in.

        Args:
            language (str): The language for responses (e.g. 'Turkish', 'Spanish').

        Returns:
            Dracula: The current instance for method chaining.

        Example:
            >>> ai.set_language("Turkish")
        """

        validate_language(language)
        self.language = language
        self.memory.clear()
        return self

    def set_persona(self, persona: str):
        """
        Switch to a built-in persona instantly.

        Each persona comes with a predefined prompt, temperature, and language.
        Switching personas automatically clears the conversation memory.

        Args:
            persona (str): The name of the persona to use.

        Returns:
            Dracula: The current instance for method chaining.

        Raises:
            PersonaException: If the persona name is not recognized.

        Example:
            >>> ai.set_persona("pirate")
        """

        if persona not in PERSONAS:
            available = ", ".join(PERSONAS.keys())

            raise PersonaException(
                f"Unknown persona '{persona}'. Available personas: {available}"
            )

        selected = PERSONAS[persona]
        self.set_prompt(selected["prompt"])
        self.set_temperature(selected["temperature"])
        self.set_language(selected["language"])
        return self

    def list_personas(self):
        """
        Return a list of all available built-in persona names.

        Returns:
            list: A list of persona name strings.

        Example:
            >>> print(ai.list_personas())
            ['assistant', 'pirate', 'chef', 'shakespeare', 'scientist', 'comedian']
        """

        return list(PERSONAS.keys())

    def get_history(self):
        """
        Return the full conversation history as a list of dictionaries.

        Returns:
            list: A list of message dictionaries with 'role' and 'content' keys.

        Example:
            >>> print(ai.get_history())
        """

        return self.memory.get_history()
