"""
Chat Controller

Controller for chat interactions with LLM providers.

v2.0 Updates:
- MCP tool call event handling with verbose mode support
- Ctrl+C cancellation with graceful interruption
"""

import asyncio
from typing import Any, Callable

from honeydb_ai.domain.exceptions import LLMRequestCancelledError
from honeydb_ai.domain.models import Session
from honeydb_ai.infrastructure.ui import ThinkingIndicator
from honeydb_ai.infrastructure.ui.console import RichConsoleUI
from honeydb_ai.services.llm_service import LLMService
from honeydb_ai.services.session_service import SessionService
from honeydb_ai.utils.logging import get_logger


class ChatController:
    """
    Controller for chat interactions with MCP integration.

    v2.0: Includes MCP tool call visibility and async task management for Ctrl+C cancellation.
    """

    def __init__(
        self,
        llm_service: LLMService,
        session_service: SessionService,
        console: RichConsoleUI,
        verbose_tool_calls: bool = False,
        config_controller: Any | None = None,
        session_controller: Any | None = None,
    ):
        """
        Initialize chat controller.

        Args:
            llm_service: LLM service instance
            session_service: Session service instance
            console: Console UI instance
            verbose_tool_calls: Whether to display MCP tool call events
            config_controller: Optional config controller for /config commands
            session_controller: Optional session controller for session commands
        """
        self.llm_service = llm_service
        self.session_service = session_service
        self.console = console
        self.verbose_tool_calls = verbose_tool_calls
        self.config_controller = config_controller
        self.session_controller = session_controller
        self.logger = get_logger()
        self.current_session: Session | None = None
        self.current_llm_task: asyncio.Task | None = None

    def set_session(self, session: Session) -> None:
        """
        Set the current active session.

        Args:
            session: Session to use for chat
        """
        self.current_session = session
        self.logger.info(f"Active session set to: {session.id}")

    def get_session(self) -> Session:
        """
        Get the current active session.

        Returns:
            Current session

        Raises:
            RuntimeError: If no session is active
        """
        if self.current_session is None:
            # Create a new session if none exists
            self.current_session = self.session_service.create_session()
            self.logger.info("Created new session automatically")

        return self.current_session

    async def send_message(self, message: str, auto_save: bool = True) -> str | None:
        """
        Send a message to the LLM and get a response.

        v2.0: Supports MCP tool call visibility and Ctrl+C cancellation without data loss.

        Args:
            message: User message to send
            auto_save: Whether to auto-save the session after response

        Returns:
            Assistant's response, or None if cancelled

        Raises:
            Various LLM exceptions (see LLMService.send_message)
        """
        session = self.get_session()

        # Add user message to session
        session.add_message("user", message)

        # Instantiate the indicator here to keep UI setup close to the LLM call and
        # before defining the event callback for clearer control-flow readability.
        # Rich's Status/Live handles concurrent console.print() calls natively — no
        # pause/resume coordination is needed from this class.
        indicator = ThinkingIndicator(
            console=self.console.console,
            unicode_support=self.console.capabilities["unicode_support"],
            is_tty=self.console.capabilities["is_tty"],
        )

        # Create event callback for MCP tool call visibility
        def event_callback(event_type: str, data: dict[str, Any]) -> None:
            """
            Handle MCP tool invocation events.

            Only displays events if verbose_tool_calls is enabled.
            Rich's live display handles interleaving with console.print() natively.
            """
            if not self.verbose_tool_calls:
                return

            if event_type == "tool_call":
                tool_name = data.get("tool_name", "unknown")
                self.console.print(f"🔧 Tool Call: {tool_name}", style="dim")
            elif event_type == "tool_result":
                tool_name = data.get("tool_name", "unknown")
                success = data.get("success", True)
                status = "✅" if success else "❌"
                self.console.print(f"{status} Tool Result: {tool_name}", style="dim")

        try:
            async with indicator:
                # Create async task for LLM request.
                # Store task reference so it can be cancelled on Ctrl+C.
                self.current_llm_task = asyncio.create_task(
                    self.llm_service.send_message(
                        messages=session.messages,
                        event_callback=event_callback,
                    )
                )
                # Wait for response — indicator animates concurrently
                response = await self.current_llm_task

            # Indicator is fully cleared before any output is printed
            session.add_message("assistant", response.content)

            if auto_save:
                self.session_service.save_session(session)

            return response.content

        except LLMRequestCancelledError:
            # Indicator's __aexit__ has already run; safe to print now
            self.console.print_warning("Request cancelled by user")

            # Remove the last user message (since no response was received)
            if session.messages and session.messages[-1].role == "user":
                session.messages.pop()

            if auto_save:
                self.session_service.save_session(session)

            return None

        except KeyboardInterrupt:
            # Indicator's __aexit__ has already run; safe to print now
            self.console.print_warning("Request cancelled by user")

            # Cancel the current task if it's still running
            if self.current_llm_task and not self.current_llm_task.done():
                self.current_llm_task.cancel()
                try:
                    await self.current_llm_task
                except asyncio.CancelledError:
                    pass

            # Remove the last user message
            if session.messages and session.messages[-1].role == "user":
                session.messages.pop()

            if auto_save:
                self.session_service.save_session(session)

            return None

        finally:
            self.current_llm_task = None

    async def run_chat_loop(self, prompt_func: Callable[[], str] | None = None, prompt_async_func: Callable[[], str] | None = None) -> None:
        """
        Run the interactive chat loop.

        v2.0 Critical Fix #2: Async loop with proper Ctrl+C handling.

        Args:
            prompt_func: Synchronous function to get user input (deprecated, for compatibility)
            prompt_async_func: Async function to get user input (preferred)
        """
        # Clear screen before starting chat
        self.console.clear()

        self.console.print_banner()

        # Display session info under banner
        session = self.get_session()
        self.console.print(f"Session: {session.title} ({session.id})", style="info")
        self.console.print("Type /exit or /quit to exit.", style="dim")
        self.console.print_divider()

        while True:
            try:
                # Get user input (prefer async version)
                if prompt_async_func:
                    user_input = await prompt_async_func()
                elif prompt_func:
                    user_input = prompt_func()
                else:
                    raise ValueError("Either prompt_func or prompt_async_func must be provided")

                # Check for exit commands
                if user_input and user_input.strip() in ["/exit", "/quit"]:
                    break

                # Hint if user types exit/quit without slash
                if user_input and user_input.strip().lower() in ["exit", "quit"]:
                    self.console.print("To exit, use /exit or /quit.", style="dim")
                    continue

                # Check for empty input - just continue to next prompt
                if not user_input or not user_input.strip():
                    continue

                # Handle slash commands
                if user_input.strip().startswith("/"):
                    command_handled = await self._handle_command(user_input.strip())
                    if command_handled:
                        continue
                    # If command not handled, show error
                    self.console.print_warning(f"Unknown command: {user_input.strip()}")
                    self.console.print("Type /help for available commands.", style="dim")
                    continue

                # Send message and get response
                response = await self.send_message(user_input)

                # Display response
                if response:
                    self.console.print_divider()
                    self.console.print_markdown(response)
                    self.console.print_divider()

            except KeyboardInterrupt:
                # Ctrl+C during input or response
                self.console.print_warning("\nUse /exit or /quit to exit the chat")
                continue

            except Exception as e:
                self.console.print_error(f"Error: {e}")
                self.logger.error(f"Chat loop error: {e}", exc_info=True)
                continue

        self.console.print("Chat session ended.", style="success")

    def clear_history(self) -> None:
        """
        Clear the current session's message history.

        Note: Does not save automatically.
        """
        session = self.get_session()
        session.messages.clear()
        self.logger.info("Session history cleared")

    def get_history(self) -> list[dict]:
        """
        Get the current session's message history.

        Returns:
            List of message dicts
        """
        session = self.get_session()
        return [
            {
                "role": msg.role,
                "content": msg.content,
                "timestamp": msg.timestamp.isoformat(),
            }
            for msg in session.messages
        ]

    def display_history(self, limit: int | None = None) -> None:
        """
        Display the current session's message history.

        Args:
            limit: Optional limit on number of messages to display
        """
        session = self.get_session()
        messages = session.messages[-limit:] if limit else session.messages

        if not messages:
            self.console.print("No messages in current session.", style="dim")
            return

        self.console.print(f"Session: {session.title}", style="info")
        self.console.print_divider()

        for msg in messages:
            # Display role
            role_style = "user" if msg.role == "user" else "assistant"
            self.console.print(f"\n[{role_style}]{msg.role.upper()}[/{role_style}]")

            # Display content
            if msg.role == "assistant":
                self.console.print_markdown(msg.content)
            else:
                self.console.print(msg.content)

            self.console.print_divider()

    def _get_available_models(self) -> dict[str, list[tuple[str, str]]]:
        """Get available models for all providers."""
        return {
            "openai": [
                ("gpt-5.4-pro", "GPT-5.4 Pro (recommended)"),
                ("gpt-5.4", "GPT-5.4"),
                ("gpt-5-mini", "GPT-5 Mini"),
            ],
            "anthropic": [
                ("claude-opus-4-6", "Claude Opus 4.6 (recommended)"),
                ("claude-sonnet-4-6", "Claude Sonnet 4.6"),
                ("claude-haiku-4-5-20251001", "Claude Haiku 4.5"),
            ],
            "gemini": [
                ("gemini-3.1-pro-preview-customtools", "Gemini 3.1 Pro — Custom Tools (recommended)"),
                ("gemini-3.1-pro-preview", "Gemini 3.1 Pro"),
                ("gemini-3.1-flash-preview", "Gemini 3.1 Flash"),
                ("gemini-3.1-flash-lite", "Gemini 3.1 Flash Lite"),
            ],
        }

    def _show_model_info(self) -> None:
        """Display current model configuration and session stats."""
        config = self.llm_service.config

        # Display current config
        info = [
            f"Provider:     {config.default_provider}",
            f"Model:        {config.default_model}",
            f"Temperature:  {config.temperature}",
            f"Max Tokens:   {config.max_tokens or 'Not set'}",
        ]

        self.console.print_panel(
            "\n".join(info),
            title="Current Model Configuration",
            style="info"
        )

        # Display session stats
        if self.current_session:
            msg_count = len(self.current_session.messages)
            # Calculate approximate tokens (rough estimate: ~4 chars per token)
            total_chars = sum(len(m.content) for m in self.current_session.messages)
            approx_tokens = total_chars // 4

            self.console.print("\nSession Stats:", style="dim")
            self.console.print(f"  Messages: {msg_count}", style="dim")
            self.console.print(f"  Total tokens (approx): {approx_tokens:,}", style="dim")

        # Display available models
        self.console.print("\nAvailable Models:", style="info")
        all_models = self._get_available_models()

        for provider, models in all_models.items():
            # Check if provider has API key
            has_key = self._has_api_key(provider)
            key_status = "✓" if has_key else "✗"

            self.console.print(f"\n  {provider.capitalize()} {key_status}", style="dim" if not has_key else "info")
            for model_id, description in models:
                # Highlight current model
                if provider == config.default_provider and model_id == config.default_model:
                    self.console.print(f"    • {description} [current]", style="success")
                else:
                    self.console.print(f"    • {description}", style="dim")

        self.console.print("\nType '/model change' to switch models.", style="dim")

    def _show_model_help(self) -> None:
        """Display /model command help."""
        help_text = """
/model Commands:

  /model              Show current model and session stats
  /model change       Interactively change model and provider
  /model help         Show this help message

Examples:
  > /model
  > /model change
        """

        self.console.print(help_text, style="info")

    async def _change_model_interactive(self) -> None:
        """Interactive model selection with API key handling."""

        # Save current config for rollback
        original_provider = self.llm_service.config.default_provider
        original_model = self.llm_service.config.default_model

        try:
            # Step 1: Select provider
            provider = self._select_provider()
            if not provider:
                self.console.print_warning("Model change cancelled.")
                return

            # Step 2: Check API key
            if not self._has_api_key(provider):
                if not self._prompt_for_api_key(provider):
                    self.console.print_warning("Model change cancelled.")
                    return

            # Step 3: Select model
            model = self._select_model(provider)
            if not model:
                self.console.print_warning("Model change cancelled.")
                return

            # Step 4: Apply changes
            self._apply_model_change(provider, model)

            self.console.print_success(
                f"✓ Model changed: {original_provider} {original_model} → {provider} {model}"
            )
            self.console.print_success("✓ Configuration saved")

        except KeyboardInterrupt:
            self.console.print_warning(
                f"\n✗ Model change cancelled. Continuing with {original_provider} {original_model}."
            )
        except Exception as e:
            self.console.print_error(f"Failed to change model: {e}")
            self.logger.error(f"Model change error: {e}", exc_info=True)
            # Rollback
            try:
                self._apply_model_change(original_provider, original_model)
                self.console.print_warning(f"Rolled back to {original_provider} {original_model}")
            except Exception:
                pass

    def _select_provider(self) -> str | None:
        """Display provider menu and get selection."""
        config = self.llm_service.config

        providers = {
            "1": ("openai", config.openai_api_key),
            "2": ("anthropic", config.anthropic_api_key),
            "3": ("gemini", config.gemini_api_key),
        }

        current = config.default_provider

        self.console.print("\nSelect LLM Provider:", style="info")
        for key, (name, api_key) in providers.items():
            status = "✓" if api_key else "✗ API key not set"
            current_marker = " (current)" if name == current else ""
            self.console.print(f"  {key}. {name.capitalize()}{current_marker} {status}")

        choice = self.console.prompt("\nChoice [1-3]", default="")

        if not choice or choice not in providers:
            return None

        return providers[choice][0]

    def _has_api_key(self, provider: str) -> bool:
        """Check if provider has API key configured."""
        config = self.llm_service.config

        key_map = {
            "openai": config.openai_api_key,
            "anthropic": config.anthropic_api_key,
            "gemini": config.gemini_api_key,
        }

        return bool(key_map.get(provider))

    def _prompt_for_api_key(self, provider: str) -> bool:
        """Prompt user to configure API key."""
        self.console.print_warning(f"\n⚠ Warning: {provider.capitalize()} API key not configured")

        configure = self.console.confirm(
            "Would you like to configure it now?",
            default=False
        )

        if not configure:
            return False

        # Get API key URLs
        urls = {
            "openai": "https://platform.openai.com/api-keys",
            "anthropic": "https://console.anthropic.com/",
            "gemini": "https://makersuite.google.com/app/apikey",
        }

        self.console.print(f"\nGet your API key at: {urls.get(provider, '')}", style="dim")

        api_key = self.console.prompt(f"\nEnter your {provider.capitalize()} API key", default="")

        if not api_key or not api_key.strip():
            self.console.print_warning("No API key provided")
            return False

        # Save API key
        try:
            from honeydb_ai.cli.app import get_config_service
            from honeydb_ai.controllers.config_controller import ConfigController

            config_service = get_config_service()
            config_controller = ConfigController(config_service, self.console)
            config_controller.set_api_key(provider, api_key.strip())

            # Reload config in LLM service
            self.llm_service.config = config_service.load_config()
            # Reinitialize adapters with new API key
            self.llm_service._initialize_adapters()

            return True

        except Exception as e:
            self.console.print_error(f"Failed to save API key: {e}")
            self.logger.error(f"API key save error: {e}", exc_info=True)
            return False

    def _select_model(self, provider: str) -> str | None:
        """Display model menu for provider and get selection."""

        # Get models from centralized list
        all_models = self._get_available_models()
        provider_models = all_models.get(provider, [])

        if not provider_models:
            self.console.print_error(f"No models configured for provider: {provider}")
            return None

        self.console.print("\nSelect Model:", style="info")
        for idx, (model_id, description) in enumerate(provider_models, 1):
            self.console.print(f"  {idx}. {description}")

        choice = self.console.prompt(f"\nChoice [1-{len(provider_models)}]", default="")

        if not choice:
            return None

        try:
            idx = int(choice) - 1
            if 0 <= idx < len(provider_models):
                return provider_models[idx][0]
        except ValueError:
            pass

        self.console.print_warning("Invalid choice")
        return None

    def _apply_model_change(self, provider: str, model: str) -> None:
        """Apply model change to config and LLM service."""
        from honeydb_ai.cli.app import get_config_service

        config_service = get_config_service()

        # Update config
        config = config_service.load_config()
        config.default_provider = provider  # type: ignore
        config.default_model = model
        config_service.save_config(config)

        # Reload in LLM service
        self.llm_service.config = config
        # Reinitialize adapters to use new provider/model
        self.llm_service._initialize_adapters()

    async def _handle_command(self, command: str) -> bool:
        """
        Route slash commands to appropriate handlers.

        Args:
            command: The command string (including leading /)

        Returns:
            True if command was handled, False otherwise
        """
        # Parse command and arguments
        parts = command.split(maxsplit=1)
        cmd = parts[0].lower()
        args = parts[1] if len(parts) > 1 else ""

        # Route to handlers
        if cmd == "/help":
            self._handle_help_command()
            return True

        elif cmd == "/clear":
            self._handle_clear_command()
            return True

        elif cmd == "/config":
            self._handle_config_command(args)
            return True

        elif cmd == "/reload-config":
            self._handle_reload_config_command()
            return True

        elif cmd == "/history":
            self._handle_history_command()
            return True

        elif cmd == "/new":
            self._handle_new_session_command()
            return True

        elif cmd == "/save":
            self._handle_save_command()
            return True

        elif cmd == "/load":
            self._handle_load_command(args)
            return True

        elif cmd == "/list":
            self._handle_list_command()
            return True

        elif cmd == "/delete":
            self._handle_delete_command(args)
            return True

        elif cmd == "/export":
            self._handle_export_command(args)
            return True

        elif cmd == "/provider":
            await self._change_model_interactive()
            return True

        elif cmd == "/model":
            if args == "change":
                await self._change_model_interactive()
            elif args == "help":
                self._show_model_help()
            else:
                self._show_model_info()
            return True

        # Command not recognized
        return False

    def _handle_help_command(self) -> None:
        """Display help message with available commands."""
        help_text = """
Available Commands:

  /help               Show this help message
  /clear              Clear the screen
  /exit, /quit        Exit the chat session

  Session Management:
  /new                Start a new chat session
  /save               Save the current session
  /load [id]          Load a specific session
  /list               List all saved sessions
  /delete [id]        Delete a session
  /export [id]        Export session to file
  /history            Show message history for current session

  Configuration:
  /config             Show current configuration
  /config show        Show current configuration (same as /config)
  /reload-config      Reload configuration from file

  Model Management:
  /model              Show current model and session stats
  /model change       Change model and provider
  /model help         Show model command help
  /provider           Change LLM provider (same as /model change)

Type your message and press Enter to chat with the AI.
        """
        self.console.print(help_text, style="info")

    def _handle_clear_command(self) -> None:
        """Clear the screen."""
        self.console.clear()
        self.console.print_banner()
        session = self.get_session()
        self.console.print(f"Session: {session.title} ({session.id})", style="info")
        self.console.print("Type /help for available commands.", style="dim")
        self.console.print_divider()

    def _handle_config_command(self, args: str = "") -> None:
        """Handle /config commands."""
        if not self.config_controller:
            self.console.print_warning("Config controller not available")
            return

        # /config or /config show
        if not args or args == "show":
            config = self.config_controller.load_config()
            self.config_controller.display_config(config)
        else:
            self.console.print_warning(f"Unknown config command: {args}")
            self.console.print("Use '/config' or '/config show' to display configuration.", style="dim")

    def _handle_reload_config_command(self) -> None:
        """Reload configuration from file."""
        try:
            from honeydb_ai.cli.app import get_config_service

            config_service = get_config_service()
            config = config_service.load_config()

            # Reload in LLM service
            self.llm_service.config = config
            self.llm_service._initialize_adapters()

            self.console.print_success("✓ Configuration reloaded")

        except Exception as e:
            self.console.print_error(f"Failed to reload configuration: {e}")
            self.logger.error(f"Config reload error: {e}", exc_info=True)

    def _handle_history_command(self) -> None:
        """Display message history for current session."""
        self.display_history()

    def _handle_new_session_command(self) -> None:
        """Start a new chat session."""
        if not self.session_controller:
            self.console.print_warning("Session controller not available")
            return

        try:
            # Save current session if it has messages
            if self.current_session and self.current_session.messages:
                self.session_service.save_session(self.current_session)

            # Create new session
            new_session = self.session_controller.create_session()
            self.set_session(new_session)

            self.console.print_success(f"✓ Started new session: {new_session.title} ({new_session.id})")

        except Exception as e:
            self.console.print_error(f"Failed to create new session: {e}")
            self.logger.error(f"New session error: {e}", exc_info=True)

    def _handle_save_command(self) -> None:
        """Save the current session."""
        try:
            session = self.get_session()
            self.session_service.save_session(session)
            self.console.print_success(f"✓ Session saved: {session.title} ({session.id})")

        except Exception as e:
            self.console.print_error(f"Failed to save session: {e}")
            self.logger.error(f"Save session error: {e}", exc_info=True)

    def _handle_load_command(self, session_id: str = "") -> None:
        """Load a specific session."""
        if not self.session_controller:
            self.console.print_warning("Session controller not available")
            return

        if not session_id:
            self.console.print_warning("Please provide a session ID")
            self.console.print("Usage: /load <session-id>", style="dim")
            self.console.print("Use '/list' to see available sessions.", style="dim")
            return

        try:
            session = self.session_controller.load_session(session_id)
            self.set_session(session)
            self.console.print_success(f"✓ Loaded session: {session.title} ({session.id})")
            self.console.print(f"Messages: {len(session.messages)}", style="dim")

        except Exception as e:
            self.console.print_error(f"Failed to load session: {e}")
            self.logger.error(f"Load session error: {e}", exc_info=True)

    def _handle_list_command(self) -> None:
        """List all saved sessions."""
        if not self.session_controller:
            self.console.print_warning("Session controller not available")
            return

        try:
            self.session_controller.list_sessions()

        except Exception as e:
            self.console.print_error(f"Failed to list sessions: {e}")
            self.logger.error(f"List sessions error: {e}", exc_info=True)

    def _handle_delete_command(self, session_id: str = "") -> None:
        """Delete a specific session."""
        if not self.session_controller:
            self.console.print_warning("Session controller not available")
            return

        if not session_id:
            self.console.print_warning("Please provide a session ID")
            self.console.print("Usage: /delete <session-id>", style="dim")
            self.console.print("Use '/list' to see available sessions.", style="dim")
            return

        try:
            # Don't allow deleting current session
            if self.current_session and self.current_session.id == session_id:
                self.console.print_warning("Cannot delete the current active session")
                self.console.print("Use '/new' to start a new session first.", style="dim")
                return

            self.session_controller.delete_session(session_id)
            self.console.print_success(f"✓ Deleted session: {session_id}")

        except Exception as e:
            self.console.print_error(f"Failed to delete session: {e}")
            self.logger.error(f"Delete session error: {e}", exc_info=True)

    def _handle_export_command(self, session_id: str = "") -> None:
        """Export a session to file."""
        if not self.session_controller:
            self.console.print_warning("Session controller not available")
            return

        # Use current session if no ID provided
        if not session_id:
            if self.current_session:
                session_id = self.current_session.id
            else:
                self.console.print_warning("No session specified and no current session")
                self.console.print("Usage: /export <session-id>", style="dim")
                return

        try:
            export_path = self.session_controller.export_session(session_id)
            self.console.print_success(f"✓ Session exported to: {export_path}")

        except Exception as e:
            self.console.print_error(f"Failed to export session: {e}")
            self.logger.error(f"Export session error: {e}", exc_info=True)

    def _handle_provider_command(self) -> None:
        """Change LLM provider (alias for /model change)."""
        asyncio.create_task(self._change_model_interactive())
