import asyncio
import os
import shutil
from pathlib import Path
from typing import Protocol
from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory

from mediagram.agent import Agent
from mediagram.agent.callbacks import (
    ProgressMessage,
    SuccessMessage,
    ErrorMessage,
    StartMessage,
)
from mediagram.media import MediaManager
from mediagram.config import (
    DEFAULT_MAX_TURNS,
    DEFAULT_TOOL_OUTPUT_LIMIT,
    DEFAULT_TOOL_DETAILS,
)


class InputSource(Protocol):
    """Protocol for input sources that provide user messages."""

    async def get_input(self, prompt: str) -> str | None:
        """Get next input. Returns None to signal EOF."""
        ...


class InteractiveInputSource:
    """Interactive input source using prompt_toolkit."""

    def __init__(self) -> None:
        self.session = PromptSession(history=InMemoryHistory())

    async def get_input(self, prompt: str) -> str | None:
        try:
            return await self.session.prompt_async(prompt)
        except (KeyboardInterrupt, EOFError):
            return None


class PreDefinedInputSource:
    """Pre-defined input source from a list of messages."""

    def __init__(self, messages: list[str]) -> None:
        self.messages = messages
        self.index = 0

    async def get_input(self, prompt: str) -> str | None:
        if self.index >= len(self.messages):
            return None
        message = self.messages[self.index]
        self.index += 1
        print(f"{prompt}{message}")
        return message


class CLIDriver:
    """Thin adapter layer for CLI - handles terminal I/O."""

    def __init__(
        self,
        default_model: str = "haiku",
        input_source: InputSource | None = None,
        media_dir_override: str | None = None,
        max_turns: int = DEFAULT_MAX_TURNS,
        tool_output_limit: int = DEFAULT_TOOL_OUTPUT_LIMIT,
        tool_details: bool = DEFAULT_TOOL_DETAILS,
    ):
        self.media_manager = MediaManager.create(media_dir_override)
        self.media_manager.create_subdir()
        self.agent = Agent(
            media_manager=self.media_manager,
            model_name=default_model,
            driver_callbacks=self,
            max_turns=max_turns,
            tool_output_limit=tool_output_limit,
            tool_details=tool_details,
        )
        self.input_source = input_source or InteractiveInputSource()
        self.username = os.getenv("USER", "user")
        self.name = self.username
        self.tool_details = tool_details

    async def on_tool_start(self, start: StartMessage, tool_id: str) -> None:
        """Handle tool start notification."""
        if self.tool_details:
            details = f" - args: {start.invocation_details['args']}, kwargs: {start.invocation_details['kwargs']}"
            print(f"🔧 Starting {start.tool_name}{details}")
        else:
            print(f"🔧 Starting {start.tool_name}")

    async def on_tool_progress(self, progress: ProgressMessage, tool_id: str) -> None:
        """Handle tool progress updates."""
        percentage = (
            f" ({progress.completion_ratio * 100:.0f}%)"
            if progress.completion_ratio is not None
            else ""
        )
        eta = (
            f" - ETA: {progress.completion_eta_minutes:.1f}m"
            if progress.completion_eta_minutes is not None
            else ""
        )
        print(f"🔄 {progress.text}{percentage}{eta}")

    def _format_message(self, text: str, is_success: bool) -> str:
        emoji = "✅" if is_success else "❌"
        if "\n" in text:
            indented = "\n".join(f"   {line}" for line in text.split("\n"))
            return f"{emoji}\n{indented}"
        else:
            return f"{emoji} {text}"

    async def on_tool_success(self, success: SuccessMessage, tool_id: str) -> None:
        """Handle tool success."""
        print(self._format_message(success.text, is_success=True))

    async def on_tool_error(self, error: ErrorMessage, tool_id: str) -> None:
        """Handle tool errors."""
        details = f" - {error.error}" if error.error else ""
        full_text = f"{error.text}{details}"
        print(self._format_message(full_text, is_success=False))

    def send_file(self, file_path: Path) -> str:
        """Send file to user by copying to Downloads folder."""
        downloads_dir = Path.home() / "Downloads"

        if not downloads_dir.exists():
            return "Warning: ~/Downloads directory does not exist. File not copied."

        try:
            dest_path = downloads_dir / file_path.name
            counter = 1
            while dest_path.exists():
                stem = file_path.stem
                suffix = file_path.suffix
                dest_path = downloads_dir / f"{stem}_{counter}{suffix}"
                counter += 1

            shutil.copy2(file_path, dest_path)
            file_size_kb = file_path.stat().st_size / 1024
            return f"File saved to {dest_path} ({file_size_kb:.1f}KB)"
        except Exception as e:
            return f"Error copying file: {e}"

    def _print_welcome(self) -> None:
        print(f"Mediagram CLI - Using model: {self.agent.model_name}")
        print("Type your messages or use commands:")
        print("  /help - Show all available commands")
        print("  /quit or /exit - Exit the program")
        print("  @/absolute/path/to/file - Copy a file to the current media directory")
        print()

    def _handle_file_input(self, file_path_str: str) -> bool:
        """Handle @ prefixed file input. Returns True if handled, False otherwise."""
        if not file_path_str.startswith("@"):
            return False

        source_path_str = file_path_str[1:].strip()
        if not source_path_str:
            print("Error: No file path provided after '@'")
            return True

        source_path = Path(source_path_str)

        if not source_path.is_absolute():
            print(f"Error: Path must be absolute, got: {source_path_str}")
            return True

        if not source_path.exists():
            print(f"Error: File does not exist: {source_path_str}")
            return True

        if not source_path.is_file():
            print(f"Error: Path is not a file: {source_path_str}")
            return True

        if not self.media_manager.current_subdir:
            print("Error: No active media subdirectory")
            return True

        try:
            dest_path = self.media_manager.current_subdir / source_path.name
            shutil.copy2(source_path, dest_path)
            file_size_kb = dest_path.stat().st_size / 1024
            print(
                f"✅ Copied {source_path.name} ({file_size_kb:.1f}KB) to media directory"
            )
        except Exception as e:
            print(f"❌ Error copying file: {e}")

        return True

    async def run_async(self) -> None:
        self._print_welcome()

        try:
            while True:
                try:
                    user_input = await self.input_source.get_input("You: ")

                    if user_input is None:
                        print("Goodbye!")
                        break

                    if not user_input.strip():
                        continue

                    # Handle CLI-specific exit commands
                    if user_input.strip() in ["/quit", "/exit"]:
                        print("Goodbye!")
                        break

                    # Handle @ prefix for file copying
                    if self._handle_file_input(user_input.strip()):
                        continue

                    # Let agent handle the message (commands or regular messages)
                    response = await self.agent.handle_message(
                        user_input, name=self.name, username=self.username
                    )

                    # Handle errors
                    if response.error:
                        print(f"Error: {response.error}")
                        continue

                    # Display response
                    print(f"Bot: {response.text}\n")

                except Exception as e:
                    print(f"Unexpected error: {e}")
                    break
        finally:
            self.media_manager.cleanup()

    def run(self) -> None:
        asyncio.run(self.run_async())


def run(
    model: str = "haiku",
    input_source: InputSource | None = None,
    media_dir_override: str | None = None,
    max_turns: int = DEFAULT_MAX_TURNS,
    tool_output_limit: int = DEFAULT_TOOL_OUTPUT_LIMIT,
    tool_details: bool = DEFAULT_TOOL_DETAILS,
) -> None:
    driver = CLIDriver(
        default_model=model,
        input_source=input_source,
        media_dir_override=media_dir_override,
        max_turns=max_turns,
        tool_output_limit=tool_output_limit,
        tool_details=tool_details,
    )
    driver.run()
