
"""Message and event handler for LLMService.

This module contains all event handling methods that process incoming
messages and events from the event bus, delegating to LLMService's
orchestration methods.
"""

import asyncio
import logging
from typing import Any, Dict, TYPE_CHECKING

if TYPE_CHECKING:
    from kollabor_events.data_models import ConversationMessage

logger = logging.getLogger(__name__)


class MessageHandler:
    """Handles incoming events and messages for the LLM service.

    This class encapsulates all event handler methods that process
    incoming events from the event bus (USER_INPUT, CANCEL_REQUEST,
    ADD_MESSAGE, TRIGGER_LLM_CONTINUE, CONTEXT_INJECTION).

    Takes a single coordinator reference instead of individual dependencies.
    """

    def __init__(self, coordinator):
        """Initialize the message handler.

        Args:
            coordinator: LLMService coordinator (provides event_bus, renderer,
                context_service, conversation_logger, message_display_service,
                api_service, conversation_history, and state properties)
        """
        self._coordinator = coordinator

    async def handle_context_injection(
        self, data: Dict[str, Any], event
    ) -> Dict[str, Any]:
        """Handle context injection before user input processing.

        Scans user input for keyword triggers and auto-loads relevant context.

        Args:
            data: Event data containing user message
            event: The event object

        Returns:
            Unmodified data (context is injected as system message)
        """
        message = data.get("message", "")
        if message.strip():
            try:
                await self._coordinator.context_service.trigger_context_injection(message)
            except Exception as e:
                logger.error(f"Context injection failed: {e}")
        return data

    async def handle_user_input(self, data: Dict[str, Any], event) -> Dict[str, Any]:
        """Handle user input hook callback.

        This is called by the event bus when user input occurs.
        Gates on startup_ready to hold messages typed during initialization.

        Args:
            data: Event data containing user message
            event: The event object

        Returns:
            Result of processing
        """
        # Wait for startup to complete before processing user input
        startup_ready = self._coordinator.event_bus.get_service("startup_ready")
        if startup_ready and not startup_ready.is_set():
            try:
                await asyncio.wait_for(startup_ready.wait(), timeout=30)
            except asyncio.TimeoutError:
                logger.error("Timed out waiting for startup to complete")
                return {"status": "startup_timeout"}

        message = data.get("message", "")
        if message.strip():
            result = await self._coordinator.process_user_input(message)
            return result
        return {"status": "empty_message"}

    async def handle_cancel_request(
        self, data: Dict[str, Any], event
    ) -> Dict[str, Any]:
        """Handle cancel request hook callback.

        This is called by the event bus when a cancellation request occurs.

        Args:
            data: Event data containing cancellation reason
            event: The event object

        Returns:
            Result of cancellation
        """
        reason = data.get("reason", "unknown")
        source = data.get("source", "unknown")

        # Check if we're in pipe mode - ignore cancel requests from stdin
        renderer = self._coordinator.renderer
        if hasattr(renderer, "pipe_mode") and getattr(renderer, "pipe_mode", False):
            logger.info(
                f"LLM SERVICE: Ignoring cancel request in pipe mode (from {source}: {reason})"
            )
            return {"status": "ignored", "reason": "pipe_mode"}

        logger.info(f"LLM SERVICE: Cancel request hook called! From {source}: {reason}")
        logger.info(f"LLM SERVICE: Currently processing: {self._coordinator.is_processing}")

        # Cancel current request
        self._coordinator.cancel_current_request()

        logger.info(
            f"LLM SERVICE: Cancellation flag set: {self._coordinator.cancel_processing}"
        )
        return {"status": "cancelled", "reason": reason}

    async def handle_add_message(self, data: Dict[str, Any], event) -> Dict[str, Any]:
        """Handle ADD_MESSAGE event - inject messages into conversation.

        This allows plugins to inject messages into the conversation that:
        - Get added to AI-visible history
        - Get logged to conversation logger
        - Get displayed to user with loading indicator
        - Optionally trigger LLM response

        Args:
            data: Event data with messages array and options
            event: The event object

        Returns:
            Result dict with status and message count
        """
        messages = data.get("messages", [])
        options = data.get("options", {})

        if not messages:
            return {"success": False, "error": "No messages provided"}

        show_loading = options.get("show_loading", True)
        loading_message = options.get("loading_message", "Loading...")
        log_messages = options.get("log_messages", True)
        add_to_history = options.get("add_to_history", True)
        display_messages = options.get("display_messages", True)
        trigger_llm = options.get("trigger_llm", False)
        parent_uuid = options.get("parent_uuid", self._coordinator.current_parent_uuid)

        coord = self._coordinator
        mds = coord.message_display_service

        try:
            # Show loading indicator
            if show_loading:
                mds.show_loading(loading_message)

            display_sequence = []

            for msg in messages:
                role = msg.get("role", "user")
                content = msg.get("content", "")

                # Add to conversation history
                if add_to_history:
                    from kollabor_events.data_models import ConversationMessage

                    coord._add_conversation_message(
                        ConversationMessage(role=role, content=content),
                        parent_uuid=parent_uuid,
                    )

                # Log message
                if log_messages:
                    conv_logger = coord.conversation_logger
                    if role == "user":
                        parent_uuid = await conv_logger.log_user_message(
                            content, parent_uuid=parent_uuid
                        )
                    elif role == "assistant":
                        parent_uuid = (
                            await conv_logger.log_assistant_message(
                                content,
                                parent_uuid=parent_uuid,
                                model=coord.api_service.model,
                                thinking_content=None,
                            )
                        )
                    elif role == "system":
                        await conv_logger.log_system_message(
                            content, parent_uuid=parent_uuid
                        )

                # Build display sequence
                if display_messages and role in ("user", "assistant", "system"):
                    display_sequence.append((role, content, {}))

            # CRITICAL FIX: Display messages BEFORE hiding loading indicator
            # The loading indicator must remain active until messages are fully rendered.
            # If we hide loading first, the render loop may clear the screen before
            # messages are displayed, causing them to disappear.
            if display_messages and display_sequence:
                mds.message_coordinator.display_message_sequence(
                    display_sequence
                )
                # Wait for messages to render before hiding loading
                await asyncio.sleep(0.1)

            # Now hide loading after messages are displayed
            if show_loading:
                mds.hide_loading()

            # Update session stats
            session_stats = coord.session_stats
            if hasattr(session_stats, "__setitem__"):
                session_stats["messages"] += len(messages)

            # Optionally trigger LLM response
            if trigger_llm:
                # Find the last user message to process
                last_user_msg = None
                for msg in reversed(messages):
                    if msg.get("role") == "user":
                        last_user_msg = msg.get("content", "")
                        break

                if last_user_msg:
                    await coord._enqueue_with_overflow_strategy(last_user_msg)
                    if not coord.is_processing:
                        # Trigger queue processing via background task
                        coord.create_background_task(
                            coord._process_queue(), name="process_queue"
                        )

            logger.info(
                f"ADD_MESSAGE: Processed {len(messages)} messages, trigger_llm={trigger_llm}"
            )
            return {
                "success": True,
                "message_count": len(messages),
                "parent_uuid": parent_uuid,
                "llm_triggered": trigger_llm,
            }

        except Exception as e:
            # Ensure loading is hidden on error
            if show_loading:
                mds.hide_loading()
            logger.error(f"Error in ADD_MESSAGE handler: {e}")
            return {"success": False, "error": str(e)}

    async def handle_llm_continue(self, data: Dict[str, Any], event) -> Dict[str, Any]:
        """Handle TRIGGER_LLM_CONTINUE event - trigger LLM to process injected messages.

        This is called when plugins inject messages asynchronously (e.g., when a
        background agent completes) and want the LLM to respond to them.

        The message has already been added to conversation history by MessageInjector.
        This handler triggers the LLM to process and respond.

        Args:
            data: Event data (may contain source info)
            event: The event object

        Returns:
            Result dict with status
        """
        source = data.get("source", "unknown")
        coord = self._coordinator
        logger.info(f"TRIGGER_LLM_CONTINUE: Received from {source}")

        # Don't trigger if already processing
        if coord.is_processing:
            logger.info("TRIGGER_LLM_CONTINUE: Already processing, skipping")
            return {"status": "already_processing"}

        # Don't trigger in pipe mode (would interfere with normal flow)
        renderer = coord.renderer
        if hasattr(renderer, "pipe_mode") and getattr(renderer, "pipe_mode", False):
            logger.info("TRIGGER_LLM_CONTINUE: Pipe mode active, skipping")
            return {"status": "pipe_mode"}

        try:
            # Get the last user message from conversation history
            # (this is the message that was injected by MessageInjector)
            last_message = None
            if coord.conversation_history:
                for msg in reversed(coord.conversation_history):
                    if msg.role == "user":
                        last_message = msg.content
                        break

            if not last_message:
                logger.warning("TRIGGER_LLM_CONTINUE: No user message found in history")
                return {"status": "no_message"}

            # Enqueue the message for processing
            # The message is already in conversation_history, but we need to
            # trigger the processing loop
            await coord._enqueue_with_overflow_strategy(last_message)

            # Start processing
            if not coord.is_processing:
                coord.create_background_task(
                    coord._process_queue(), name="process_queue_continue"
                )

            logger.info("TRIGGER_LLM_CONTINUE: Triggered LLM processing")
            return {"status": "triggered", "source": source}

        except Exception as e:
            logger.error(f"Error in TRIGGER_LLM_CONTINUE handler: {e}")
            return {"status": "error", "error": str(e)}
