from __future__ import annotations

import asyncio
import json
import os
import re
import shlex
import time

import typer
from loguru import logger
from rich.console import Console
from rich.markup import escape

from ragops_agent_ce.schemas.agent_schemas import AgentSettings

try:
    import readline

    # Advanced readline configuration for better UX
    readline.parse_and_bind("tab: complete")
    readline.parse_and_bind("set editing-mode emacs")
    readline.parse_and_bind("set completion-ignore-case on")
    readline.parse_and_bind("set show-all-if-ambiguous on")
    readline.parse_and_bind("set menu-complete-display-prefix on")

    # History management
    history_file = os.path.expanduser("~/.ragops_history")
    try:
        readline.read_history_file(history_file)
        readline.set_history_length(1000)
    except Exception:
        logger.warning("Failed to load readline history")
        pass

    # Save history on exit
    import atexit

    atexit.register(readline.write_history_file, history_file)

    READLINE_AVAILABLE = True
except ImportError:
    READLINE_AVAILABLE = False

from . import __version__
from .agent.agent import LLMAgent
from .agent.agent import default_tools
from .agent.prompts import OPENAI_SYSTEM_PROMPT
from .agent.prompts import VERTEX_SYSTEM_PROMPT
from .checklist_manager import ChecklistWatcherWithRenderer
from .checklist_manager import active_checklist
from .checklist_manager import get_active_checklist_text
from .config import load_settings
from .display import ScreenRenderer
from .interactive_input import get_user_input
from .llm.provider_factory import PROVIDER_PATHS
from .llm.provider_factory import get_provider
from .llm.types import Message
from .logging_config import setup_logging
from .mcp.client import MCPClient
from .model_selector import save_model_selection
from .model_selector import select_model_at_startup
from .prints import RAGOPS_LOGO_ART
from .prints import RAGOPS_LOGO_TEXT
from .setup_wizard import run_setup_if_needed

app = typer.Typer(
    pretty_exceptions_enable=False,
)
console = Console()


def version_callback(value: bool) -> None:
    if value:
        console.print(f"ragops-agent-ce {__version__}")
        raise typer.Exit(code=0)


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    setup: bool = typer.Option(
        False,
        "--setup",
        help="Run setup wizard to configure the agent",
    ),
    system: str | None = typer.Option(
        None, "--system", "-s", help="System prompt to guide the agent"
    ),
    model: str | None = typer.Option(
        None, "--model", "-m", help="LLM model to use (overrides settings)"
    ),
    provider: str | None = typer.Option(
        None, "--provider", "-p", help="LLM provider to use (overrides .env settings)"
    ),
    show_checklist: bool = typer.Option(
        True,
        "--show-checklist/--no-checklist",
        help="Render checklist panel at start and after each step",
    ),
) -> None:
    """RagOps Agent CE - LLM-powered CLI agent for building RAG pipelines."""
    # Setup logging according to .env / settings
    try:
        setup_logging(load_settings())
    except Exception:
        # Don't break CLI if logging setup fails
        pass

    # If no subcommand is provided, run the REPL
    if ctx.invoked_subcommand is None:
        # Run setup wizard if needed or forced
        if not run_setup_if_needed(force=setup):
            raise typer.Exit(code=1)

        # If --setup flag was used, exit after setup
        if setup:
            raise typer.Exit(code=0)

        # Model selection at startup (mandatory)
        # Only skip if provider is explicitly provided via CLI flag
        if provider is None:
            model_selection = select_model_at_startup()
            if model_selection is None:
                console.print("[red]Model selection cancelled. Exiting.[/red]")
                raise typer.Exit(code=1)
            provider, model_from_selection = model_selection
            # Override model from selection if not provided via CLI
            if model is None:
                model = model_from_selection
        else:
            # Provider provided via CLI, save it to KV database as latest
            save_model_selection(provider, model)

        asyncio.run(
            _astart_repl(
                system=system or VERTEX_SYSTEM_PROMPT
                if provider == "vertex" or provider == "vertexai"
                else OPENAI_SYSTEM_PROMPT,
                model=model,
                provider=provider,
                mcp_commands=DEFAULT_MCP_COMMANDS,
                mcp_only=False,
                show_checklist=show_checklist,
            )
        )


@app.command()
def ping() -> None:
    """Simple health command to verify the CLI is working."""
    console.print("pong")


DEFAULT_MCP_COMMANDS = [
    "ragops-compose-manager",
    "ragops-rag-planner",
    "ragops-read-engine",
    "ragops-chunker",
    "ragops-vectorstore-loader",
    "ragops-checklist",
    "ragops-rag-query",
]


def _time_str() -> str:
    """Get current time string for transcript."""
    return "[dim]" + time.strftime("[%H:%M]", time.localtime()) + "[/]"


def _render_markdown_to_rich(text: str) -> str:
    """Convert markdown text to simple Rich markup without breaking formatting."""
    # Simple markdown to rich markup conversion without full rendering
    # This preserves the transcript panel formatting

    # Bold: **text** -> [bold]text[/bold]
    result = re.sub(r"\*\*(.+?)\*\*", r"[bold]\1[/bold]", text)

    # Italic: *text* -> [italic]text[/italic] (but don't match list items)
    result = re.sub(r"(?<!\*)\*([^\*\n]+?)\*(?!\*)", r"[italic]\1[/italic]", result)

    # Inline code: `text` -> [cyan]text[/cyan]
    result = re.sub(r"`(.+?)`", r"[cyan]\1[/cyan]", result)

    # Headers: ## text -> [bold cyan]text[/bold cyan]
    result = re.sub(r"^#+\s+(.+)$", r"[bold cyan]\1[/bold cyan]", result, flags=re.MULTILINE)

    # List items: - text or * text -> • text (with proper indentation preserved)
    result = re.sub(r"^(\s*)[*-]\s+", r"\1• ", result, flags=re.MULTILINE)

    # Numbered lists: 1. text -> 1. text (keep as is)

    return result


async def _astart_repl(
    *,
    system: str | None,
    model: str | None,
    provider: str | None,
    mcp_commands: list[str] | None,
    mcp_only: bool,
    show_checklist: bool,
) -> None:
    console.print(RAGOPS_LOGO_TEXT)
    console.print(RAGOPS_LOGO_ART)

    settings = load_settings()
    if provider:
        os.environ.setdefault("RAGOPS_LLM_PROVIDER", provider)
        settings = settings.model_copy(update={"llm_provider": provider})

    # Try to get provider and validate credentials
    try:
        prov = get_provider(settings, llm_provider=provider)
    except (ValueError, FileNotFoundError, json.JSONDecodeError) as e:
        console.print(f"[red]Error initializing provider '{provider}':[/red] {e}")
        console.print("[yellow]Please ensure credentials are configured correctly.[/yellow]")
        raise typer.Exit(code=1)

    tools = [] if mcp_only else default_tools()

    session_started_at = time.time()

    history: list[Message] = []
    if system:
        history.append(Message(role="system", content=system))

    # Transcript buffer must be available for helper functions below
    transcript: list[str] = []
    agent_settings = AgentSettings(llm_provider=prov, model=model)
    renderer = ScreenRenderer()

    # Progress state for MCP tools
    progress_line_index: int | None = None

    def mcp_progress_callback(progress: float, total: float | None, message: str | None) -> None:
        """Callback for MCP tool progress updates."""
        nonlocal progress_line_index

        if total is not None:
            percentage = (progress / total) * 100
            progress_text = f"[dim]⏳ Progress: {percentage:.1f}% - {message or ''}[/dim]"
        else:
            progress_text = f"[dim]⏳ Progress: {progress} - {message or ''}[/dim]"

        if progress_line_index is None:
            # First progress update - add new line
            transcript.append(progress_text)
            progress_line_index = len(transcript) - 1
        else:
            # Update existing progress line
            transcript[progress_line_index] = progress_text

        # Re-render to show progress
        cl_text = _get_session_checklist() if show_checklist else None
        renderer.render_project(
            transcript, cl_text, agent_settings=agent_settings, show_input_space=False
        )

    # Create MCP clients with progress callback
    mcp_clients = []
    commands = mcp_commands if mcp_commands is not None else []
    if commands:
        for cmd_str in commands:
            cmd_parts = shlex.split(cmd_str)
            logger.debug(f"Starting MCP client: {cmd_parts}")
            mcp_clients.append(
                MCPClient(cmd_parts[0], cmd_parts[1:], progress_callback=mcp_progress_callback)
            )

    # Helpers for rendering and transcript updates
    def _render_current_screen(show_input_space: bool) -> None:
        cl_text = _get_session_checklist()
        renderer.render_project(
            transcript, cl_text, agent_settings=agent_settings, show_input_space=show_input_space
        )

    def _append_user_line(text: str) -> None:
        transcript.append(f"\n\n{_time_str()} [bold blue]you>[/bold blue] {escape(text)}")

    def _start_agent_placeholder() -> int:
        transcript.append(f"\n{_time_str()} [bold green]RagOps Agent>[/bold green] ")
        return len(transcript) - 1

    def _set_agent_line(index: int, display_content: str, temp_executing: str) -> None:
        transcript[index] = (
            f"\n{_time_str()} [bold green]RagOps Agent>[/bold green] {display_content}{temp_executing}"  # noqa
        )

    # Formatting helpers for tool execution messages
    def _tool_executing_message(tool_name: str, tool_args: dict | None) -> str:
        args_str = ", ".join(tool_args.keys()) if tool_args else ""
        return f"\n[dim]🔧 Executing tool:[/dim] [yellow]{escape(tool_name)}[/yellow]({args_str})"

    def _tool_done_message(tool_name: str) -> str:
        return f"\n[dim]✓ Tool:[/dim] [green]{escape(tool_name)}[/green]\n"

    def _tool_error_message(tool_name: str, error: str) -> str:
        return f"\n[dim]✗ Tool failed:[/dim] [red]{escape(tool_name)}[/red] - {escape(error)}\n"

    # Stream event handler: returns updated (reply, display_content, temp_executing)
    def _process_stream_event(
        event, reply: str, display_content: str, temp_executing: str
    ) -> tuple[str, str, str]:
        nonlocal progress_line_index
        et = getattr(event, "type", None)
        if et == "content":
            content_chunk = event.content or ""
            reply = reply + content_chunk
            display_content = display_content + content_chunk
            return reply, display_content, temp_executing
        if et == "tool_call_start":
            return reply, display_content, _tool_executing_message(event.tool_name, event.tool_args)
        if et == "tool_call_end":
            if getattr(event, "tool_name", None) == "get_checklist":
                # Refresh checklist after get_checklist tool
                # Safely parse tool result - it might be JSON or plain text error message
                try:
                    tool_result = history[-1].content or "{}"
                    checklist_content = json.loads(tool_result)
                    if checklist_content.get("name"):
                        active_checklist.name = checklist_content.get("name") + ".json"
                except (json.JSONDecodeError, ValueError):
                    # Tool returned plain text (e.g., "Checklist '...' not found."), not JSON
                    # Skip checklist update in this case - checklist doesn't exist yet
                    pass
                cl_text = _get_session_checklist()
                renderer.render_project(
                    transcript,
                    cl_text,
                    agent_settings=agent_settings,
                    show_input_space=False,
                )
            if getattr(event, "tool_name", None) in ("create_checklist", "update_checklist_item"):
                # Refresh checklist after create/update operations
                try:
                    tool_result = history[-1].content or "{}"
                    checklist_content = json.loads(tool_result)
                    if checklist_content.get("name"):
                        active_checklist.name = checklist_content.get("name") + ".json"
                except (json.JSONDecodeError, ValueError):
                    # Tool returned plain text error, skip checklist update
                    pass
                cl_text = _get_session_checklist()
                renderer.render_project(
                    transcript,
                    cl_text,
                    agent_settings=agent_settings,
                    show_input_space=False,
                )
            # Remove progress line and reset for next tool execution
            if progress_line_index is not None:
                transcript.pop(progress_line_index)
            progress_line_index = None
            return reply, display_content + _tool_done_message(event.tool_name), ""
        if et == "tool_call_error":
            # Remove progress line and reset for next tool execution
            if progress_line_index is not None:
                transcript.pop(progress_line_index)
            progress_line_index = None
            return (
                reply,
                display_content + _tool_error_message(event.tool_name, event.error or ""),
                "",
            )
        return reply, display_content, temp_executing

    # Sanitize transcript from any legacy checklist lines (we now render checklist separately)
    def _sanitize_transcript(trans: list[str]) -> None:
        markers = {
            "[dim]--- Checklist Created ---[/dim]",
        }
        # Remove any lines that exactly match known markers or start with the checklist header
        i = 0
        while i < len(trans):
            line = trans[i].strip()
            if line in markers or line.startswith("[white on blue]"):  # checklist header style
                trans.pop(i)
                # Do not increment i, continue checking at same index after pop
                continue
            i += 1

    def _get_session_checklist() -> str | None:
        return get_active_checklist_text(session_started_at)

    # Create agent
    agent = LLMAgent(
        prov,
        tools=tools,
        mcp_clients=mcp_clients,
    )
    # Initialize MCP tools asynchronously
    await agent._ainit_mcp_tools()
    renderer.render_startup_screen()

    # Render welcome message as markdown
    welcome_msg = (
        "Hello! I'm **Donkit - RagOps Agent**, your assistant for building RAG pipelines. "
        "How can I help you today?"
    )
    rendered_welcome = _render_markdown_to_rich(welcome_msg)
    transcript.append(f"{_time_str()} [bold green]RagOps Agent>[/bold green] {rendered_welcome}")
    watcher = None
    if show_checklist:
        watcher = ChecklistWatcherWithRenderer(
            transcript,
            agent_settings,
            renderer,
            session_start_mtime=session_started_at,
        )
        watcher.start()

    while True:
        try:
            _render_current_screen(show_input_space=True)
            user_input = get_user_input()
        except (EOFError, KeyboardInterrupt):
            transcript.append("[Exiting REPL]")
            if watcher:
                watcher.stop()
            _sanitize_transcript(transcript)
            _render_current_screen(show_input_space=False)
            renderer.render_goodbye_screen()
            break

        if not user_input:
            continue

        if user_input == ":help":
            transcript += [
                "",
                "  [yellow]Available commands:[/yellow]",
                "  [bold]:help[/bold] - Show this help message",
                "  [bold]:q[/bold] or [bold]:quit[/bold] - Exit the agent",
                "  [bold]:clear[/bold] - Clear the conversation transcript",
                "  [bold]:agent [cyan]<llm_provider>/<model>[/cyan][/bold] - "
                "Change agent LLM provider and model",
            ]
            continue

        if user_input == ":clear":
            transcript = []
            continue

        if user_input.startswith(":agent "):
            _render_current_screen(show_input_space=False)
            parts = user_input[len(":agent ") :].strip().split("/", 1)
            if len(parts) != 2:
                transcript.append("[bold red]Error:[/bold red] Invalid agent command format.")
                continue
            new_provider, new_model = parts
            if new_provider not in PROVIDER_PATHS:
                transcript.append(
                    f"[bold red]Error:[/bold red] Unknown provider '{new_provider}'. "
                    f"Available providers: {', '.join(PROVIDER_PATHS.keys())}."
                )
                continue
            os.environ["RAGOPS_LLM_PROVIDER"] = new_provider
            settings = settings.model_copy(update={"llm_provider": new_provider})
            prov = get_provider(settings)
            agent_settings.llm_provider = prov
            agent_settings.model = new_model
            model = new_model
            agent = LLMAgent(
                prov,
                tools=tools,
                mcp_clients=mcp_clients,
            )
            await agent._ainit_mcp_tools()
            transcript.append(
                "[bold cyan]Agent updated:[/bold cyan] "
                f"Provider set to [yellow]{new_provider}[/yellow], "
                f"Model set to [yellow]{new_model}[/yellow]."
            )
            _render_current_screen(show_input_space=True)
            continue

        if user_input in {":q", ":quit", ":exit", "exit", "quit"}:
            transcript.append("[Bye]")
            if watcher:
                watcher.stop()
            _sanitize_transcript(transcript)
            _render_current_screen(show_input_space=False)
            renderer.render_goodbye_screen()
            break

        _append_user_line(user_input)
        _sanitize_transcript(transcript)
        _render_current_screen(show_input_space=False)

        try:
            history.append(Message(role="user", content=user_input))
            # Use streaming if provider supports it
            if prov.supports_streaming():
                reply = ""
                interrupted = False

                # Add placeholder to transcript
                response_index = _start_agent_placeholder()

                try:
                    # Accumulate everything in display order
                    display_content = ""
                    # Temporary message shown only during execution
                    temp_executing = ""

                    # Stream events - process content and tool calls
                    async for event in agent.arespond_stream(history, model=model):
                        reply, display_content, temp_executing = _process_stream_event(
                            event, reply, display_content, temp_executing
                        )

                        # Update transcript with permanent content + temporary executing message
                        _set_agent_line(response_index, display_content, temp_executing)

                        # Re-render screen after each event
                        _render_current_screen(show_input_space=False)
                except KeyboardInterrupt:
                    interrupted = True
                    transcript[response_index] = (
                        f"{_time_str()} [yellow]⚠ Generation interrupted by user[/yellow]"
                    )

                # Add to history if we got a response
                if reply and not interrupted:
                    history.append(Message(role="assistant", content=reply))
            else:
                # Fall back to non-streaming mode
                # Add placeholder
                response_index = _start_agent_placeholder()

                try:
                    reply = await agent.arespond(history, model=model)
                    history.append(Message(role="assistant", content=reply))

                    # Replace placeholder with actual response
                    rendered_reply = _render_markdown_to_rich(reply)
                    _set_agent_line(response_index, rendered_reply, "")
                except KeyboardInterrupt:
                    console.print("\n[yellow]⚠ Generation interrupted by user[/yellow]")
                    transcript[response_index] = (
                        f"{_time_str()} [yellow]Generation interrupted[/yellow]"
                    )
        except Exception as e:
            transcript.append(f"{_time_str()} [bold red]Error:[/bold red] {str(e)}")
