"""
Native REPL Core - Interactive REPL with full TUI integration

The main REPL class that integrates all components:
- Input processing (slash commands, shell, file references)
- TUI components (keybindings, fuzzy search, session management)
- Response rendering (Markdown, streaming)
- Tool execution with permissions
- Session persistence

Examples:
    # Basic usage
    repl = NativeREPL(project_path=Path.cwd())
    repl.run()

    # Custom configuration
    config = REPLConfig(
        model="gpt-4o",
        stream_responses=True,
        auto_save=True,
    )
    repl = NativeREPL(project_path=Path.cwd(), config=config)
    repl.run()
"""

import os
import select
import sys
import termios
import threading
import tty
from concurrent.futures import Future, ThreadPoolExecutor
from pathlib import Path
from typing import Any, Optional

from dotenv import load_dotenv
from prompt_toolkit import PromptSession
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.styles import Style
from rich.console import Console

from groknroll import __version__
from groknroll.connect import ProviderConnector
from groknroll.repl.config import InputType, ProcessedInput, REPLConfig, REPLState
from groknroll.repl.input_processor import InputProcessor
from groknroll.repl.modes import ModeManager, Mode
from groknroll.repl.renderer import ResponseRenderer
from groknroll.repl.session import REPLSessionManager
from groknroll.repl.streaming import StreamingRLMHandler
from groknroll.repl.tool_executor import ToolExecutor
from groknroll.tui.fuzzy_search import FuzzyFileSearch
from groknroll.tui.keybindings import KeybindingManager
from groknroll.tui.shell_runner import ShellRunner
from groknroll.tui.slash_commands import CommandType, SlashCommandHandler


class CancellationMonitor:
    """Monitor for ESC key to cancel operations"""

    def __init__(self):
        self.cancelled = False
        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None

    def start(self) -> None:
        """Start monitoring for ESC key"""
        self.cancelled = False
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._monitor_keys, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        """Stop monitoring"""
        self._stop_event.set()
        if self._thread:
            self._thread.join(timeout=0.1)
            self._thread = None

    def _monitor_keys(self) -> None:
        """Monitor stdin for ESC key (0x1b)"""
        try:
            # Only works if stdin is a tty
            if not sys.stdin.isatty():
                return

            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setcbreak(fd)
                while not self._stop_event.is_set():
                    # Check if there's input available (non-blocking)
                    if select.select([sys.stdin], [], [], 0.1)[0]:
                        char = sys.stdin.read(1)
                        if char == "\x1b":  # ESC key
                            self.cancelled = True
                            break
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        except Exception:
            # Silently fail if terminal manipulation doesn't work
            pass


class NativeREPL:
    """
    Native REPL with full TUI integration

    Provides an interactive command-line interface with:
    - Slash commands (/help, /save, /agent, /quit)
    - Shell commands (!ls, !git status)
    - File references (@filename)
    - Streaming LLM responses
    - Session persistence with auto-save

    Example:
        repl = NativeREPL(project_path=Path.cwd())
        repl.run()

        # Or with custom config
        config = REPLConfig(model="gpt-4o", stream_responses=True)
        repl = NativeREPL(project_path=Path.cwd(), config=config)
        repl.run()
    """

    def __init__(
        self,
        project_path: Optional[Path] = None,
        config: Optional[REPLConfig] = None,
        console: Optional[Console] = None,
    ):
        """
        Initialize Native REPL

        Args:
            project_path: Project root path
            config: REPL configuration
            console: Rich console instance
        """
        self.project_path = project_path or Path.cwd()
        self.config = config or REPLConfig()
        self.console = console or Console()

        # Load .env from project directory for API keys
        env_file = self.project_path / ".env"
        if env_file.exists():
            load_dotenv(env_file, override=False)

        # Initialize state
        self.state = REPLState()
        self.state.current_agent = self.config.default_agent

        # Initialize mode manager
        initial_mode = Mode.FSD if self.config.fsd_mode else Mode.NORMAL
        self.mode_manager = ModeManager(initial_mode=initial_mode)

        # Initialize components
        self._init_components()

        # Setup callbacks
        self._setup_callbacks()

    def _init_components(self) -> None:
        """Initialize all REPL components"""
        # Prompt toolkit session for better input handling (paste, history, etc.)
        self._prompt_style = Style.from_dict(
            {
                "prompt": "cyan bold",
                "agent": "dim",
            }
        )
        self._prompt_history = InMemoryHistory()

        # Check for simple input mode (set GROKNROLL_SIMPLE_INPUT=1 if paste doesn't work)
        self._use_simple_input = os.environ.get("GROKNROLL_SIMPLE_INPUT", "").lower() in (
            "1",
            "true",
            "yes",
        )

        if not self._use_simple_input:
            # Create key bindings for mode switching and paste
            kb = KeyBindings()

            @kb.add("c-v")  # Ctrl+V paste fallback
            def _(event):
                data = event.app.clipboard.get_data()
                if data:
                    event.current_buffer.insert_text(data.text)

            # Mode cycling - Tab for forward, Shift+Tab for reverse
            @kb.add("s-tab")  # Shift+Tab - cycle mode reverse
            def _(event):
                self._on_cycle_mode_reverse()
                # Update prompt to show new mode
                event.app.invalidate()

            self._prompt_session: PromptSession = PromptSession(
                history=self._prompt_history,
                style=self._prompt_style,
                enable_history_search=True,
                mouse_support=False,  # Disable mouse to prevent paste issues
                key_bindings=kb,
                enable_open_in_editor=False,  # Disable to prevent delays
                enable_system_prompt=False,  # Disable to prevent delays
                complete_while_typing=False,  # Disable to prevent paste slowdown
                multiline=False,  # Single line input for faster paste
                editing_mode=EditingMode.EMACS,  # Use EMACS mode (not VI) - no ESC needed
                vi_mode=False,  # Explicitly disable vi mode
            )
        else:
            self._prompt_session = None  # Use simple input()

        # Input processing
        self.fuzzy_search = FuzzyFileSearch(workspace_root=self.project_path)
        self.input_processor = InputProcessor(
            workspace_root=self.project_path,
            fuzzy_search=self.fuzzy_search,
        )

        # TUI components
        self.slash_handler = SlashCommandHandler(available_agents=["build", "plan", "oracle"])
        self.shell_runner = ShellRunner(
            cwd=self.project_path,
            console=self.console,
        )
        self.keybinding_manager = KeybindingManager()

        # Response rendering
        self.renderer = ResponseRenderer(
            console=self.console,
            stream_enabled=self.config.stream_responses,
        )

        # Tool execution (with FSD mode from config)
        self.tool_executor = ToolExecutor(
            project_path=self.project_path,
            console=self.console,
            fsd_mode=self.config.fsd_mode,
        )

        # Streaming LLM handler
        self.streaming_handler = StreamingRLMHandler(
            project_path=self.project_path,
            model=self.config.model,
        )

        # Session management
        self.session_manager = REPLSessionManager(
            project_path=self.project_path,
            auto_save=self.config.auto_save,
            auto_save_interval=self.config.auto_save_interval,
        )

        # Provider connection
        self.provider_connector = ProviderConnector(
            env_path=self.project_path / ".env",
            console=self.console,
        )

    def _setup_callbacks(self) -> None:
        """Setup callbacks for TUI components"""
        # Slash command callbacks
        self.slash_handler.on_new_session = self._on_new_session
        self.slash_handler.on_save_session = self._on_save_session
        self.slash_handler.on_export_session = self._on_export_session
        self.slash_handler.on_switch_agent = self._on_switch_agent
        self.slash_handler.on_mode = self._on_mode
        self.slash_handler.on_clear = self._on_clear
        self.slash_handler.on_quit = self._on_quit
        self.slash_handler.on_connect = self._on_connect
        self.slash_handler.on_providers = self._on_providers
        self.slash_handler.on_init = self._on_init

        # Keybinding callbacks
        self.keybinding_manager.register_action("help", self._show_help)
        self.keybinding_manager.register_action("save", lambda: self._on_save_session(None))
        self.keybinding_manager.register_action("new", lambda: self._on_new_session(None))
        self.keybinding_manager.register_action("quit", self._on_quit)
        self.keybinding_manager.register_action("clear", self._on_clear)
        self.keybinding_manager.register_action("cycle_agent", self._on_cycle_mode)
        self.keybinding_manager.register_action("cycle_agent_reverse", self._on_cycle_mode_reverse)

    def run(self) -> None:
        """
        Run the main REPL loop

        Shows welcome banner and enters the interactive loop.
        Handles KeyboardInterrupt (Ctrl+C) gracefully.
        """
        # Show welcome banner
        self._show_welcome()

        # Start session
        self.session_manager.start_session(agent=self.state.current_agent)

        # Main loop
        while self.state.running:
            try:
                user_input = self._get_input()

                if not user_input:
                    continue

                self.process_input(user_input)

            except KeyboardInterrupt:
                self.console.print("\n[dim]Use /quit to exit[/dim]")
                continue

            except EOFError:
                self._on_quit()
                break

            except Exception as e:
                self.renderer.render_error(str(e))
                continue

        # Cleanup
        self._cleanup()

    def process_input(self, text: str) -> None:
        """
        Process user input and route to appropriate handler

        Args:
            text: Raw user input
        """
        # Preprocess input
        processed = self.input_processor.preprocess(text)

        # Route based on input type
        if processed.input_type == InputType.SLASH_COMMAND:
            self._handle_slash_command(processed)

        elif processed.input_type == InputType.SHELL_COMMAND:
            self._handle_shell_command(processed)

        elif processed.input_type == InputType.FILE_REFERENCE:
            self._handle_natural_language(processed)

        else:  # NATURAL_LANGUAGE
            self._handle_natural_language(processed)

    def _get_input(self) -> str:
        """
        Get user input with prompt

        Uses prompt_toolkit for better input handling, or falls back to
        simple input() if GROKNROLL_SIMPLE_INPUT=1 is set.

        Returns:
            User input string
        """
        # Get mode display info
        mode_display = self.mode_manager.get_display_string()

        if self._use_simple_input:
            # Simple input mode - better paste compatibility
            prompt = f"({mode_display}) > "
            return input(prompt).strip()

        # Build prompt with mode indicator
        prompt_text = [
            ("class:agent", f"({mode_display}) "),
            ("class:prompt", "> "),
        ]

        # Get input using prompt_toolkit
        return self._prompt_session.prompt(prompt_text).strip()

    def _show_welcome(self) -> None:
        """Show welcome banner"""
        self.renderer.render_welcome(
            version=__version__,
            agent=self.state.current_agent,
            custom_message=self.config.welcome_message,
        )

    def _show_help(self) -> None:
        """Show help information"""
        commands = [
            {"name": "/help", "description": "Show this help", "usage": "/help [command]"},
            {"name": "/init", "description": "Initialize project", "usage": "/init"},
            {"name": "/new", "description": "Start new session", "usage": "/new [name]"},
            {"name": "/save", "description": "Save current session", "usage": "/save [name]"},
            {"name": "/export", "description": "Export to Markdown", "usage": "/export [file]"},
            {"name": "/agent", "description": "Switch agent", "usage": "/agent <name>"},
            {"name": "/mode", "description": "Switch mode", "usage": "/mode [name]"},
            {
                "name": "/connect",
                "description": "Configure LLM provider",
                "usage": "/connect [provider]",
            },
            {"name": "/providers", "description": "List LLM providers", "usage": "/providers"},
            {"name": "/clear", "description": "Clear screen", "usage": "/clear"},
            {"name": "/quit", "description": "Exit REPL", "usage": "/quit"},
            {"name": "!cmd", "description": "Run shell command", "usage": "!ls -la"},
            {"name": "@file", "description": "Reference file", "usage": "explain @main.py"},
        ]
        self.renderer.render_help(commands)

    def _handle_slash_command(self, processed: ProcessedInput) -> None:
        """
        Handle slash command

        Args:
            processed: Processed input
        """
        result = self.slash_handler.process(processed.original)

        if result.command_type == CommandType.HELP:
            if result.success:
                self._show_help()
            else:
                self.renderer.render_status(result.output, "warning")

        elif result.command_type == CommandType.QUIT:
            # Handled by callback
            pass

        elif result.command_type == CommandType.CLEAR:
            # Handled by callback
            pass

        elif result.command_type == CommandType.CONNECT:
            # Output handled by connector, just show errors
            if not result.success:
                self.renderer.render_status(result.output, "error")

        elif result.command_type == CommandType.PROVIDERS:
            # Output handled by connector, just show errors
            if not result.success:
                self.renderer.render_status(result.output, "error")

        elif result.command_type == CommandType.UNKNOWN:
            self.renderer.render_status(result.output, "warning")

        elif result.success:
            self.renderer.render_status(result.output, "success")

        else:
            self.renderer.render_status(result.output, "error")

    def _handle_shell_command(self, processed: ProcessedInput) -> None:
        """
        Handle shell command

        Args:
            processed: Processed input
        """
        command = processed.metadata.get("command", "")
        if not command:
            return

        result = self.shell_runner.run(command, show_output=True)

        # Track command execution
        if result.execution:
            self.session_manager.add_command(
                command=command,
                exit_code=result.execution.exit_code,
                output=result.execution.output,
            )

    def _handle_natural_language(self, processed: ProcessedInput) -> None:
        """
        Handle natural language input

        Press ESC to cancel during processing.

        Args:
            processed: Processed input
        """
        # Build context with file references
        context = {}
        if processed.file_references:
            files = {}
            for file_path in processed.file_references:
                try:
                    full_path = self.project_path / file_path
                    if full_path.exists():
                        files[file_path] = full_path.read_text(encoding="utf-8")
                except Exception:
                    pass
            if files:
                context["files"] = files

        # Setup cancellation monitoring
        cancel_monitor = CancellationMonitor()
        result = None

        # Show processing indicator
        with self.renderer.show_progress("Thinking... (ESC to cancel)") as progress:
            task = progress.add_task("Working...", total=None)

            # Start ESC key monitoring
            cancel_monitor.start()

            try:
                # Run LLM call in thread pool so we can check for cancellation
                with ThreadPoolExecutor(max_workers=1) as executor:
                    if self.config.stream_responses:
                        tokens = []

                        def on_token(token: str) -> None:
                            tokens.append(token)
                            progress.update(task, description=f"Tokens: {len(tokens)}")

                        future: Future = executor.submit(
                            self.streaming_handler.complete_streaming_sync,
                            prompt=processed.processed,
                            context=context,
                            on_token=on_token,
                        )
                    else:
                        future = executor.submit(
                            self.streaming_handler.complete_streaming_sync,
                            prompt=processed.processed,
                            context=context,
                        )

                    # Wait for completion or cancellation
                    while not future.done():
                        if cancel_monitor.cancelled:
                            self.console.print("\n[yellow]Cancelled by user[/yellow]")
                            future.cancel()
                            return
                        future.result(timeout=0.1)  # Check every 100ms
            except TimeoutError:
                pass  # Just continue checking
            except Exception:
                pass
            finally:
                cancel_monitor.stop()

            # Get result if not cancelled
            if not cancel_monitor.cancelled and future.done():
                try:
                    result = future.result(timeout=0)
                except Exception as e:
                    self.renderer.render_error("LLM call failed", details=str(e))
                    return

        if cancel_monitor.cancelled:
            return

        if result is None:
            self.renderer.render_error("No response received")
            return

        if result.success:
            # Render response
            self.renderer.render_response(result.response)

            # Update state
            self.state.update_usage(cost=result.cost, tokens=result.tokens)

            # Add turn to session
            self.session_manager.add_turn(
                user_input=processed.original,
                response=result.response,
                cost=result.cost,
                tokens=result.tokens,
            )

            # Show usage if configured
            if self.config.show_cost or self.config.show_tokens:
                usage_info = []
                if self.config.show_tokens:
                    usage_info.append(f"Tokens: {result.tokens}")
                if self.config.show_cost:
                    usage_info.append(f"Cost: ${result.cost:.4f}")
                self.renderer.render_status(" | ".join(usage_info), "info")

        else:
            self.renderer.render_error(
                "Failed to get response",
                details=result.error,
            )

    # Callback methods

    def _on_new_session(self, name: Optional[str]) -> Any:
        """Handle new session request"""
        # Save current session first
        if self.session_manager.has_unsaved_changes():
            self.session_manager.save()

        # Start new session
        session = self.session_manager.start_session(
            name=name,
            agent=self.state.current_agent,
        )

        # Reset state
        self.state.conversation_history.clear()
        self.state.total_cost = 0.0
        self.state.total_tokens = 0
        self.state.turn_count = 0

        return session

    def _on_save_session(self, name: Optional[str]) -> Any:
        """Handle save session request"""
        path = self.session_manager.save(name)
        if path:
            self.renderer.render_status(f"Session saved to {path}", "success")
        return path

    def _on_export_session(self, filename: Optional[str]) -> Any:
        """Handle export session request"""
        content = self.session_manager.export_session(format="markdown")
        if content:
            if filename:
                path = Path(filename)
                path.write_text(content)
                self.renderer.render_status(f"Exported to {path}", "success")
            else:
                self.console.print(content)
        return content

    def _on_switch_agent(self, agent: str) -> Any:
        """Handle agent switch request"""
        self.state.current_agent = agent
        self.session_manager.switch_agent(agent)
        self.tool_executor.set_agent(agent)
        return agent

    def _on_mode(self, mode_name: str) -> Any:
        """Handle mode switch request via /mode command"""
        new_mode = self.mode_manager.set_mode_by_name(mode_name)
        if new_mode:
            self._apply_mode_change(new_mode)
            return new_mode
        return None

    def _on_clear(self) -> None:
        """Handle clear request"""
        self.renderer.clear()

    def _on_cycle_mode(self) -> None:
        """Handle mode cycle forward (Tab)"""
        new_mode = self.mode_manager.cycle(reverse=False)
        self._apply_mode_change(new_mode)

    def _on_cycle_mode_reverse(self) -> None:
        """Handle mode cycle reverse (Shift+Tab)"""
        new_mode = self.mode_manager.cycle(reverse=True)
        self._apply_mode_change(new_mode)

    def _apply_mode_change(self, new_mode: Mode) -> None:
        """Apply mode change and update components"""
        # Update agent if mode is an agent mode
        if self.mode_manager.is_agent():
            agent_name = self.mode_manager.get_agent_name()
            self.state.current_agent = agent_name
            self.session_manager.switch_agent(agent_name)
            self.tool_executor.set_agent(agent_name)

        # Update FSD mode in tool executor based on mode
        permissions = self.mode_manager.get_permissions()
        self.tool_executor.fsd_mode = permissions.skip_all_checks

        # Display mode change
        display = self.mode_manager.get_display_string()
        info = self.mode_manager.get_display_info()
        self.renderer.render_status(f"Mode: {display} - {info['description']}", "info")

    def _on_quit(self) -> None:
        """Handle quit request"""
        self.state.running = False
        self.renderer.render_goodbye()

    def _on_connect(self, provider_name: Optional[str]) -> Any:
        """Handle connect provider request"""
        if provider_name:
            return self.provider_connector.configure_provider(provider_name)
        else:
            selected = self.provider_connector.select_provider()
            if selected:
                return self.provider_connector.configure_provider(selected)
            return False

    def _on_providers(self) -> None:
        """Handle list providers request"""
        self.provider_connector.show_providers()

    def _on_init(self) -> dict:
        """Handle project initialization request"""
        groknroll_dir = self.project_path / ".groknroll"
        created_files = []

        # Create .groknroll directory
        groknroll_dir.mkdir(exist_ok=True)
        created_files.append(str(groknroll_dir))

        # Create instructions.md for agent guidance
        instructions_file = groknroll_dir / "instructions.md"
        if not instructions_file.exists():
            instructions_file.write_text("""# Project Instructions

This file provides guidance to groknroll when working with this codebase.

## Project Overview
<!-- Describe your project here -->

## Code Style
<!-- Describe your preferred code style -->

## Important Notes
<!-- Add any important context about your project -->
""")
            created_files.append(str(instructions_file))

        # Create config.json
        config_file = groknroll_dir / "config.json"
        if not config_file.exists():
            import json

            config = {
                "default_agent": "build",
                "model": "gpt-4o",
                "auto_save": True,
                "permissions": {
                    "read": "allow",
                    "write": "ask",
                    "edit": "ask",
                    "bash": "ask",
                },
            }
            config_file.write_text(json.dumps(config, indent=2))
            created_files.append(str(config_file))

        # Create sessions directory
        sessions_dir = groknroll_dir / "sessions"
        sessions_dir.mkdir(exist_ok=True)

        self.console.print("\n[bold green]✓ Initialized groknroll project[/bold green]")
        self.console.print(f"  Created: [cyan]{groknroll_dir}[/cyan]")
        if len(created_files) > 1:
            for f in created_files[1:]:
                self.console.print(f"    - {Path(f).name}")

        # Index the project for better context
        self.console.print("\n[dim]Indexing project...[/dim]")
        try:
            agent = self.streaming_handler.agent
            if hasattr(agent, "context") and hasattr(agent.context, "index_project"):
                stats = agent.context.index_project(force=True)
                self.console.print(
                    f"[green]✓ Indexed {stats['indexed']} files ({stats['total_lines']:,} lines)[/green]"
                )
        except Exception as e:
            self.console.print(f"[yellow]⚠ Indexing skipped: {e}[/yellow]")

        return {"path": str(groknroll_dir), "files": created_files}

    def _cleanup(self) -> None:
        """Cleanup resources on exit"""
        # Close session manager (handles final save)
        self.session_manager.close()

    # Public API methods

    def switch_agent(self, agent: str) -> None:
        """
        Switch to a different agent

        Args:
            agent: Agent name (build, plan, oracle)
        """
        self._on_switch_agent(agent)
        self.renderer.render_status(f"Switched to {agent} agent", "info")

    def get_session_info(self) -> dict[str, Any]:
        """
        Get current session information

        Returns:
            Session info dict
        """
        session = self.session_manager.get_current_session()
        if session is None:
            return {}

        return {
            "name": session.name,
            "agent": session.agent,
            "turns": self.session_manager.get_turn_count(),
            "messages": len(session.messages),
            "files_changed": len(session.file_changes),
            "commands_run": len(session.commands),
            "total_cost": self.state.total_cost,
            "total_tokens": self.state.total_tokens,
        }

    def get_history(self, limit: int = 10) -> list[dict]:
        """
        Get conversation history

        Args:
            limit: Maximum turns to return

        Returns:
            List of turn dicts
        """
        turns = self.session_manager.get_turns(limit=limit)
        return [turn.to_dict() for turn in turns]


def run_repl(
    project_path: Optional[Path] = None,
    config: Optional[REPLConfig] = None,
) -> None:
    """
    Run the Native REPL (convenience function)

    Args:
        project_path: Project path
        config: REPL configuration
    """
    repl = NativeREPL(project_path=project_path, config=config)
    repl.run()
