"""Interactive REPL / chat mode for Fliiq."""

import signal
from dataclasses import dataclass, field

import structlog

from fliiq.cli import display
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

log = structlog.get_logger()


def _repl_ask_user_choice(question: str, options: list[dict], recommended: int) -> str:
    """REPL handler for ask_user_choice — presents numbered options."""
    display.print_markdown(f"\n**Agent asks:** {question}")
    for i, opt in enumerate(options, 1):
        rec = " (recommended)" if i == recommended else ""
        display.console.print(f"  [{i}] {opt['label']}{rec} — {opt['description']}")
    display.console.print("  [0] Custom answer")

    while True:
        answer = display.console.input("  Your choice (number or custom text): ").strip()
        try:
            choice = int(answer)
            if choice == 0:
                custom = display.console.input("  Your custom answer: ").strip()
                return f"User chose: {custom}"
            if 1 <= choice <= len(options):
                picked = options[choice - 1]
                return f"User chose: {picked['label']} — {picked['description']}"
            display.console.print(f"  [yellow]Pick 0-{len(options)}[/yellow]")
        except ValueError:
            return f"User chose: {answer}"


def _repl_plan_complete(summary: str, options: list[dict], recommended: int) -> str:
    """REPL handler for plan_complete — presents numbered options + free-form."""
    display.print_markdown(f"\n**Plan Summary:**\n{summary}")
    for i, opt in enumerate(options, 1):
        rec = " (recommended)" if i == recommended else ""
        display.console.print(f"  \\[{i}] {opt['label']}{rec} — {opt['description']}")
    display.console.print(f"  \\[{len(options) + 1}] Type your own response")

    while True:
        try:
            answer = display.console.input("[yellow]Your choice:[/yellow] ").strip()
        except (KeyboardInterrupt, EOFError):
            display.console.print("\n[dim]Rejected.[/dim]")
            return "rejected"
        try:
            choice = int(answer)
            if choice == len(options) + 1:
                try:
                    custom = display.console.input("Your response: ").strip()
                except (KeyboardInterrupt, EOFError):
                    display.console.print("\n[dim]Rejected.[/dim]")
                    return "rejected"
                return f"revise: {custom}"
            if 1 <= choice <= len(options):
                opt = options[choice - 1]
                if opt.get("action") == "execute":
                    return "approved"
                return f"User chose: {opt['label']} — {opt['description']}"
        except ValueError:
            pass
        display.console.print(f"  [yellow]Pick 1-{len(options) + 1}[/yellow]")


@dataclass
class ChatSession:
    """Manages a multi-turn chat session with the agent."""

    llm: BaseLLM
    tools: ToolRegistry
    soul: str
    user_soul: str | None
    skill_info: list[dict]
    memory_context: str | None
    project_root: any  # Path
    mode: str = "autonomous"
    user_profile: dict | None = None
    fliiq_email: str | None = None
    persona: str | None = None
    messages: list[dict] = field(default_factory=list)
    iteration_total: int = 0

    def _build_system_prompt(self, prompt: str | None = None) -> str:
        """Build system prompt, optionally with domain detection from prompt."""
        playbook_content = None
        if self.persona:
            playbook_content = load_playbook(self.persona, project_root=self.project_root)
        elif prompt:
            domains = detect_domain(prompt, 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

        # Enrich memory with prompt-relevant entities
        enriched_memory = self.memory_context
        if prompt:
            from fliiq.runtime.agent.setup import enrich_memory_context

            enriched_memory = enrich_memory_context(self.memory_context, prompt, self.project_root)

        return assemble_agent_prompt(
            soul=self.soul,
            user_soul=self.user_soul,
            playbook=playbook_content,
            skills=self.skill_info if (self.skill_info and self.mode != "plan") else None,
            mode=self.mode,
            clarifications=None,
            memory_context=enriched_memory,
            user_profile=self.user_profile,
            fliiq_email=self.fliiq_email,
        )

    async def send(self, user_input: str, cancellation: CancellationToken | None = None, thinking=None) -> AgentResult:
        """Send a message and get the agent's response. Returns full AgentResult."""
        self.messages.append({"role": "user", "content": user_input})

        system = self._build_system_prompt(user_input)
        config = AgentConfig(mode=self.mode)

        # Filter tool definitions by current mode
        BUILTIN_TOOLS = {"todo", "ask_user", "ask_user_choice", "plan_complete"}
        all_defs = self.tools.get_tool_definitions()
        if self.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"]

        # Supervised mode approval
        approve_all = False

        def _on_before_tool_call(name: str, tool_input: dict) -> bool:
            nonlocal approve_all
            if approve_all:
                return True
            response = display.prompt_tool_approval(name, tool_input)
            if response == "a":
                approve_all = True
                return True
            return response == "y"

        def _on_tool_call(name, tool_input, result):
            display.print_tool_call(name, result, tool_input=tool_input)
            # 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 base on_before_tool_call
        if self.mode == "supervised":
            _base_before = _on_before_tool_call
        elif self.mode == "plan":
            def _base_before(name, _):
                return name in BUILTIN_TOOLS
        else:
            _base_before = None

        # Wrap callbacks to pause/resume thinking indicator around tool execution
        if thinking:
            def _before_with_pause(name, tool_input):
                thinking.pause()
                return _base_before(name, tool_input) if _base_before else True

            def _on_tool_call_with_resume(name, tool_input, result):
                _on_tool_call(name, tool_input, result)
                thinking.resume()
        else:
            _before_with_pause = _base_before
            _on_tool_call_with_resume = _on_tool_call

        result = await agent_loop(
            llm=self.llm,
            messages=self.messages,
            tools=self.tools,
            config=config,
            system=system,
            on_tool_call=_on_tool_call_with_resume,
            on_before_tool_call=_before_with_pause,
            cancellation=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 self.mode == "plan" else "Tool skipped by user"
            ),
        )

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

        # Post-conversation memory extraction (non-fatal)
        if result.stop_reason != "cancelled" and result.iterations > 0:
            try:
                from fliiq.runtime.memory.extractor import extract_and_save_memories

                await extract_and_save_memories(self.llm, self.messages, self.project_root)
            except Exception:
                pass

        return result

    def cycle_mode(self) -> str:
        """Cycle to the next mode and return the new mode name."""
        self.mode = next_mode(self.mode)
        self.tools._mode = self.mode
        return self.mode

    def clear(self) -> None:
        """Reset conversation history, keep config."""
        self.messages = []
        self.iteration_total = 0

    @property
    def status(self) -> dict:
        """Return session status info."""
        return {
            "mode": self.mode,
            "message_count": len(self.messages),
            "iteration_total": self.iteration_total,
        }


HELP_TEXT = """
**Available commands:**
- `/help`   — Show this help
- `/mode`   — Cycle mode (plan -> supervised -> autonomous)
- `/status` — Show session status
- `/clear`  — Clear conversation history
- `/exit`   — Exit chat mode
"""


async def run_repl(session: ChatSession) -> None:
    """Run the interactive REPL loop."""
    display.print_mode(session.mode)
    display.console.print("[dim]Type /help for commands, /exit to quit[/dim]\n")
    from fliiq.runtime.update_check import check_update_cached
    notice = check_update_cached()
    if notice:
        display.console.print(f"[dim]{notice}[/dim]\n")

    mode_colors = {"autonomous": "green", "supervised": "yellow", "plan": "red"}

    while True:
        try:
            color = mode_colors.get(session.mode, "white")
            user_input = display.console.input(f"[{color}]{session.mode}[/{color}] [bold]>[/bold] ").strip()
        except (KeyboardInterrupt, EOFError):
            display.console.print("\n[dim]Exiting...[/dim]")
            break

        if not user_input:
            continue

        # Slash command dispatch
        if user_input.startswith("/"):
            cmd = user_input.lower().split()[0]

            if cmd == "/help":
                display.print_markdown(HELP_TEXT)
            elif cmd == "/mode":
                new_mode = session.cycle_mode()
                display.print_mode(new_mode)
            elif cmd == "/status":
                s = session.status
                display.print_status(s["mode"], s["message_count"], s["iteration_total"])
            elif cmd == "/clear":
                session.clear()
                display.console.print("[dim]Conversation cleared.[/dim]")
            elif cmd == "/exit":
                display.console.print("[dim]Exiting...[/dim]")
                break
            else:
                display.console.print(f"[yellow]Unknown command: {cmd}. Type /help for available commands.[/yellow]")
            continue

        # Regular message — send to agent
        try:
            cancellation = CancellationToken()
            # Set up SIGINT handler to cancel agent (double Ctrl+C to force)
            original_handler = signal.getsignal(signal.SIGINT)
            sigint_count = 0

            def _sigint_handler(signum, frame):
                nonlocal sigint_count
                sigint_count += 1
                cancellation.cancel()
                if sigint_count >= 2:
                    raise KeyboardInterrupt
                display.console.print("\n[dim]Cancelling... (Ctrl+C again to force)[/dim]")

            signal.signal(signal.SIGINT, _sigint_handler)

            try:
                thinking = display.ThinkingIndicator()
                await thinking.start()
                result = await session.send(user_input, cancellation=cancellation, thinking=thinking)
                await thinking.stop()
                if result.stop_reason == "cancelled":
                    display.console.print("[dim]Cancelled.[/dim]")
                elif result.final_text:
                    display.console.print()
                    display.print_markdown(result.final_text)
                    display.console.print()

                # Plan mode: auto-continue in supervised mode on approval
                if session.mode == "plan" and plan_was_approved(result):
                    session.mode = "supervised"
                    session.tools._mode = "supervised"
                    session.tools._tools.pop("plan_complete", None)
                    display.print_mode("supervised")
                    display.console.print("[dim]Plan approved. Executing...[/dim]")

                    cancellation = CancellationToken()
                    thinking = display.ThinkingIndicator()
                    await thinking.start()
                    result = await session.send(
                        "Plan approved. Proceed with execution.",
                        cancellation=cancellation,
                        thinking=thinking,
                    )
                    await thinking.stop()
                    if result.stop_reason == "cancelled":
                        display.console.print("[dim]Cancelled.[/dim]")
                    elif result.final_text:
                        display.console.print()
                        display.print_markdown(result.final_text)
                        display.console.print()
            finally:
                signal.signal(signal.SIGINT, original_handler)
        except Exception as e:
            display.print_error(f"Error: {e}")
