"""Fliiq TUI Application — Claude Code-style full-screen interface."""

import asyncio
import subprocess
import time
from pathlib import Path

import structlog
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container
from textual.widgets import Input

from fliiq.cli.tui.widgets import (
    CompletionMessage,
    HeaderWidget,
    Message,
    MessageLog,
    ModeInput,
    PlanApprovalSelector,
)
from textual.widgets import OptionList

from fliiq.runtime.agent.config import AgentConfig, next_mode
from fliiq.runtime.agent.loop import AgentResult, CancellationToken, agent_loop, plan_was_approved
from fliiq.runtime.agent.prompt import assemble_agent_prompt
from fliiq.runtime.agent.tools import ToolRegistry
from fliiq.runtime.llm.providers import BaseLLM
from fliiq.runtime.planning.domain_detector import detect_domain
from fliiq.runtime.planning.playbook_loader import load_playbook
from fliiq import __version__
_log = structlog.get_logger()


class FliiqApp(App):
    """Full-screen TUI for Fliiq with Claude Code-style layout."""

    CSS_PATH = "styles.tcss"

    BINDINGS = [
        Binding("ctrl+c", "quit", "Quit", show=False),
        Binding("shift+tab", "cycle_mode", "Cycle Mode", show=False),
        Binding("escape", "focus_input", "Focus Input", show=False),
    ]

    def __init__(
        self,
        llm: BaseLLM,
        tools: ToolRegistry,
        soul: str,
        user_soul: str | None,
        skill_info: list[dict],
        memory_context: str | None,
        project_root: Path,
        mode: str = "autonomous",
        user_profile: dict | None = None,
        fliiq_email: str | None = None,
    ) -> None:
        super().__init__()
        self.llm = llm
        self.tools = tools
        self.soul = soul
        self.user_soul = user_soul
        self.skill_info = skill_info
        self.memory_context = memory_context
        self.project_root = project_root
        self.mode = mode
        self.user_profile = user_profile
        self.fliiq_email = fliiq_email
        self.messages: list[dict] = []
        self.iteration_total = 0
        self.thinking_widget: Message | None = None
        self.thinking_start: float = 0
        self.approve_all = False
        self._pending_input: asyncio.Future | None = None
        self._cancellation: CancellationToken | None = None
        self._last_assistant_text: str = ""
        self._last_user_prompt: str = ""

    def compose(self) -> ComposeResult:
        """Build the UI layout."""
        with Container(id="app-grid"):
            yield HeaderWidget(__version__, str(self.project_root))
            yield MessageLog(id="message-log")
            yield ModeInput(id="mode-input")

    def on_mount(self) -> None:
        """Focus input on startup and re-register builtins with TUI handlers."""
        self.query_one(ModeInput).mode = self.mode
        self.query_one(ModeInput).focus_input()
        from fliiq.runtime.update_check import check_update_cached
        notice = check_update_cached()
        if notice:
            self.query_one(MessageLog).add_message(notice, role="tool")
        # Re-register builtins with async TUI handlers (fixes ask_user hanging in TUI)
        self.tools.register_builtins(
            ask_user_handler=self._tui_ask_user,
            ask_user_choice_handler=self._tui_ask_user_choice,
            plan_complete_handler=self._tui_plan_complete,
            mode=self.mode,
        )

    def action_quit(self) -> None:
        """Exit the app."""
        self.exit()

    def action_cycle_mode(self) -> None:
        """Cycle through modes: plan -> supervised -> autonomous."""
        self.mode = next_mode(self.mode)
        self.tools._mode = self.mode
        self.query_one(ModeInput).mode = self.mode
        msg_log = self.query_one(MessageLog)
        msg_log.add_message(f"Mode: {self.mode}", role="tool")

    def action_focus_input(self) -> None:
        """ESC handler: cancel running agent, or focus input when idle."""
        if self._cancellation is not None:
            self._cancellation.cancel()
            msg_log = self.query_one(MessageLog)
            msg_log.add_message("Cancelling...", role="tool")
            # Also cancel any pending input future
            if self._pending_input is not None and not self._pending_input.done():
                self._pending_input.cancel()
        else:
            self.query_one(ModeInput).focus_input()

    async def on_input_submitted(self, event: Input.Submitted) -> None:
        """Handle user input submission."""
        user_input = event.value.strip()
        event.input.value = ""

        if not user_input:
            return

        # If a tool is waiting for input, resolve the future instead of sending to agent
        if self._pending_input is not None and not self._pending_input.done():
            self._pending_input.set_result(user_input)
            return

        # Handle slash commands
        if user_input.startswith("/"):
            await self._handle_command(user_input)
            return

        # Regular message — send to agent
        log = self.query_one(MessageLog)
        log.add_user_message(user_input)

        # Start thinking indicator
        self.thinking_start = time.time()
        self.thinking_widget = log.add_thinking("0s")

        # Run agent in background
        self.run_worker(self._run_agent(user_input), exclusive=True)

    async def _handle_command(self, command: str) -> None:
        """Process slash commands."""
        log = self.query_one(MessageLog)
        cmd = command.lower().split()[0]

        if cmd == "/help":
            help_text = """Commands:
/help   — Show this help
/mode   — Cycle mode (plan -> supervised -> autonomous)
/status — Show session status
/clear  — Clear conversation history
/copy   — Copy last response to clipboard
/exit   — Exit"""
            log.add_message(help_text, role="assistant")

        elif cmd == "/mode":
            self.action_cycle_mode()

        elif cmd == "/status":
            status = f"""Mode: {self.mode}
Messages: {len(self.messages)}
Iterations: {self.iteration_total}"""
            log.add_message(status, role="assistant")

        elif cmd == "/clear":
            self.messages = []
            self.iteration_total = 0
            self.approve_all = False
            log.clear_messages()
            log.add_message("Conversation cleared.", role="tool")

        elif cmd == "/copy":
            if self._last_assistant_text:
                try:
                    subprocess.run(
                        ["pbcopy"], input=self._last_assistant_text.encode(), check=True,
                    )
                    log.add_message("Copied last response to clipboard.", role="tool")
                except FileNotFoundError:
                    try:
                        subprocess.run(
                            ["xclip", "-selection", "clipboard"],
                            input=self._last_assistant_text.encode(), check=True,
                        )
                        log.add_message("Copied last response to clipboard.", role="tool")
                    except FileNotFoundError:
                        log.add_message("No clipboard tool found (pbcopy/xclip).", role="tool")
            else:
                log.add_message("No assistant response to copy.", role="tool")

        elif cmd == "/exit":
            self.exit()

        else:
            log.add_message(f"Unknown command: {cmd}. Type /help for available commands.", role="tool")

    async def _wait_for_input(self) -> str:
        """Create a Future and wait for user input (resolved by on_input_submitted or selectors)."""
        loop = asyncio.get_running_loop()
        self._pending_input = loop.create_future()
        try:
            return await self._pending_input
        finally:
            self._pending_input = None

    async def _prompt_user_input(self, prompt_text: str) -> str:
        """Show a prompt in the message log and wait for user input via the input widget."""
        msg_log = self.query_one(MessageLog)
        msg_log.add_message(prompt_text, role="tool")
        return await self._wait_for_input()

    async def _tui_ask_user(self, question: str) -> str:
        """TUI handler for ask_user — uses async Future-based input."""
        return await self._prompt_user_input(f"Agent asks: {question}")

    async def _tui_plan_complete(self, summary: str, options: list[dict], recommended: int) -> str:
        """TUI handler for plan_complete — shows summary + arrow-key option selector."""
        msg_log = self.query_one(MessageLog)
        msg_log.add_plan_approval(summary)

        selector = PlanApprovalSelector(options, recommended)
        msg_log.mount(selector)
        msg_log.scroll_end(animate=False)
        selector.focus()

        try:
            answer = await self._wait_for_input()
        except asyncio.CancelledError:
            return "rejected"

        if answer == "__custom__":
            feedback = await self._prompt_user_input("Type your response:")
            return f"revise: {feedback}"
        return answer

    async def _tui_ask_user_choice(self, question: str, options: list[dict], recommended: int) -> str:
        """TUI handler for ask_user_choice — presents numbered options."""
        lines = [f"Agent asks: {question}"]
        for i, opt in enumerate(options, 1):
            rec = " (recommended)" if i == recommended else ""
            lines.append(f"  [{i}] {opt['label']}{rec} — {opt['description']}")
        lines.append("  [0] Custom answer")
        prompt_text = "\n".join(lines)

        answer = await self._prompt_user_input(prompt_text)
        try:
            choice = int(answer)
            if choice == 0:
                custom = await self._prompt_user_input("Enter your custom answer:")
                return f"User chose: {custom}"
            if 1 <= choice <= len(options):
                picked = options[choice - 1]
                return f"User chose: {picked['label']} — {picked['description']}"
        except ValueError:
            pass
        return f"User chose: {answer}"

    def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
        """Handle selection from PlanApprovalSelector."""
        if not isinstance(event.option_list, PlanApprovalSelector):
            return
        selector = event.option_list
        idx = event.option_index
        # "Type your own" is the last option (index == len(option_data))
        if idx == len(selector.option_data):
            result = "__custom__"
        else:
            opt = selector.option_data[idx]
            if opt.get("action") == "execute":
                result = "approved"
            else:
                result = f"User chose: {opt['label']} — {opt['description']}"
        if self._pending_input is not None and not self._pending_input.done():
            self._pending_input.set_result(result)
        selector.remove()
        self.query_one(ModeInput).focus_input()

    async def _run_agent(self, user_input: str) -> None:
        """Run the agent loop and update UI."""
        self.messages.append({"role": "user", "content": user_input})
        self._last_user_prompt = user_input

        # Snapshot mode — safe even if user cycles mid-run
        run_mode = self.mode

        # Filter tool definitions for the LLM based on mode
        BUILTIN_TOOLS = {"todo", "ask_user", "ask_user_choice", "plan_complete"}
        all_defs = self.tools.get_tool_definitions()
        if run_mode == "plan":
            tool_defs_override = [t for t in all_defs if t["name"] in BUILTIN_TOOLS]
        else:
            tool_defs_override = [t for t in all_defs if t["name"] != "plan_complete"]

        # Filter skill_info in system prompt to match
        effective_skill_info = self.skill_info if run_mode != "plan" else None

        # Build system prompt with domain detection
        playbook_content = None
        domains = detect_domain(user_input, working_dir=self.project_root, project_root=self.project_root)
        for domain in domains:
            pb = load_playbook(domain, project_root=self.project_root)
            if pb:
                playbook_content = pb
                break

        system = assemble_agent_prompt(
            soul=self.soul,
            user_soul=self.user_soul,
            playbook=playbook_content,
            skills=effective_skill_info if effective_skill_info else None,
            mode=run_mode,
            clarifications=None,
            memory_context=self.memory_context,
            user_profile=self.user_profile,
            fliiq_email=self.fliiq_email,
        )

        config = AgentConfig(mode=run_mode)
        msg_log = self.query_one(MessageLog)
        self._cancellation = CancellationToken()
        self.query_one(ModeInput).agent_running = True

        def on_tool_call(name: str, tool_input: dict, result: str) -> None:
            """Called after each tool execution."""
            if self.thinking_widget:
                elapsed = self._format_elapsed(time.time() - self.thinking_start)
                self.thinking_widget.update(f"~(o.o)~ Thinking... {elapsed}")
            msg_log.add_tool_call(name, result)
            # Refresh skill_info after a successful skill installation
            if name == "install_skill" and "installed successfully" in str(result):
                self.skill_info = [
                    {"name": d["name"], "description": d["description"]}
                    for d in self.tools.get_tool_definitions()
                ]

        # Build approval callback based on mode
        if run_mode == "supervised":
            async def on_before_tool_call(name: str, tool_input: dict) -> bool:
                """Supervised mode — async approval via TUI input."""
                if self.approve_all:
                    return True
                params = "\n".join(f"  {k}: {str(v)[:100]}" for k, v in tool_input.items())
                prompt = f"Run tool '{name}'?\n{params}\n\\[y]es / \\[n]o / \\[a]pprove all"
                answer = await self._prompt_user_input(prompt)
                answer = answer.strip().lower()
                if answer in ("a", "all"):
                    self.approve_all = True
                    return True
                return answer in ("y", "yes", "")
        elif run_mode == "plan":
            async def on_before_tool_call(name: str, tool_input: dict) -> bool:
                """Plan mode safety net — only allow builtins."""
                return name in BUILTIN_TOOLS
        else:
            on_before_tool_call = None  # Autonomous — no blocking

        try:
            result = await agent_loop(
                llm=self.llm,
                messages=self.messages,
                tools=self.tools,
                config=config,
                system=system,
                on_tool_call=on_tool_call,
                on_before_tool_call=on_before_tool_call,
                cancellation=self._cancellation,
                tool_defs_override=tool_defs_override,
                tool_skip_message=(
                    "BLOCKED: tool unavailable in plan mode. "
                    "Use only: todo, ask_user, ask_user_choice, plan_complete."
                    if run_mode == "plan" else "Tool skipped by user"
                ),
            )

            self.messages = result.messages
            self.iteration_total += result.iterations

            # Remove thinking indicator
            if self.thinking_widget:
                self.thinking_widget.remove()
                self.thinking_widget = None

            # Show response
            iters = result.iterations
            iter_label = f"{iters} iteration{'s' if iters != 1 else ''}"

            if result.stop_reason == "cancelled":
                msg_log.add_message("Cancelled.", role="tool")
            elif result.final_text:
                self._last_assistant_text = result.final_text
                msg_log.add_completion_message(result.final_text, iter_label)
            else:
                msg_log.add_completion_message("Done.", iter_label)

            # Plan mode: auto-continue in supervised mode on approval
            if run_mode == "plan" and plan_was_approved(result):
                self.mode = "supervised"
                self.tools._mode = self.mode
                self.tools._tools.pop("plan_complete", None)
                self.query_one(ModeInput).mode = self.mode
                msg_log.add_message("Plan approved. Executing...", role="tool")

                # Auto-continue in supervised mode
                self.messages.append({"role": "user", "content": "Plan approved. Proceed with execution."})

                system = assemble_agent_prompt(
                    soul=self.soul,
                    user_soul=self.user_soul,
                    playbook=playbook_content,
                    skills=self.skill_info if self.skill_info else None,
                    mode="supervised",
                    memory_context=self.memory_context,
                    user_profile=self.user_profile,
                    fliiq_email=self.fliiq_email,
                )
                config = AgentConfig(mode="supervised")

                # Reset thinking indicator
                self.thinking_start = time.time()
                self.thinking_widget = msg_log.add_thinking("0s")

                # Supervised approval callback
                async def on_before_supervised(name: str, tool_input: dict) -> bool:
                    if self.approve_all:
                        return True
                    params = "\n".join(f"  {k}: {str(v)[:100]}" for k, v in tool_input.items())
                    prompt = f"Run tool '{name}'?\n{params}\n\\[y]es / \\[n]o / \\[a]pprove all"
                    answer = await self._prompt_user_input(prompt)
                    answer = answer.strip().lower()
                    if answer in ("a", "all"):
                        self.approve_all = True
                        return True
                    return answer in ("y", "yes", "")

                result = await agent_loop(
                    llm=self.llm,
                    messages=self.messages,
                    tools=self.tools,
                    config=config,
                    system=system,
                    on_tool_call=on_tool_call,
                    on_before_tool_call=on_before_supervised,
                    cancellation=self._cancellation,
                )

                self.messages = result.messages
                self.iteration_total += result.iterations

                # Remove thinking + show result
                if self.thinking_widget:
                    self.thinking_widget.remove()
                    self.thinking_widget = None

                iters = result.iterations
                iter_label = f"{iters} iteration{'s' if iters != 1 else ''}"
                if result.stop_reason == "cancelled":
                    msg_log.add_message("Cancelled.", role="tool")
                elif result.final_text:
                    self._last_assistant_text = result.final_text
                    msg_log.add_completion_message(result.final_text, iter_label)
                else:
                    msg_log.add_completion_message("Done.", iter_label)

        except Exception as e:
            if self.thinking_widget:
                self.thinking_widget.remove()
                self.thinking_widget = None
            msg_log.add_message(f"Error: {e}", role="tool")
        finally:
            self._cancellation = None
            self.query_one(ModeInput).agent_running = False

    def _format_elapsed(self, seconds: float) -> str:
        """Format elapsed time as '12s' or '1m 12s'."""
        if seconds < 60:
            return f"{int(seconds)}s"
        minutes = int(seconds // 60)
        secs = int(seconds % 60)
        return f"{minutes}m {secs}s"


async def run_tui(
    llm: BaseLLM,
    tools: ToolRegistry,
    soul: str,
    user_soul: str | None,
    skill_info: list[dict],
    memory_context: str | None,
    project_root: Path,
    mode: str = "autonomous",
    user_profile: dict | None = None,
    fliiq_email: str | None = None,
) -> None:
    """Launch the Fliiq TUI application."""
    app = FliiqApp(
        llm=llm,
        tools=tools,
        soul=soul,
        user_soul=user_soul,
        skill_info=skill_info,
        memory_context=memory_context,
        project_root=project_root,
        mode=mode,
        user_profile=user_profile,
        fliiq_email=fliiq_email,
    )
    await app.run_async()
