"""Queue processing for Kollabor CLI LLM service.

Handles message queue management, overflow strategies, and LLM turn execution.
Extracted from LLMService as part of the llm_service.py decomposition (Phase D).
"""

import asyncio
import logging
import time
from typing import Any, Callable, Dict, List, Optional

from kollabor_events.data_models import ConversationMessage
from kollabor_events.models import EventType
from kollabor_tui.status.core_widgets import get_token_io_state

logger = logging.getLogger(__name__)


class QueueProcessor:
    """Handles queue processing and LLM turn execution.

    Responsibilities:
    - Message queue management with overflow strategies
    - Batch message processing
    - Conversation continuation (agentic turns)
    - Deduped LLM turn execution (_execute_llm_turn)
    - Queue state management (is_processing, turn_completed, etc.)

    Key Design:
    - _execute_llm_turn() deduplicates _process_message_batch and _continue_conversation
    - Only difference: _process_message_batch adds user message first
    - All queue state owned by this class (accessed via properties on LLMService)
    """

    def __init__(
        self,
        conversation_history: List[ConversationMessage],
        session_stats: Dict[str, Any],
        stats: Dict[str, Any],
        pending_tools: List[Dict[str, Any]],
        queue_metrics: Dict[str, Any],
        task_config,
        api_service,
        tool_executor,
        response_parser,
        message_display_service,
        renderer,
        config,
        event_bus,
        conversation_logger,
        streaming_handler,
        native_tools_handler,
        add_message_fn: Callable,
        max_history: int,
        question_gate_enabled: bool,
        max_queue_size: int,
    ):
        """Initialize queue processor.

        Args:
            conversation_history: Shared conversation history list (mutable reference)
            session_stats: Shared session stats dict (mutable reference)
            stats: Shared stats dict (mutable reference)
            pending_tools: Shared pending tools list (mutable reference)
            queue_metrics: Shared queue metrics dict (mutable reference)
            task_config: LLMTaskConfig for queue settings
            api_service: APICommunicationService instance
            tool_executor: ToolExecutor instance
            response_parser: ResponseParser instance
            message_display_service: MessageDisplayService instance
            renderer: TerminalRenderer instance
            config: Config service instance
            event_bus: EventBus instance
            conversation_logger: KollaborConversationLogger instance
            streaming_handler: StreamingHandler instance
            native_tools_handler: NativeToolsHandler instance
            add_message_fn: Callback to add message to conversation (LLMService._add_conversation_message)
            max_history: Maximum history messages for API calls
            question_gate_enabled: Whether question gate is enabled
            max_queue_size: Maximum queue size
        """
        # Shared mutable containers (passed by reference)
        self.conversation_history = conversation_history
        self.session_stats = session_stats
        self.stats = stats
        self.pending_tools = pending_tools
        self._queue_metrics = queue_metrics

        # Configuration and dependencies
        self.task_config = task_config
        self.api_service = api_service
        self.tool_executor = tool_executor
        self.response_parser = response_parser
        self.message_display_service = message_display_service
        self.renderer = renderer
        self.config = config
        self.event_bus = event_bus
        self.conversation_logger = conversation_logger
        self._streaming_handler = streaming_handler
        self._native_tools_handler = native_tools_handler
        self._add_message_fn = add_message_fn
        self._max_history = max_history
        self.question_gate_enabled = question_gate_enabled

        # Queue state (owned by QueueProcessor, accessed via properties)
        self.processing_queue = asyncio.Queue(maxsize=max_queue_size)
        self.max_queue_size = max_queue_size
        self.dropped_messages = 0
        self.is_processing = False
        self.turn_completed = False
        self.cancel_processing = False
        self.cancellation_message_shown = False

        # Processing state (owned by QueueProcessor)
        self.current_processing_tokens = 0
        self.processing_start_time = None
        self.question_gate_active = False

    async def enqueue(self, message: str) -> None:
        """Enqueue message with overflow strategy."""
        self._queue_metrics["total_enqueue_attempts"] += 1

        if self.task_config.queue.log_queue_events:
            logger.debug(
                f"Attempting to enqueue message (queue size: {self.processing_queue.qsize()}/{self.max_queue_size})"
            )

        # Try immediate enqueue
        try:
            self.processing_queue.put_nowait(message)
            self._queue_metrics["total_enqueue_successes"] += 1
            if self.task_config.queue.log_queue_events:
                logger.debug("Message enqueued successfully")
            return
        except asyncio.QueueFull:
            pass

        strategy = self.task_config.queue.overflow_strategy

        if strategy == "drop_oldest":
            await self._drop_oldest_strategy(message)
        elif strategy == "drop_newest":
            self._drop_newest_strategy()
        elif strategy == "block":
            await self._block_strategy(message)
        else:
            self._unknown_strategy(message)

    async def _drop_oldest_strategy(self, message: str) -> None:
        """Drop oldest task to make room."""
        if self.task_config.queue.log_queue_events:
            logger.debug("Applying drop_oldest strategy")

        # Find oldest task by start_time from task manager
        # Note: We need access to task_manager metadata here
        # For now, we'll drop from queue directly
        try:
            self.processing_queue.get_nowait()
            self._queue_metrics["drop_oldest_count"] += 1
            if self.task_config.queue.log_queue_events:
                logger.info("Dropped oldest message from queue")
        except asyncio.QueueEmpty:
            pass

        await asyncio.sleep(0.01)

        try:
            self.processing_queue.put_nowait(message)
            self._queue_metrics["total_enqueue_successes"] += 1
        except asyncio.QueueFull:
            self.dropped_messages += 1

    def _drop_newest_strategy(self) -> None:
        """Raise error when queue is full."""
        self._queue_metrics["drop_newest_count"] += 1
        if self.task_config.queue.log_queue_events:
            logger.debug("Applying drop_newest strategy - raising RuntimeError")
        raise RuntimeError(
            f"Queue is full (max size: {self.max_queue_size}) and overflow strategy is 'drop_newest'"
        )

    async def _block_strategy(self, message: str) -> None:
        """Block until queue has space or timeout."""
        self._queue_metrics["block_count"] += 1
        if self.task_config.queue.log_queue_events:
            logger.debug(
                f"Applying block strategy (timeout: {self.task_config.queue.block_timeout}s)"
            )

        start_time = time.time()
        poll_interval = 0.01

        while True:
            if self.processing_queue.qsize() < self.max_queue_size:
                try:
                    self.processing_queue.put_nowait(message)
                    self._queue_metrics["total_enqueue_successes"] += 1
                    return
                except asyncio.QueueFull:
                    pass

            elapsed = time.time() - start_time
            if (
                self.task_config.queue.block_timeout is not None
                and elapsed >= self.task_config.queue.block_timeout
            ):
                self._queue_metrics["block_timeout_count"] += 1
                if self.task_config.queue.log_queue_events:
                    logger.warning(f"Block timeout after {elapsed:.2f}s, dropping message")
                self.dropped_messages += 1
                return

            await asyncio.sleep(poll_interval)

    def _unknown_strategy(self, message: str) -> None:
        """Handle unknown overflow strategy."""
        logger.warning(
            f"Unknown overflow strategy '{self.task_config.queue.overflow_strategy}', defaulting to drop_oldest"
        )
        try:
            self.processing_queue.get_nowait()
            self.processing_queue.put_nowait(message)
            self._queue_metrics["total_enqueue_successes"] += 1
        except asyncio.QueueEmpty:
            self.dropped_messages += 1

    async def process_queue(
        self,
        task_manager,
        process_message_batch_fn: Callable,
        continue_conversation_fn: Callable,
    ):
        """Process queued messages.

        Args:
            task_manager: BackgroundTaskManager for accessing task metadata
            process_message_batch_fn: Callable to process message batch
            continue_conversation_fn: Callable to continue conversation
        """
        self.is_processing = True
        self.current_processing_tokens = 0
        self.processing_start_time = time.time()
        logger.info("Started processing queue")

        try:
            # Process all queued messages
            while not self.processing_queue.empty() and not self.cancel_processing:
                try:
                    messages = []
                    while not self.processing_queue.empty():
                        message = await self.processing_queue.get()
                        messages.append(message)

                    if messages and not self.cancel_processing:
                        await process_message_batch_fn(messages)

                except Exception as e:
                    logger.error(f"Queue processing error: {e}")
                    error_msg = str(e)
                    if "'str' object has no attribute 'get'" in error_msg:
                        error_msg = (
                            "API format mismatch. Your profile's tool_format setting may be wrong.\n"
                            "Run /profile, press 'e' to edit, and check Tool Format matches your API."
                        )
                    self.message_display_service.display_error_message(error_msg)
                    break

            # Continue conversation until completed
            turn_count = 0
            while not self.turn_completed and not self.cancel_processing:
                try:
                    turn_count += 1
                    logger.info(f"Turn not completed - continuing conversation (turn {turn_count})")
                    await continue_conversation_fn()
                except Exception as e:
                    logger.error(f"Continued conversation error (turn {turn_count}): {e}")
                    self.turn_completed = True
                    break

        finally:
            self.is_processing = False
            self.current_processing_tokens = 0
            self.processing_start_time = None

            if self.cancel_processing:
                logger.info("Processing cancelled by user")
                if not self.cancellation_message_shown:
                    self.cancellation_message_shown = True
                    self.message_display_service.display_cancellation_message()
            else:
                logger.info("Finished processing queue")

    async def process_message_batch(
        self,
        messages: List[str],
        current_parent_uuid: str,
    ) -> str:
        """Process a batch of messages.

        Args:
            messages: List of user messages to process
            current_parent_uuid: Current parent UUID for message threading

        Returns:
            Updated parent UUID
        """
        combined_message = "\n".join(messages)

        # Add user message to conversation history
        self._add_message_fn(
            ConversationMessage(role="user", content=combined_message),
            parent_uuid=current_parent_uuid,
        )

        # Execute LLM turn with user message
        return await self._execute_llm_turn(
            user_message_provided=True,
            current_parent_uuid=current_parent_uuid,
        )

    async def continue_conversation(self, current_parent_uuid: str) -> str:
        """Continue an ongoing conversation (no new user message).

        Args:
            current_parent_uuid: Current parent UUID for message threading

        Returns:
            Updated parent UUID
        """
        return await self._execute_llm_turn(
            user_message_provided=False,
            current_parent_uuid=current_parent_uuid,
        )

    async def _execute_llm_turn(
        self,
        user_message_provided: bool,
        current_parent_uuid: str,
    ) -> str:
        """Execute a single LLM turn (deduped from process_message_batch and continue_conversation).

        The ONLY difference between the two call sites:
        - process_message_batch: adds user message to history first (user_message_provided=True)
        - continue_conversation: skips user message step (user_message_provided=False)

        Args:
            user_message_provided: True if user message was just added to history
            current_parent_uuid: Current parent UUID for message threading

        Returns:
            Updated parent UUID

        Raises:
            asyncio.CancelledError: If request was cancelled by user
        """
        # Start thinking animation - Sending phase
        self.renderer.update_thinking(True, "Sending...")
        thinking_start = time.time()

        # Estimate input tokens for status display
        total_input_chars = sum(
            len(msg.content) for msg in self.conversation_history[-3:]
        )
        estimated_input_tokens = total_input_chars // 4
        self.current_processing_tokens = estimated_input_tokens

        # Trigger token I/O upload animation
        get_token_io_state().start_upload(estimated_input_tokens)

        # Switch to Waiting phase before API call
        self.renderer.update_thinking(True, "Waiting...")
        get_token_io_state().start_waiting()

        # Emit LLM_REQUEST_PRE directly (POST emitted separately after API call)
        await self.event_bus.emit_with_hooks(
            EventType.LLM_REQUEST_PRE,
            {
                "model": getattr(self.api_service, "model", "unknown"),
                "message_count": len(self.conversation_history),
                "max_history": self._max_history,
            },
            "llm_service",
        )

        response = None
        parent_uuid = current_parent_uuid

        try:
            # Call LLM API via streaming handler
            response = await self._streaming_handler.call_llm(
                conversation_history=self.conversation_history,
                max_history=self._max_history,
                native_tools=self._native_tools_handler.tools,
                mcp_discovery_complete=self._native_tools_handler.discovery_complete,
                is_cancelled_fn=lambda: self.cancel_processing,
            )

            # Update session stats with actual token usage
            token_usage = self.api_service.get_last_token_usage()
            prompt_tokens = 0
            completion_tokens = 0
            if token_usage:
                prompt_tokens = token_usage.get("prompt_tokens", 0)
                completion_tokens = token_usage.get("completion_tokens", 0)

                # Finalize token I/O with actual counts
                token_io = get_token_io_state()
                token_io.upload_target = prompt_tokens
                token_io.upload_tokens = prompt_tokens
                token_io.upload_display = prompt_tokens
                token_io.finish(completion_tokens)

                # Store and accumulate stats
                self.session_stats["input_tokens"] = prompt_tokens
                self.session_stats["output_tokens"] = completion_tokens
                self.session_stats["total_input_tokens"] += prompt_tokens
                self.session_stats["total_output_tokens"] += completion_tokens
                logger.debug(
                    f"Token usage: {prompt_tokens} input, {completion_tokens} output"
                )

            # Emit LLM_REQUEST_POST with actual token data
            # Also read from session_stats which may have been set by streaming
            final_in = prompt_tokens or self.session_stats.get("input_tokens", 0)
            final_out = completion_tokens or self.session_stats.get("output_tokens", 0)
            await self.event_bus.emit_with_hooks(
                EventType.LLM_REQUEST_POST,
                {
                    "model": getattr(self.api_service, "model", "unknown"),
                    "message_count": len(self.conversation_history),
                    "input_tokens": final_in,
                    "output_tokens": final_out,
                },
                "llm_service",
            )

            # Check for native tool calls (API function calling)
            if (
                self._native_tools_handler.tool_calling_enabled
                and self.api_service.has_pending_tool_calls()
            ):
                thinking_duration = time.time() - thinking_start
                self.renderer.update_thinking(False)

                logger.info("Processing native tool calls from API response")

                raw_tool_calls = self.api_service.get_last_tool_calls()
                original_tools = [
                    {"name": tc.name, "arguments": tc.input} for tc in raw_tool_calls
                ]

                tool_count = len(raw_tool_calls)
                tool_desc = (
                    raw_tool_calls[0].name if tool_count == 1 else f"{tool_count} tools"
                )
                self.renderer.update_thinking(True, f"Executing {tool_desc}...")

                native_results = await self._native_tools_handler.execute_tool_calls(
                    self.tool_executor
                )

                self.renderer.update_thinking(False)

                # Display response and native tool results
                self.message_display_service.display_complete_response(
                    thinking_duration=thinking_duration,
                    response=response,
                    tool_results=native_results,
                    original_tools=original_tools,
                )

                # Build structured tool call list for JSONL logging
                tool_call_entries = [
                    {"id": tc.id, "name": tc.name, "input": tc.input}
                    for tc in raw_tool_calls
                ]

                # Get reasoning/thinking content from provider response
                native_thinking = self.api_service.last_thinking_content
                thinking_list = [native_thinking] if native_thinking else None

                # Log assistant message with tool calls in content array
                parent_uuid = await self.conversation_logger.log_assistant_message(
                    response,
                    parent_uuid=parent_uuid,
                    model=self.api_service.model,
                    usage_stats={
                        "input_tokens": self.session_stats.get("input_tokens", 0),
                        "output_tokens": self.session_stats.get("output_tokens", 0),
                        "thinking_duration": thinking_duration,
                    },
                    thinking_content=thinking_list,
                    tool_calls=tool_call_entries,
                )
                # Add to conversation history + snapshot via _add_message_fn
                self._add_message_fn(
                    ConversationMessage(role="assistant", content=response),
                    parent_uuid=parent_uuid,
                )

                # Log tool results to JSONL and add to conversation history
                for result in native_results:
                    # Log to JSONL (matches XML tools path behavior)
                    output_text = result.output if result.success else result.error
                    await self.conversation_logger.log_system_message(
                        f"Executed {result.tool_type} ({result.tool_id}): {output_text}",
                        parent_uuid=parent_uuid,
                        subtype="tool_result",
                        tool_use_id=result.tool_id,
                    )

                    # Add to conversation history in native format
                    tool_calls = self.api_service.get_last_tool_calls()
                    for tc in tool_calls:
                        if tc.id == result.tool_id:
                            msg = self.api_service.format_tool_result(
                                tc.id,
                                output_text,
                                is_error=not result.success,
                            )
                            self.conversation_history.append(
                                ConversationMessage(
                                    role=msg.get("role", "tool"),
                                    content=str(msg.get("content", result.output)),
                                )
                            )
                            break

                # Continue conversation to get LLM response with tool results
                self.turn_completed = False
                self.stats["total_thinking_time"] += thinking_duration
                self.session_stats["messages"] += 1
                return parent_uuid

            # Stop thinking animation
            thinking_duration = time.time() - thinking_start
            self.renderer.update_thinking(False)

            # Brief pause for clean transition
            await asyncio.sleep(self.config.get("kollabor.llm.processing_delay", 0.1))

            # Parse response for XML-based tools
            parsed_response = self.response_parser.parse_response(response)
            clean_response = parsed_response["content"]
            all_tools = self.response_parser.get_all_tools(parsed_response)

            # Emit LLM_THINKING if thinking content present
            thinking_blocks = parsed_response.get("components", {}).get("thinking", [])
            if thinking_blocks:
                await self.event_bus.emit_with_hooks(
                    EventType.LLM_THINKING,
                    {
                        "content": thinking_blocks[0][:200] if thinking_blocks else "",
                        "thinking": thinking_blocks,
                        "block_count": len(thinking_blocks),
                    },
                    "llm_service",
                )

            # Update turn completion and stats
            self.turn_completed = parsed_response["turn_completed"]
            self.stats["total_thinking_time"] += thinking_duration
            self.session_stats["messages"] += 1

            # Show "Receiving..." briefly before displaying
            if clean_response.strip() or all_tools:
                estimated_tokens = len(clean_response) // 4 if clean_response else 0
                self.current_processing_tokens = estimated_tokens
                self.renderer.update_thinking(
                    True, f"Receiving... ({estimated_tokens} tokens)"
                )
                await asyncio.sleep(self.config.get("kollabor.llm.thinking_delay", 0.3))
                self.renderer.update_thinking(False)

            # Question gate: suspend tool execution if question tag present
            tool_results = None
            if all_tools:
                if self.question_gate_enabled and parsed_response.get(
                    "question_gate_active"
                ):
                    self.pending_tools = all_tools
                    self.question_gate_active = True
                    logger.info(
                        f"Question gate: suspended {len(all_tools)} tool(s) pending user response"
                    )
                else:
                    tool_count = len(all_tools)
                    tool_desc = (
                        all_tools[0].get("type", "tool")
                        if tool_count == 1
                        else f"{tool_count} tools"
                    )
                    self.renderer.update_thinking(True, f"Executing {tool_desc}...")

                    tool_results = await self.tool_executor.execute_all_tools(
                        all_tools
                    )

                    self.renderer.update_thinking(False)

            # Emit LLM_RESPONSE event
            response_context = await self.event_bus.emit_with_hooks(
                EventType.LLM_RESPONSE,
                {
                    "response_text": response,
                    "clean_response": clean_response,
                    "thinking_duration": thinking_duration,
                    "tool_results": tool_results,
                },
                "llm_service",
            )

            # Check for force_continue from plugins
            force_continue = False
            if response_context:
                for phase in ["pre", "main", "post"]:
                    phase_data = response_context.get(phase, {})
                    final_data = phase_data.get("final_data", {})
                    if final_data.get("force_continue"):
                        force_continue = True
                        break
            if force_continue:
                self.turn_completed = False
                logger.info("Plugin requested turn continuation")

            # Display response
            self.message_display_service.display_complete_response(
                thinking_duration=thinking_duration,
                response=clean_response,
                tool_results=tool_results,
                original_tools=all_tools if not self.question_gate_active else None,
            )

            # Build tool call entries for JSONL logging (XML tools have type/params)
            xml_tool_entries = None
            if all_tools:
                xml_tool_entries = [
                    {
                        "id": t.get("id", f"xml_{i}"),
                        "name": t.get("type", "unknown"),
                        "input": {k: v for k, v in t.items() if k not in ("type", "id")},
                    }
                    for i, t in enumerate(all_tools)
                ]

            # Merge thinking from XML tags and native provider reasoning
            thinking_list = parsed_response["components"].get("thinking", [])
            native_thinking = self.api_service.last_thinking_content
            if native_thinking and native_thinking not in thinking_list:
                thinking_list.append(native_thinking)

            # Log assistant response with tool calls
            parent_uuid = await self.conversation_logger.log_assistant_message(
                clean_response or response,
                parent_uuid=parent_uuid,
                model=self.api_service.model,
                usage_stats={
                    "input_tokens": self.session_stats.get("input_tokens", 0),
                    "output_tokens": self.session_stats.get("output_tokens", 0),
                    "thinking_duration": thinking_duration,
                },
                thinking_content=thinking_list or None,
                tool_calls=xml_tool_entries,
            )

            # Add to conversation history (via _add_message_fn for snapshot sync)
            self._add_message_fn(
                ConversationMessage(role="assistant", content=response),
                parent_uuid=parent_uuid,
            )

            # Log and batch tool results
            if tool_results:
                batched_tool_results = []
                for result in tool_results:
                    await self.conversation_logger.log_system_message(
                        f"Executed {result.tool_type} ({result.tool_id}): {result.output if result.success else result.error}",
                        parent_uuid=parent_uuid,
                        subtype="tool_call",
                    )

                    tool_context = self.tool_executor.format_result_for_conversation(
                        result
                    )
                    batched_tool_results.append(f"Tool result: {tool_context}")

                if batched_tool_results:
                    self.conversation_history.append(
                        ConversationMessage(
                            role="user", content="\n".join(batched_tool_results)
                        )
                    )

        except asyncio.CancelledError:
            logger.info("Message processing cancelled by user")
            thinking_duration = time.time() - thinking_start
            self.renderer.update_thinking(False)
            self.renderer.clear_active_area()

            if not self.cancellation_message_shown:
                self.cancellation_message_shown = True
                self.message_display_service.display_cancellation_message()

            self.turn_completed = True
            self.stats["total_thinking_time"] += thinking_duration

        except Exception as e:
            logger.error(f"Error processing message: {e}")
            self.renderer.update_thinking(False)
            error_msg = str(e)
            if "'str' object has no attribute 'get'" in error_msg:
                error_msg = (
                    "API format mismatch. Your profile's tool_format setting may be wrong.\n"
                    "Run /profile, press 'e' to edit, and check Tool Format matches your API."
                )
            self.message_display_service.display_error_message(error_msg)
            self.turn_completed = True

        return parent_uuid
