"""Tool output formatters for ieum.

This module provides a flexible system for formatting tool outputs in the TUI.
Each formatter handles specific tool types and provides consistent output styling.
"""

from __future__ import annotations

import ast
import json
import re
from abc import ABC, abstractmethod
from pathlib import Path, PurePosixPath
from typing import Any

# Truncation limits for display
_MAX_TODO_CONTENT_LEN = 70
_MAX_WEB_CONTENT_LEN = 100
_MAX_WEB_PREVIEW_LEN = 150


def escape_markup(text: str) -> str:
    """Escape Rich markup characters."""
    return text.replace("[", r"\[").replace("]", r"\]")


class BaseFormatter(ABC):
    """Base class for tool output formatters."""

    @abstractmethod
    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Format the tool output.

        Args:
            output: Raw output string
            is_preview: Whether this is for preview (truncated) display

        Returns:
            Formatted output string with Rich markup
        """


class DefaultFormatter(BaseFormatter):
    """Default formatter for unknown tools."""

    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Return escaped output as-is."""
        return escape_markup(output.strip())


class LinesFormatter(BaseFormatter):
    """Formatter for line-based output."""

    def __init__(self, preview_lines: int = 4):
        self.preview_lines = preview_lines

    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Format as indented lines with optional truncation."""
        lines = output.strip().split("\n")
        max_lines = self.preview_lines if is_preview else len(lines)

        formatted_lines = [escape_markup(line) for line in lines[:max_lines]]
        result = "\n".join(formatted_lines)

        if is_preview and len(lines) > max_lines:
            result += f"\n[dim]... {len(lines) - max_lines} more lines[/dim]"

        return result


class TodoFormatter(BaseFormatter):
    """Formatter for write_todos output."""

    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Format write_todos output as a checklist."""
        items = self._parse_todo_items(output)
        if items is None:
            return escape_markup(output)

        if not items:
            return "    [dim]No todos[/dim]"

        lines: list[str] = []
        max_items = 4 if is_preview else len(items)

        # Build stats header
        stats_header = self._build_todo_stats(items)
        if stats_header:
            lines.extend([f"    [dim]{stats_header}[/dim]", ""])

        # Format each item
        lines.extend(self._format_single_todo(item) for item in items[:max_items])

        if is_preview and len(items) > max_items:
            lines.append(f"    [dim]... {len(items) - max_items} more[/dim]")

        return "\n".join(lines)

    def _parse_todo_items(self, output: str) -> list | None:
        """Parse todo items from output. Returns None if parsing fails."""
        list_match = re.search(r"\[(\{.*\})\]", output.replace("\n", " "), re.DOTALL)
        if list_match:
            try:
                return ast.literal_eval("[" + list_match.group(1) + "]")
            except (ValueError, SyntaxError):
                return None
        try:
            items = ast.literal_eval(output)
            return items if isinstance(items, list) else None
        except (ValueError, SyntaxError):
            return None

    def _build_todo_stats(self, items: list) -> str:
        """Build stats string for todo list."""
        completed = sum(1 for i in items if isinstance(i, dict) and i.get("status") == "completed")
        active = sum(1 for i in items if isinstance(i, dict) and i.get("status") == "in_progress")
        pending = len(items) - completed - active

        parts = []
        if active:
            parts.append(f"[yellow]{active} active[/yellow]")
        if pending:
            parts.append(f"{pending} pending")
        if completed:
            parts.append(f"[green]{completed} done[/green]")
        return " | ".join(parts)

    def _format_single_todo(self, item: dict | str) -> str:
        """Format a single todo item."""
        if isinstance(item, dict):
            content = item.get("content", str(item))
            status = item.get("status", "pending")
        else:
            content = str(item)
            status = "pending"

        if len(content) > _MAX_TODO_CONTENT_LEN:
            content = content[: _MAX_TODO_CONTENT_LEN - 3] + "..."

        escaped = escape_markup(content)
        if status == "completed":
            return f"    [green]✓ done[/green]   [dim]{escaped}[/dim]"
        if status == "in_progress":
            return f"    [yellow]● active[/yellow] {escaped}"
        return f"    [dim]○ todo[/dim]   {escaped}"


class LsFormatter(BaseFormatter):
    """Formatter for ls/directory listing output."""

    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Format ls output as a clean directory listing."""
        try:
            items = ast.literal_eval(output)
            if isinstance(items, list):
                return self._format_list(items, is_preview=is_preview)
        except (ValueError, SyntaxError):
            pass

        # Fallback: plain lines
        return LinesFormatter(preview_lines=5).format(output, is_preview=is_preview)

    def _format_list(self, items: list, *, is_preview: bool) -> str:
        """Format a list of paths."""
        lines = []
        max_items = 5 if is_preview else len(items)

        for item in items[:max_items]:
            # Handle dict format: {'path': '/file.txt', 'is_dir': False, ...}
            if isinstance(item, dict):
                path_str = item.get("path", "")
                is_dir = item.get("is_dir", False)
            else:
                path_str = str(item)
                is_dir = path_str.endswith("/")

            # Use PurePosixPath for virtual paths (always forward slash)
            path = PurePosixPath(path_str)
            name = path.name or path_str
            suffix = path.suffix

            # Color by file type
            if suffix in (".py", ".pyx"):
                lines.append(f"    [#3b82f6]{name}[/#3b82f6]")
            elif suffix in (".md", ".txt", ".rst"):
                lines.append(f"    {name}")
            elif suffix in (".json", ".yaml", ".yml", ".toml"):
                lines.append(f"    [#f59e0b]{name}[/#f59e0b]")
            elif is_dir or suffix == "":
                display_name = name.rstrip("/") + "/"
                lines.append(f"    [#10b981]{display_name}[/#10b981]")
            else:
                lines.append(f"    {name}")

        if is_preview and len(items) > max_items:
            lines.append(f"    [dim]... {len(items) - max_items} more[/dim]")

        return "\n".join(lines)


class SearchFormatter(BaseFormatter):
    """Formatter for grep/glob search output."""

    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Format grep/glob search output."""
        try:
            items = ast.literal_eval(output.strip())
            if isinstance(items, list):
                return self._format_path_list(items, is_preview=is_preview)
        except (ValueError, SyntaxError):
            pass

        # Fallback: line-based output (grep results)
        lines = output.split("\n")
        max_lines = 5 if is_preview else len(lines)

        formatted_lines = [
            f"    {escape_markup(raw_line.strip())}"
            for raw_line in lines[:max_lines]
            if raw_line.strip()
        ]

        result = "\n".join(formatted_lines)
        if is_preview and len(lines) > max_lines:
            result += f"\n    [dim]... {len(lines) - max_lines} more[/dim]"

        return result

    def _format_path_list(self, items: list, *, is_preview: bool) -> str:
        """Format a list of paths (glob results)."""
        lines = []
        max_items = 5 if is_preview else len(items)

        for item in items[:max_items]:
            path = Path(str(item))
            try:
                rel = path.relative_to(Path.cwd())
                display = str(rel)
            except ValueError:
                display = path.name
            lines.append(f"    {display}")

        if is_preview and len(items) > max_items:
            lines.append(f"    [dim]... {len(items) - max_items} more files[/dim]")

        return "\n".join(lines)


class WebFormatter(BaseFormatter):
    """Formatter for web_search/fetch_url/http_request output."""

    def format(self, output: str, *, is_preview: bool = False) -> str:
        """Format web output."""
        data = self._try_parse_web_data(output)
        if isinstance(data, dict):
            return self._format_web_dict(data, is_preview=is_preview)

        # Fallback: plain text
        return LinesFormatter(preview_lines=4).format(output, is_preview=is_preview)

    def _try_parse_web_data(self, output: str) -> dict | None:
        """Try to parse web output as JSON or dict."""
        try:
            if output.strip().startswith("{"):
                return json.loads(output)
            return ast.literal_eval(output)
        except (ValueError, SyntaxError, json.JSONDecodeError):
            return None

    def _format_web_dict(self, data: dict, *, is_preview: bool) -> str:
        """Format a parsed web response dict."""
        # Handle web_search results
        if "results" in data:
            return self._format_web_search_results(data.get("results", []), is_preview=is_preview)

        # Handle fetch_url/http_request response
        if "markdown_content" in data:
            lines = data["markdown_content"].split("\n")
            return LinesFormatter(preview_lines=4).format("\n".join(lines), is_preview=is_preview)

        if "content" in data:
            content = str(data["content"])
            if is_preview and len(content) > _MAX_WEB_PREVIEW_LEN:
                return escape_markup(content[:_MAX_WEB_PREVIEW_LEN]) + "\n[dim]...[/dim]"
            return escape_markup(content)

        # Generic dict - show key fields
        lines = []
        max_keys = 3 if is_preview else len(data)
        for k, v in list(data.items())[:max_keys]:
            v_str = str(v)
            if is_preview and len(v_str) > _MAX_WEB_CONTENT_LEN:
                v_str = v_str[:_MAX_WEB_CONTENT_LEN] + "..."
            lines.append(f"  {k}: {escape_markup(v_str)}")
        return "\n".join(lines)

    def _format_web_search_results(self, results: list, *, is_preview: bool) -> str:
        """Format web search results."""
        if not results:
            return "[dim]No results[/dim]"
        lines = []
        max_results = 3 if is_preview else len(results)
        for r in results[:max_results]:
            title = r.get("title", "")
            url = r.get("url", "")
            lines.append(f"  [bold]{escape_markup(title)}[/bold]")
            lines.append(f"  [dim]{escape_markup(url)}[/dim]")
        if is_preview and len(results) > max_results:
            lines.append(f"  [dim]... {len(results) - max_results} more results[/dim]")
        return "\n".join(lines)


class ToolOutputFormatter:
    """Main formatter dispatcher for tool outputs.

    Provides a unified interface for formatting tool outputs based on tool type.
    """

    # Formatter registry mapping tool names to formatter instances
    _formatters: dict[str, BaseFormatter] = {
        # Todo tools
        "write_todos": TodoFormatter(),
        # Filesystem tools
        "ls": LsFormatter(),
        "read_file": LinesFormatter(preview_lines=4),
        "write_file": LinesFormatter(preview_lines=4),
        "edit_file": LinesFormatter(preview_lines=4),
        # Search tools
        "grep": SearchFormatter(),
        "glob": SearchFormatter(),
        # Shell tools
        "shell": LinesFormatter(preview_lines=4),
        "bash": LinesFormatter(preview_lines=4),
        "execute": LinesFormatter(preview_lines=4),
        # Web tools
        "web_search": WebFormatter(),
        "fetch_url": WebFormatter(),
        "http_request": WebFormatter(),
        # Agent tools
        "task": LinesFormatter(preview_lines=4),
    }

    _default_formatter = DefaultFormatter()

    @classmethod
    def format(cls, tool_name: str, output: str, *, is_preview: bool = False) -> str:
        """Format tool output based on tool type.

        Args:
            tool_name: Name of the tool
            output: Raw output string
            is_preview: Whether this is for preview (truncated) display

        Returns:
            Formatted output string with Rich markup
        """
        output = output.strip()
        if not output:
            return ""

        formatter = cls._formatters.get(tool_name, cls._default_formatter)
        return formatter.format(output, is_preview=is_preview)

    @classmethod
    def register(cls, tool_name: str, formatter: BaseFormatter) -> None:
        """Register a custom formatter for a tool.

        Args:
            tool_name: Name of the tool
            formatter: Formatter instance to use
        """
        cls._formatters[tool_name] = formatter
