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

import asyncio
import codecs
import subprocess
import time
from pathlib import Path

import structlog
import textual.drivers.linux_driver as _textual_driver
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container
from textual.theme import Theme
from textual.widgets import OptionList

from fliiq import __version__
from fliiq.cli.tui.widgets import (
    FliiqInput,
    MessageLog,
    PlanApprovalSelector,
    StatusBar,
    WelcomeScreen,
)
from fliiq.runtime.agent.config import AgentConfig, next_mode
from fliiq.runtime.agent.loop import 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

_log = structlog.get_logger()

# ---------------------------------------------------------------------------
# Monkey-patch: Textual's LinuxDriver creates a strict UTF-8 incremental
# decoder that crashes when pasted text splits a multi-byte character across
# os.read() boundaries.  Replace with errors='replace' so invalid bytes
# become U+FFFD instead of raising UnicodeDecodeError.
# ---------------------------------------------------------------------------
_orig_getincrementaldecoder = codecs.getincrementaldecoder


def _safe_getincrementaldecoder(encoding):  # noqa: E302
    decoder_class = _orig_getincrementaldecoder(encoding)
    if encoding == "utf-8":
        _orig_init = decoder_class.__init__

        class _SafeDecoder(decoder_class):
            def __init__(self, errors="replace"):
                _orig_init(self, errors="replace")

        return _SafeDecoder
    return decoder_class


_textual_driver.getincrementaldecoder = _safe_getincrementaldecoder


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,
        persona: str | None = None,
    ) -> None:
        super().__init__()
        self.register_theme(
            Theme(
                name="fliiq",
                primary="#3AC9C2",
                secondary="#C4A8F2",
                accent="#C3BEE4",
                warning="#ffa62b",
                error="#FF6F61",
                success="#3AC9C2",
                foreground="#0C0F2C",
                background="#F4F6FB",
                surface="#F0FAFB",
                panel="#E8ECF4",
                dark=False,
            )
        )
        self.theme = "fliiq"
        self.scroll_sensitivity_y = 1.0
        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.persona = persona
        self.messages: list[dict] = []
        self.iteration_total = 0
        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 = ""
        self._welcome_visible = True
        self._thinking_timer = None

    def compose(self) -> ComposeResult:
        """Build the UI layout."""
        with Container(id="app-grid"):
            yield MessageLog(id="message-log")
            yield StatusBar(model_name=getattr(self.llm, "model", ""), id="status-bar")

    def on_mount(self) -> None:
        """Focus input on startup, show welcome screen, re-register builtins."""
        status = self.query_one(StatusBar)
        status.mode = self.mode
        status.focus_input()

        # Show welcome screen
        msg_log = self.query_one(MessageLog)
        welcome = WelcomeScreen(
            __version__,
            str(self.project_root),
            model=getattr(self.llm, "model", ""),
        )
        msg_log.mount(welcome)

        # Check for updates
        from fliiq.runtime.update_check import check_update_cached
        notice = check_update_cached()
        if notice:
            msg_log.add_system_message(notice)

        # Re-register builtins with async TUI handlers
        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:
        self.exit()

    def action_cycle_mode(self) -> None:
        self.mode = next_mode(self.mode)
        self.tools._mode = self.mode
        self.query_one(StatusBar).mode = self.mode
        self.query_one(MessageLog).add_system_message(f"Mode: {self.mode}")

    def action_focus_input(self) -> None:
        if self._cancellation is not None:
            self._cancellation.cancel()
            self.query_one(MessageLog).add_system_message("Cancelling...")
            if self._pending_input is not None and not self._pending_input.done():
                self._pending_input.cancel()
        else:
            self.query_one(StatusBar).focus_input()

    def _remove_welcome(self) -> None:
        """Remove welcome screen on first interaction."""
        if self._welcome_visible:
            self._welcome_visible = False
            for w in self.query_one(MessageLog).query(WelcomeScreen):
                w.remove()

    async def on_fliiq_input_submitted(self, event: FliiqInput.Submitted) -> None:
        """Handle user input submission from FliiqInput."""
        user_input = event.value.strip()
        if not user_input:
            return

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

        # Remove welcome screen on first real input
        self._remove_welcome()

        # 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 with periodic timer
        self.thinking_start = time.time()
        self.query_one(StatusBar).show_thinking("0s")
        self._thinking_timer = self.set_interval(1.0, self._update_thinking_elapsed)

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

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

        if cmd == "/help":
            help_text = (
                "Commands:\n"
                "/help   \u2014 Show this help\n"
                "/mode   \u2014 Cycle mode (plan \u2192 supervised \u2192 autonomous)\n"
                "/status \u2014 Show session status\n"
                "/clear  \u2014 Clear conversation history\n"
                "/copy   \u2014 Copy last response to clipboard\n"
                "/exit   \u2014 Exit"
            )
            log.add_system_message(help_text)

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

        elif cmd == "/status":
            status = (
                f"Mode: {self.mode}\n"
                f"Messages: {len(self.messages)}\n"
                f"Iterations: {self.iteration_total}"
            )
            log.add_system_message(status)

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

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

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

        else:
            log.add_system_message(f"Unknown command: {cmd}. Type /help for available commands.")

    async def _wait_for_input(self) -> str:
        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:
        self.query_one(MessageLog).add_system_message(prompt_text)
        return await self._wait_for_input()

    async def _tui_ask_user(self, question: str) -> str:
        return await self._prompt_user_input(f"Agent asks: {question}")

    async def _tui_plan_complete(self, summary: str, options: list[dict], recommended: int) -> str:
        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:
        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} \u2014 {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']} \u2014 {picked['description']}"
        except ValueError:
            pass
        return f"User chose: {answer}"

    def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
        if not isinstance(event.option_list, PlanApprovalSelector):
            return
        selector = event.option_list
        idx = event.option_index
        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']} \u2014 {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(StatusBar).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

        run_mode = self.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"]

        effective_skill_info = self.skill_info if run_mode != "plan" else None

        playbook_content = None
        if self.persona:
            playbook_content = load_playbook(self.persona, project_root=self.project_root)
        else:
            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(StatusBar).agent_running = True

        async def on_tool_call(name: str, tool_input: dict, result: str) -> None:
            """Called after each tool execution — display tool and yield for UI refresh."""
            msg_log.add_tool_call(name, tool_input, result)
            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()
                ]
            await asyncio.sleep(0)  # Yield to event loop for UI refresh

        # Build approval callback based on mode
        if run_mode == "supervised":
            async def on_before_tool_call(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", "")
        elif run_mode == "plan":
            async def on_before_tool_call(name: str, tool_input: dict) -> bool:
                return name in BUILTIN_TOOLS
        else:
            on_before_tool_call = None

        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
            self._stop_thinking()

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

            if result.stop_reason == "cancelled":
                msg_log.add_system_message("Cancelled.")
            elif result.final_text:
                self._last_assistant_text = result.final_text
                msg_log.add_completion(result.final_text, iter_label)
            else:
                msg_log.add_completion("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(StatusBar).mode = self.mode
                msg_log.add_system_message("Plan approved. Executing...")

                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")

                self.thinking_start = time.time()
                self.query_one(StatusBar).show_thinking("0s")
                self._thinking_timer = self.set_interval(1.0, self._update_thinking_elapsed)

                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
                self._stop_thinking()

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

        except Exception as e:
            self._stop_thinking()
            msg_log.add_system_message(f"Error: {e}")
        finally:
            self._cancellation = None
            self.query_one(StatusBar).agent_running = False

    def _format_elapsed(self, seconds: float) -> str:
        if seconds < 60:
            return f"{int(seconds)}s"
        minutes = int(seconds // 60)
        secs = int(seconds % 60)
        return f"{minutes}m {secs}s"

    def _update_thinking_elapsed(self) -> None:
        """Periodic callback to update the thinking timer display."""
        if self.thinking_start:
            elapsed = self._format_elapsed(time.time() - self.thinking_start)
            self.query_one(StatusBar).update_thinking(elapsed)

    def _stop_thinking(self) -> None:
        """Hide thinking indicator and stop timer."""
        if self._thinking_timer:
            self._thinking_timer.stop()
            self._thinking_timer = None
        self.thinking_start = 0
        self.query_one(StatusBar).hide_thinking()


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,
    persona: 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,
        persona=persona,
    )
    await app.run_async()
