"""
Manages conversation history, token counting, and SQLite-backed persistence for any LLM provider.

The Conversation class is keyed by chat_id (Telegram chat ID) and maintains an in-memory message
history, delegating persistence to the database module. Supports async token counting via provider-
specific APIs and automatic pruning when token limits are approached.
"""
from .database import insert_message, load_full_user_context, delete_messages_for_user, delete_messages_for_chat
from .models import TokenLimits
from .utils import generate_error_path, log_error

class Conversation:
    """
    Manages a chat conversation including message history, token counting, and SQLite persistence.

    Keyed by chat_id (Telegram chat ID): negative for groups, positive (== user_id) for private chats.
    Works with any LLM provider (OpenAI, Anthropic). When the token limit is approached, oldest
    messages are pruned from memory; the database retains the full history.
    """

    def __init__(self, chat_id: int, chat_type: str, system_content: str, system_model: str = "gpt-4o-mini"):
        """
        Initialize a conversation keyed by chat_id.

        Args:
            chat_id: Telegram chat ID (negative for groups, positive/==user_id for private chats).
            chat_type: Chat type ('private', 'group', 'supergroup', 'channel').
            system_content: System prompt (bot persona).
            system_model: LLM model to use for token counting (default: "gpt-4o-mini").
        """
        self.error_log = generate_error_path()

        self.chat_id = chat_id
        self.chat_type = chat_type
        self.chat_print = f"{self.chat_type} Chat {self.chat_id}"

        self.system_content = system_content
        self.system_model = TokenLimits(system_model)
        self.messages = [{"role": "system", "content": system_content}]

        self._loaded_user_ids: set = set()

    def set_system_content(self, new_content: str):
        """
        Replace the system content (bot's personality prompt) in the conversation.

        Updates both self.system_content and the first message in self.messages.

        Args:
            new_content: The new system prompt text.
        """
        self.system_content = new_content
        self.messages[0] = {"role": "system", "content": new_content}

    async def add_user_message(self, content: str, user_id: int, username: str | None, is_private: bool = False):
        """
        Add a user message to conversation memory and persist it to the database.

        Args:
            content: The message text.
            user_id: Telegram user ID of the sender.
            username: Telegram username (display only, may be None).
            is_private: If True, excludes this message from group context loading (used for /forget or private commands).
        """
        self.messages.append({"role": "user", "content": content})
        await insert_message(self.chat_id, user_id, username, "user", content, is_private)

    async def add_assistant_message(self, content: str, bot_user_id: int, bot_username: str | None, is_private: bool = False):
        """
        Add an assistant message to conversation memory and persist it to the database.

        Args:
            content: The message text (LLM response).
            bot_user_id: Telegram user ID of the bot account.
            bot_username: Telegram username of the bot (display only, may be None).
            is_private: If True, excludes this reply from group context loading. Should match
                        the is_private flag of the user message it responds to, so that both
                        sides of a private-mode exchange are excluded from shared contexts.
        """
        self.messages.append({"role": "assistant", "content": content})
        await insert_message(self.chat_id, bot_user_id, bot_username, "assistant", content, is_private)

    async def get_message_token_count(self) -> int:
        """
        Get the current token count of all messages in this conversation.

        Delegates to the configured model's provider to count tokens accurately.
        This is an async operation because it may contact the provider API (e.g., for Anthropic).

        Returns:
            The total token count for all messages (system, user, assistant).

        Raises:
            ProviderAuthError: If API credentials are missing or invalid.
            ProviderConnectionError: If the provider API cannot be reached.
        """
        return await self.system_model.num_tokens_from_messages(self.messages)

    async def prune_conversation(self, token_limit: int):
        """
        Remove oldest user-assistant messages until token count is below threshold.

        Recursively removes messages starting from index 1 (preserving the system message at index 0)
        until the conversation fits within the token limit. The database is not modified — pruning
        only affects the in-memory context window for the current session.

        Args:
            token_limit: The maximum token count threshold. Messages are removed until
                         the conversation is strictly below this limit.
        """
        try:
            self.messages.pop(1)
            if await self.get_message_token_count() > token_limit:
                await self.prune_conversation(token_limit)
        except Exception as e:
            log_error(e, f"{type(e).__name__} pruning under {token_limit} tokens for {self.chat_print}", self.error_log)

    async def get_past_interaction(self, token_limit: int, user_id: int) -> bool:
        """
        Load past conversation messages from the database up to a token limit.

        Implements bidirectional cross-pollination: loads all messages from the current chat
        (all participants) plus all messages from the user's private chat (both user and
        assistant turns), merged chronologically. This eliminates amnesia when a user switches
        between private and group contexts.

        For group and supergroup chats, messages marked as private (is_private=1) are excluded
        so private-mode messages don't surface in shared contexts.

        Per-user idempotent: calling again for the same user_id returns True without reloading.
        Calling for a new user_id (e.g. a second participant joining a group session) merges
        that user's cross-chat context into the existing conversation. Context is considered
        "found" only if messages were actually inserted for this user (rows_inserted > 0), not
        merely if the conversation has other content from prior participants.

        Args:
            token_limit: The maximum token count to load into the in-memory context.
            user_id: Telegram user ID of the requesting user; used to fetch bidirectional context.

        Returns:
            True if past interactions were found for this user (either on this call or previously),
            False if no past interactions exist for this user.

        Side Effects:
            - Modifies self.messages in-place by inserting historical messages after the system message
              (attempting to fit messages starting from most recent, stopping when limit is reached).
            - Adds user_id to self._loaded_user_ids once their context has been merged.
            - Prints to stdout showing token count and whether the token limit was reached.
        """
        if user_id in self._loaded_user_ids:
            print(f"Context already loaded for User {user_id} in {self.chat_print}")
            return len(self.messages) > 1

        exclude_private = self.chat_type in ('group', 'supergroup')
        rows = await load_full_user_context(user_id, self.chat_id, exclude_private=exclude_private)

        token_limit_reached = False
        token_count = 0
        rows_inserted = 0

        for row in reversed(rows):
            self.messages.insert(1, row)
            token_count = await self.get_message_token_count()
            if token_count > token_limit:
                self.messages.pop(1)
                token_count = await self.get_message_token_count()
                token_limit_reached = True
                break
            rows_inserted += 1

        has_context = rows_inserted > 0

        # Only mark as loaded after confirming context was found. If no rows exist yet
        # (user has no history), leave _loaded_user_ids clean so the next call retries.
        if has_context:
            self._loaded_user_ids.add(user_id)
            if token_limit_reached:
                print(
                    f"Partial context loaded for User {user_id} in {self.chat_print} — "
                    f"full history exceeded token limit ({token_limit}), storing {token_count} token(s)"
                )
            else:
                print(
                    f"Full context loaded for User {user_id} in {self.chat_print} — "
                    f"storing {token_count} token(s)"
                )
        else:
            print(f"No prior context found for User {user_id} in {self.chat_print}")

        return has_context

    async def clear_interaction(self, user_id: int):
        """
        Clear this user's messages from memory and from the database.

        In private chats, all messages in the chat are deleted (including bot replies),
        fully clearing the conversation. In group chats, only this user's rows are removed;
        other members' messages remain.

        Args:
            user_id: Telegram user ID of the user requesting the forget.
        """
        self.messages = [self.messages[0]]
        self._loaded_user_ids = set()
        if self.chat_type == 'private':
            await delete_messages_for_chat(self.chat_id)
        else:
            await delete_messages_for_user(user_id)
