"""Interactive terminal assistant for yier_agents."""

import asyncio
from contextlib import suppress
from dataclasses import dataclass
from difflib import get_close_matches
import json
from pathlib import Path
import re
import shlex
import sys
from typing import Awaitable, Callable, Sequence
from uuid import uuid4
from pydantic import BaseModel, Field

from prompt_toolkit import PromptSession
from prompt_toolkit.application.current import get_app
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.shortcuts import CompleteStyle
from prompt_toolkit.styles import Style as PromptStyle
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
from rich import box

from yier_agents import (
    Agent,
    LLM,
    Tool,
    ToolContext,
    ToolOutput,
    LLMEndEvent,
    Message,
    ToolCallStartEvent,
    ToolCallEndEvent,
    MessageCompactEvent,
    ErrorEvent,
    CompactionConfig,
    JSONSessionStore,
    Skill,
    SkillCatalog,
    create_list_files_tool,
    create_read_file_tool,
    create_replace_in_file_tool,
    create_run_command_tool,
    create_search_files_tool,
    create_write_file_tool,
    MCPManager,
)


# Initialize Rich console
console = Console()

PROMPT_STYLE = PromptStyle.from_dict(
    {
        "prompt": "bold ansicyan",
        "marker": "bold ansigreen",
        "toolbar": "bg:#1f2937 #e5e7eb",
        "toolbar.label": "bg:#1f2937 #93c5fd",
        "toolbar.value": "bg:#1f2937 #f9fafb bold",
        "toolbar.hint": "bg:#1f2937 #cbd5e1",
        "toolbar.separator": "bg:#1f2937 #4b5563",
        "rprompt": "fg:#9ca3af",
    }
)

SLASH_COMMANDS = {
    "/help": {
        "description": "Show command help",
        "usage": "/help [command]",
    },
    "/clear": {
        "description": "Clear current session memory",
        "usage": "/clear",
    },
    "/memory": {
        "description": "Inspect session memory",
        "usage": "/memory [summary|full]",
    },
    "/new": {
        "description": "Start a fresh session",
        "usage": "/new",
    },
    "/skills": {
        "description": "List or filter discovered skills",
        "usage": "/skills [query]",
    },
}
EXIT_WORDS = {"quit", "exit", "q"}
WORD_BOUNDARY_PATTERN = re.compile(r"(?:\s+)?\S+\s*$")


@dataclass(frozen=True, slots=True)
class SlashCommandSpec:
    """One slash command with description and usage metadata."""

    name: str
    description: str
    usage: str


@dataclass(slots=True)
class ChatUIState:
    """Mutable UI state for prompt chrome."""

    session_id: str
    skill_names: tuple[str, ...]
    memory_count: int = 0
    response_count: int = 0
    last_status: str = "Ready"

    @property
    def skill_count(self) -> int:
        return len(self.skill_names)


SLASH_COMMAND_SPECS = {
    name: SlashCommandSpec(name=name, **payload) for name, payload in SLASH_COMMANDS.items()
}


class SlashCommandCompleter(Completer):
    """Command-first completer that only activates in slash mode."""

    def __init__(self, skill_names: Callable[[], Sequence[str]]):
        self._skill_names = skill_names

    def get_completions(self, document: Document, complete_event):
        text = document.text_before_cursor.lstrip()
        if not text.startswith("/"):
            return

        tokens = text.split()
        current = "" if text.endswith(" ") else (tokens[-1] if tokens else "")

        if len(tokens) <= 1 and not text.endswith(" "):
            prefix = current.lower()
            for name, spec in SLASH_COMMAND_SPECS.items():
                if name.startswith(prefix):
                    yield Completion(
                        name,
                        start_position=-len(current),
                        display=name,
                        display_meta=spec.description,
                    )
            return

        command_name = tokens[0].lower() if tokens else ""
        suggestions = get_command_argument_suggestions(command_name, self._skill_names())

        for suggestion in suggestions:
            if current and not suggestion.lower().startswith(current.lower()):
                continue
            yield Completion(
                suggestion,
                start_position=-len(current),
                display=suggestion,
                display_meta=get_argument_description(command_name, suggestion),
            )


def get_ip():
    from httpx import AsyncClient

    class GetIP(BaseModel):
        pass

    async def get_ip_from_ipify():
        with suppress(Exception):
            async with AsyncClient() as client:
                resp = await client.get("http://api.ipify.org?format=json")
                ip = resp.json()["ip"]
                return ip

    async def get_ip_from_ip_api():
        with suppress(Exception):
            async with AsyncClient() as client:
                resp = await client.get("http://ip-api.com/json/?lang=zh-CN")
                ip = resp.json()["query"]
                return ip

    async def get_ip_from_ip_sb():
        with suppress(Exception):
            async with AsyncClient() as client:
                resp = await client.get("https://api.ip.sb/geoip/")
                ip = resp.json()["ip"]
                return ip

    async def get_ip_from_ip2location():
        with suppress(Exception):
            async with AsyncClient() as client:
                resp = await client.get("https://api.ip2location.io/")
                ip = resp.json()["ip"]
                return ip

    async def get_ip_from_httpbin():
        with suppress(Exception):
            async with AsyncClient() as client:
                resp = await client.get("https://httpbin.org/ip")
                ip = resp.json()["origin"]
                return ip

    async def execute(params: GetIP, ctx: ToolContext) -> ToolOutput:
        ip = None
        for item in (
            get_ip_from_ip_api(),
            get_ip_from_httpbin(),
            get_ip_from_ip_sb(),
            get_ip_from_ip2location(),
            get_ip_from_ipify(),
        ):
            ip = await item
            if ip:
                break
        return ToolOutput(content=ip or "Unable to determine the public IP address.")

    return Tool(
        name="get_ip",
        description="Get the public IP address of the local machine",
        execute=execute,
        parameters=GetIP,
    )


def print_welcome():
    """Print welcome banner."""
    welcome_text = """
    # Personal Assistant Terminal

    A calmer interactive assistant with memory, skills, and a more polished terminal UX.

    **Commands:**
    - Type a message and press `Enter` to send
    - `/clear` to clear current session memory
    - `/memory full` to inspect recent stored messages
    - `/new` to start a fresh session
    - `/skills write` to filter discovered skills
    - `/help /memory` to see command-specific help
    - `quit` or `exit` to exit
    """
    console.print(
        Panel(
            Markdown(welcome_text),
            title="[bold cyan]Welcome[/bold cyan]",
            border_style="cyan",
            box=box.DOUBLE,
        )
    )


def print_user_message(message: str):
    """Print user message panel."""
    console.print(
        Panel(
            message,
            title="[bold green]You[/bold green]",
            border_style="green",
            box=box.ROUNDED,
            padding=(0, 1),
        )
    )


def print_agent_message(message: str):
    """Print agent response panel."""
    console.print(
        Panel(
            Markdown(message),
            title="[bold blue]Assistant[/bold blue]",
            border_style="blue",
            box=box.ROUNDED,
            padding=(0, 1),
        )
    )


def print_error(error_msg: str):
    """Print error panel."""
    console.print(
        Panel(
            f"[red]{error_msg}[/red]",
            title="[bold red]Error[/bold red]",
            border_style="red",
            box=box.HEAVY,
        )
    )


def print_info(info_msg: str):
    """Print dim info line."""
    console.print(f"[dim italic]{info_msg}[/dim italic]")


def print_tool_call(event: ToolCallStartEvent):
    """Print tool call with its arguments."""
    console.print(
        f"  [bold yellow]⚙[/bold yellow]  [yellow]Tool call:[/yellow] [bold cyan]{event.tool_name}[/bold cyan]"
    )
    if event.arguments:
        items = list(event.arguments.items())
        for i, (key, value) in enumerate(items):
            branch = "└" if i == len(items) - 1 else "├"
            value_str = (
                json.dumps(value, ensure_ascii=False)
                if not isinstance(value, str)
                else f'"{value}"'
            )
            console.print(
                f"     [dim]{branch}[/dim] [cyan]{key}[/cyan] = [green]{value_str}[/green]"
            )


def print_tool_result(event: ToolCallEndEvent):
    """Print tool result."""
    if event.is_error:
        console.print(f"  [bold red]✗[/bold red]  [red]{event.result[:120]}[/red]")
    else:
        console.print(f"  [bold green]✓[/bold green]  [dim]{event.result[:120]}[/dim]")
    console.print()


def print_reasoning(content: str):
    """Print reasoning/thinking content."""
    console.print(
        Panel(
            Markdown(content),
            title="[bold yellow]💭 Thinking[/bold yellow]",
            border_style="yellow",
            box=box.ROUNDED,
            padding=(0, 1),
        )
    )
    console.print()


def get_command_argument_suggestions(
    command_name: str,
    skill_names: Sequence[str],
) -> tuple[str, ...]:
    """Return contextual suggestions for a slash command."""
    if command_name == "/help":
        return tuple(SLASH_COMMAND_SPECS)

    if command_name == "/memory":
        return ("summary", "full")

    if command_name == "/skills":
        return tuple(sorted(skill_names))

    return ()


def get_argument_description(command_name: str, argument: str) -> str:
    """Describe one completion option."""
    if command_name == "/help":
        spec = SLASH_COMMAND_SPECS.get(argument)
        return spec.description if spec else "Command"

    if command_name == "/memory":
        return "Detailed transcript preview" if argument == "full" else "Role counts and summary"

    if command_name == "/skills":
        return "Filter by skill name"

    return "Option"


def resolve_slash_command(command_name: str) -> SlashCommandSpec | None:
    """Resolve an exact or unique-prefix slash command."""
    normalized = command_name.lower()
    if normalized in SLASH_COMMAND_SPECS:
        return SLASH_COMMAND_SPECS[normalized]

    prefix_matches = [
        spec for name, spec in SLASH_COMMAND_SPECS.items() if name.startswith(normalized)
    ]
    if len(prefix_matches) == 1:
        return prefix_matches[0]

    return None


def build_input_hint(
    current_input: str,
    skill_names: Sequence[str],
    has_history_suggestion: bool,
) -> str:
    """Return the most useful hint for the current input mode."""
    stripped = current_input.lstrip()

    if not stripped:
        if has_history_suggestion:
            return "Right/Tab accepts the history suggestion. /help shows commands."
        return "Enter sends. Up recalls history. Start with / for command mode."

    if not stripped.startswith("/"):
        if has_history_suggestion:
            return "Right/Tab accepts the history suggestion. Ctrl-W deletes the previous word."
        return "Natural chat mode. Ctrl-W deletes the previous word. /help lists commands."

    command_token = stripped.split()[0].lower()
    spec = resolve_slash_command(command_token)
    if spec is not None:
        if spec.name == "/skills":
            return f"{spec.usage} | {len(skill_names)} skill(s) available for filtering."
        return f"{spec.description}. Usage: {spec.usage}"

    prefix_matches = [
        name for name in SLASH_COMMAND_SPECS if name.startswith(command_token)
    ]
    if prefix_matches:
        return f"Matching commands: {', '.join(prefix_matches)}"

    close_matches = get_close_matches(
        command_token,
        list(SLASH_COMMAND_SPECS),
        n=3,
        cutoff=0.45,
    )
    if close_matches:
        return f"Unknown command. Try: {', '.join(close_matches)}"

    return "Unknown command. Use /help to inspect the command palette."


def build_bottom_toolbar(state: ChatUIState):
    """Build the helper toolbar shown under the input."""
    buffer = get_app().current_buffer
    hint = build_input_hint(
        current_input=buffer.document.text,
        skill_names=state.skill_names,
        has_history_suggestion=bool(buffer.suggestion and buffer.document.is_cursor_at_the_end),
    )
    return [
        ("class:toolbar.label", " Session "),
        ("class:toolbar.value", state.session_id[:8]),
        ("class:toolbar.separator", " | "),
        ("class:toolbar.label", "Memory "),
        ("class:toolbar.value", str(state.memory_count)),
        ("class:toolbar.separator", " | "),
        ("class:toolbar.label", "Skills "),
        ("class:toolbar.value", str(state.skill_count)),
        ("class:toolbar.separator", " | "),
        ("class:toolbar.label", "Replies "),
        ("class:toolbar.value", str(state.response_count)),
        ("class:toolbar.separator", " | "),
        ("class:toolbar.hint", f" {hint} "),
    ]


def build_right_prompt(state: ChatUIState):
    """Build a compact status chip on the right side of the prompt."""
    stripped = get_app().current_buffer.document.text.lstrip()
    if stripped.startswith("/"):
        label = "Slash mode"
    elif get_app().current_buffer.suggestion:
        label = "History hint"
    else:
        label = state.last_status
    return [("class:rprompt", label)]


def accept_history_suggestion(buffer: Buffer) -> bool:
    """Accept the visible autosuggestion if present."""
    if buffer.suggestion and buffer.document.is_cursor_at_the_end:
        buffer.insert_text(buffer.suggestion.text)
        return True
    return False


def delete_previous_word(buffer: Buffer):
    """Delete the previous shell-style word, including trailing spaces."""
    before_cursor = buffer.document.current_line_before_cursor
    if not before_cursor:
        return

    match = WORD_BOUNDARY_PATTERN.search(before_cursor)
    if match:
        buffer.delete_before_cursor(count=len(match.group(0)))


def create_key_bindings() -> KeyBindings:
    """Create key bindings tuned for chat-style input."""
    bindings = KeyBindings()

    @bindings.add("c-space")
    def open_completion(event):
        event.app.current_buffer.start_completion(select_first=False)

    @bindings.add("tab")
    def tab_complete_or_accept(event):
        buffer = event.app.current_buffer
        if buffer.document.text.lstrip().startswith("/"):
            buffer.start_completion(select_first=True)
            return
        if accept_history_suggestion(buffer):
            return
        buffer.insert_text("    ")

    @bindings.add("right")
    def accept_suggestion_or_move(event):
        buffer = event.app.current_buffer
        if accept_history_suggestion(buffer):
            return
        buffer.cursor_right(count=1)

    @bindings.add("c-w")
    def delete_word(event):
        delete_previous_word(event.app.current_buffer)

    @bindings.add("escape", "backspace")
    def delete_word_backward(event):
        delete_previous_word(event.app.current_buffer)

    @bindings.add("c-u")
    def clear_before_cursor(event):
        buffer = event.app.current_buffer
        if buffer.document.current_line_before_cursor:
            buffer.delete_before_cursor(count=len(buffer.document.current_line_before_cursor))

    @bindings.add("c-l")
    def clear_screen(event):
        event.app.renderer.clear()

    return bindings


def create_prompt_session(
    history_path: Path,
    skill_names: Sequence[str],
) -> PromptSession[str]:
    """Create a prompt_toolkit session with persistent history."""
    history_path.parent.mkdir(parents=True, exist_ok=True)
    skill_name_supplier = lambda: tuple(skill_names)
    return PromptSession(
        history=FileHistory(str(history_path)),
        auto_suggest=AutoSuggestFromHistory(),
        completer=SlashCommandCompleter(skill_name_supplier),
        complete_while_typing=Condition(
            lambda: get_app().current_buffer.document.text.lstrip().startswith("/")
        ),
        complete_style=CompleteStyle.COLUMN,
        reserve_space_for_menu=6,
        key_bindings=create_key_bindings(),
    )


def print_help(command_name: str | None = None):
    """Show the interactive command reference."""
    if command_name:
        spec = resolve_slash_command(command_name)
        if spec is None:
            print_info(f"Unknown command '{command_name}'. Use /help to list available commands.")
            return

        help_lines = [
            f"## {spec.name}",
            spec.description,
            "",
            f"**Usage:** `{spec.usage}`",
        ]
        if spec.name == "/memory":
            help_lines.append("")
            help_lines.append("- `summary`: role counts and totals")
            help_lines.append("- `full`: preview the most recent stored messages")
        elif spec.name == "/skills":
            help_lines.append("")
            help_lines.append("- Pass a word after `/skills` to filter by name or description.")

        console.print(
            Panel(
                Markdown("\n".join(help_lines)),
                title="[bold cyan]Command Help[/bold cyan]",
                border_style="cyan",
            )
        )
        return

    help_lines = ["## Commands"]
    for spec in SLASH_COMMAND_SPECS.values():
        help_lines.append(f"- **{spec.name}**: {spec.description}  \n  Usage: `{spec.usage}`")
    help_lines.append("- **quit / exit / q**: Leave the assistant")
    console.print(
        Panel(
            Markdown("\n".join(help_lines)),
            title="[bold cyan]Help[/bold cyan]",
            border_style="cyan",
        )
    )


def summarize_message(message: Message) -> str:
    """Build a short one-line preview for a stored message."""
    if message.content:
        preview = " ".join(message.content.split())
    elif message.tool_calls:
        preview = f"{len(message.tool_calls)} tool call(s)"
    elif message.tool_call_id:
        preview = f"Tool result for {message.tool_call_id}"
    else:
        preview = "(empty)"

    if len(preview) > 120:
        return f"{preview[:117]}..."
    return preview


def print_memory_summary(messages: list[Message], view: str):
    """Render a summary or detailed preview of session memory."""
    if not messages:
        print_info("No messages in session memory yet.")
        return

    role_counts: dict[str, int] = {}
    for message in messages:
        role_counts[message.role] = role_counts.get(message.role, 0) + 1

    info_lines = [f"**Total messages:** {len(messages)}", "", "**By role:**"]
    info_lines.extend(f"- {role}: {count}" for role, count in sorted(role_counts.items()))

    if view == "full":
        info_lines.extend(["", "**Recent messages:**"])
        for message in messages[-8:]:
            info_lines.append(
                f"- **{message.role}**: {summarize_message(message)}"
            )

    console.print(
        Panel(
            Markdown("\n".join(info_lines)),
            title="[bold cyan]Session Memory[/bold cyan]",
            border_style="cyan",
        )
    )


def print_skills(skill_catalog: SkillCatalog | None, query: str | None):
    """Render discovered skills with optional filtering."""
    if skill_catalog is None or skill_catalog.is_empty():
        print_info("No skills discovered.")
        return

    normalized_query = (query or "").strip().lower()
    skills = skill_catalog.list()
    if normalized_query:
        skills = [
            skill
            for skill in skills
            if normalized_query in skill.name.lower()
            or normalized_query in skill.description.lower()
        ]

    if not skills:
        print_info(f"No skills matched '{query}'.")
        return

    skill_lines = ["## Discovered Skills"]
    for skill in skills:
        skill_lines.append(
            f"- **{skill.name}**: {skill.description}\n  {skill.location}"
        )

    console.print(
        Panel(
            Markdown("\n".join(skill_lines)),
            title=f"[bold cyan]Skills ({len(skills)})[/bold cyan]",
            border_style="cyan",
        )
    )


def update_memory_count(agent: Agent, ui_state: ChatUIState):
    """Sync the UI state with stored session memory."""
    messages = agent.get_session_messages(ui_state.session_id) or []
    ui_state.memory_count = len(messages)


def handle_slash_command(agent: Agent, ui_state: ChatUIState, user_input: str):
    """Handle slash command input and update UI state as needed."""
    try:
        parts = shlex.split(user_input.strip())
    except ValueError as exc:
        print_error(f"Unable to parse command: {exc}")
        ui_state.last_status = "Command error"
        return

    if not parts:
        print_info("Please enter a slash command.")
        return

    spec = resolve_slash_command(parts[0])
    if spec is None:
        suggestions = get_close_matches(
            parts[0].lower(),
            list(SLASH_COMMAND_SPECS),
            n=3,
            cutoff=0.45,
        )
        if suggestions:
            print_info(
                f"Unknown command '{parts[0]}'. Try: {', '.join(suggestions)}"
            )
        else:
            print_info(f"Unknown command '{parts[0]}'. Use /help.")
        ui_state.last_status = "Unknown command"
        return

    args = parts[1:]

    if spec.name == "/help":
        print_help(args[0] if args else None)
        ui_state.last_status = "Help shown"
        return

    if spec.name == "/clear":
        agent.clear_session(ui_state.session_id)
        ui_state.memory_count = 0
        ui_state.response_count = 0
        ui_state.last_status = "Memory cleared"
        console.print(
            Panel("[green]✓[/green] Session memory cleared!", border_style="green")
        )
        return

    if spec.name == "/memory":
        view = args[0].lower() if args else "summary"
        if view not in {"summary", "full"}:
            print_info("Use `/memory summary` or `/memory full`.")
            ui_state.last_status = "Memory usage"
            return
        messages = agent.get_session_messages(ui_state.session_id) or []
        print_memory_summary(messages, view=view)
        ui_state.last_status = "Memory shown"
        return

    if spec.name == "/new":
        ui_state.session_id = str(uuid4())
        ui_state.memory_count = 0
        ui_state.response_count = 0
        ui_state.last_status = "New session"
        console.print(
            Panel(
                f"[green]✓[/green] Started a new session: [bold]{ui_state.session_id[:8]}[/bold]",
                border_style="green",
            )
        )
        return

    if spec.name == "/skills":
        query = " ".join(args) if args else None
        print_skills(agent.skill_catalog, query=query)
        ui_state.last_status = "Skills shown"
        return


async def prompt_for_input(
    session: PromptSession[str],
    ui_state: ChatUIState,
) -> str | None:
    """Read one user message with prompt_toolkit."""
    return await session.prompt_async(
        [
            ("class:prompt", "assistant"),
            ("class:marker", " > "),
        ],
        style=PROMPT_STYLE,
        bottom_toolbar=lambda: build_bottom_toolbar(ui_state),
        rprompt=lambda: build_right_prompt(ui_state),
    )


async def chat_loop(
    agent: Agent,
    ui_state: ChatUIState,
    prompt_session: PromptSession[str],
    sync_agent: Callable[[Agent], Awaitable[Agent]] | None = None,
):
    """Main chat loop using streaming events."""
    while True:
        try:
            console.print()
            if sync_agent is not None:
                agent = await sync_agent(agent)
            update_memory_count(agent, ui_state)
            user_input = await prompt_for_input(prompt_session, ui_state=ui_state)
            normalized_input = (user_input or "").strip().lower()

            if user_input is None or normalized_input in EXIT_WORDS:
                console.print()
                console.print(
                    Panel(
                        "[cyan]Thank you for using AI Agent Terminal! Goodbye! 👋[/cyan]",
                        border_style="cyan",
                        box=box.DOUBLE,
                    )
                )
                break

            if not user_input.strip():
                print_info("Please enter a message.")
                ui_state.last_status = "Waiting"
                continue

            if user_input.lstrip().startswith("/"):
                handle_slash_command(agent, ui_state, user_input)
                continue

            # Display user message
            ui_state.last_status = "Thinking"
            console.print()
            print_user_message(user_input)
            console.print()

            # Stream agent events
            try:
                with console.status("[dim]Thinking...[/dim]", spinner="dots") as status:
                    async for event in agent.run_stream(user_input, ui_state.session_id):
                        if isinstance(event, ToolCallStartEvent):
                            ui_state.last_status = f"Tool {event.tool_name}"
                            status.update(f"[dim]Running {event.tool_name}...[/dim]")
                            status.stop()
                            print_tool_call(event)
                            status.start()

                        elif isinstance(event, ToolCallEndEvent):
                            ui_state.last_status = "Thinking"
                            status.update("[dim]Thinking...[/dim]")
                            status.stop()
                            print_tool_result(event)
                            status.start()

                        elif isinstance(event, LLMEndEvent):
                            if event.message.reasoning_content:
                                status.stop()
                                print_reasoning(event.message.reasoning_content)
                                status.start()

                            if event.finish_reason == "stop":
                                status.stop()
                                if event.message.content:
                                    print_agent_message(event.message.content)
                                    ui_state.response_count += 1
                                    ui_state.last_status = "Ready"
                                else:
                                    ui_state.last_status = "No reply"
                                    print_info("No response received from agent.")
                            else:
                                ui_state.last_status = event.finish_reason

                        elif isinstance(event, MessageCompactEvent):
                            ui_state.last_status = "Compacting"
                            status.update(
                                f"[dim]Compacting messages ({event.original_count} → {event.compacted_count})...[/dim]"
                            )

                        elif isinstance(event, ErrorEvent):
                            ui_state.last_status = "Error"
                            status.stop()
                            print_error(f"{event.error_type}: {event.error_message}")
                            status.start()

            except Exception as e:
                ui_state.last_status = "Failed"
                print_error(f"Failed to get response: {str(e)}")
                continue

            update_memory_count(agent, ui_state)

        except KeyboardInterrupt:
            console.print()
            console.print(
                "[yellow]Interrupted. Your current prompt was cancelled.[/yellow]"
            )
            ui_state.last_status = "Cancelled"
            continue
        except EOFError:
            console.print()
            console.print(
                Panel("[cyan]Session ended. Goodbye! 👋[/cyan]", border_style="cyan")
            )
            break
        except Exception as e:
            ui_state.last_status = "Error"
            print_error(f"Unexpected error: {str(e)}")
            continue


def build_assistant_agent(
    llm: LLM,
    workspace_tools: list[Tool],
    mcp_tools: list[Tool],
    network_skill: Skill,
    skill_catalog: SkillCatalog,
    storage_path: Path,
) -> Agent:
    """Create the interactive agent with the latest MCP tool snapshot."""
    return Agent(
        llm=llm,
        tools=[*workspace_tools, *mcp_tools],
        skills=[network_skill],
        skill_catalog=skill_catalog,
        max_iterations=100,
        system_prompt=(
            "You are a thoughtful personal assistant. Help with planning, writing, "
            "summaries, decisions, research guidance, and everyday organization. "
            "Be calm, practical, and concise."
        ),
        verbose=False,
        enable_memory=True,
        session_store=JSONSessionStore(storage_path),
        compaction_config=CompactionConfig(
            enabled=True,
            trigger_message_count=24,
            preserve_recent_messages=8,
            summary_max_tokens=512,
        ),
    )


async def main():
    """Run the interactive assistant CLI."""
    console.clear()
    print_welcome()
    workspace_root = Path.cwd()
    storage_path = workspace_root / ".agent_storage" / "sessions"
    prompt_history_path = workspace_root / ".agent_storage" / "prompt_history.txt"
    skill_catalog = SkillCatalog.discover(
        workspace_root,
        include_project=True,
        include_global=True,
    )
    allowed_roots = [workspace_root, storage_path, *skill_catalog.dirs()]
    workspace_tools = [
        create_list_files_tool(allowed_roots),
        create_read_file_tool(allowed_roots),
        create_replace_in_file_tool(allowed_roots),
        create_run_command_tool(allowed_roots),
        create_write_file_tool(allowed_roots),
        create_search_files_tool(allowed_roots),
    ]

    async with MCPManager() as mcp_manager:
        network_skill = Skill(
            name="network-basics",
            description="Basic network utilities for the local machine.",
            tools=[get_ip()],
            system_prompt=(
                "When the user asks about the current machine's public network identity, "
                "use the get_ip tool instead of guessing."
            ),
        )

        with console.status(
            "[bold cyan]Initializing AI Agent...[/bold cyan]", spinner="dots"
        ):
            try:
                llm = LLM()
                agent = build_assistant_agent(
                    llm=llm,
                    workspace_tools=workspace_tools,
                    mcp_tools=await mcp_manager.get_tools(),
                    network_skill=network_skill,
                    skill_catalog=skill_catalog,
                    storage_path=storage_path,
                )
                session_id = str(uuid4())
            except Exception as e:
                print_error(f"Failed to initialize agent: {str(e)}")
                sys.exit(1)

        console.print()
        if skill_catalog.is_empty():
            skill_message = "[yellow]![/yellow] No skills discovered."
        else:
            skill_message = (
                f"[green]✓[/green] Discovered {len(skill_catalog.list())} skill(s). "
                "Use /skills to inspect them."
            )
        console.print(
            Panel(
                "\n".join(
                    [
                        "[green]✓[/green] Agent initialized successfully! Ready to chat!",
                        skill_message,
                    ]
                ),
                border_style="green",
            )
        )

        skill_names = tuple(skill.name for skill in skill_catalog.list())
        ui_state = ChatUIState(session_id=session_id, skill_names=skill_names)
        prompt_session = create_prompt_session(prompt_history_path, skill_names)
        known_mcp_version = mcp_manager.version

        async def sync_agent(current_agent: Agent, current_version: int) -> tuple[Agent, int]:
            await mcp_manager.reload_if_changed()
            if mcp_manager.version == current_version:
                return current_agent, current_version

            refreshed_agent = build_assistant_agent(
                llm=llm,
                workspace_tools=workspace_tools,
                mcp_tools=await mcp_manager.get_tools(),
                network_skill=network_skill,
                skill_catalog=skill_catalog,
                storage_path=storage_path,
            )
            print_info("Reloaded MCP server configuration.")
            return refreshed_agent, mcp_manager.version

        async def sync_agent_for_chat(current_agent: Agent) -> Agent:
            nonlocal known_mcp_version
            refreshed_agent, known_mcp_version = await sync_agent(
                current_agent,
                known_mcp_version,
            )
            return refreshed_agent

        await chat_loop(
            agent,
            ui_state,
            prompt_session,
            sync_agent=sync_agent_for_chat,
        )


def run() -> None:
    """Synchronous entry point for the packaged assistant CLI."""
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        console.print()
        console.print("[yellow]Program terminated by user.[/yellow]")
        sys.exit(0)


if __name__ == "__main__":
    run()
