"""TUIApp - Main Textual application for LAIDA Mission Control.

Contract: CNT-TUI-APP-001
Traceability: UX-001, UI-002
"""

import contextlib
import fcntl
import os
import platform
import pty
import re
import select
import shutil
import subprocess
import threading
import time
from pathlib import Path

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Label

from laida_cli.adapters.claude_code_adapter import ClaudeCodeSubprocessAdapter
from laida_cli.ports.claude_code_port import ClaudeCodeAuthError
from laida_cli.services.detect_state import DetectStateService
from laida_cli.services.execute_phase import ExecutePhaseService
from laida_cli.tui.services.telemetry_service import TelemetryService
from laida_cli.tui.widgets.banner import BannerWidget
from laida_cli.tui.widgets.file_tree import FileTreeWidget
from laida_cli.tui.widgets.git_status import GitStatusWidget
from laida_cli.tui.widgets.live_feed import LiveFeedWidget
from laida_cli.tui.widgets.progress_panel import ProgressPanel
from laida_cli.tui.widgets.quick_actions import QuickActionsWidget
from laida_cli.tui.widgets.telemetry_bar import TelemetryBar


class ConfirmRunModal(ModalScreen[bool]):
    """Modal dialog to confirm execution."""

    CSS = """
    ConfirmRunModal {
        align: center middle;
    }

    #confirm-dialog {
        width: 60;
        height: 12;
        border: thick #8b5cf6;
        background: #1e1b4b;
        padding: 1 2;
    }

    #confirm-title {
        text-align: center;
        text-style: bold;
        color: #a78bfa;
        margin-bottom: 1;
    }

    #confirm-message {
        text-align: center;
        color: #e2e8f0;
        margin-bottom: 1;
    }

    #confirm-buttons {
        align: center middle;
        height: 3;
    }

    #confirm-buttons Button {
        margin: 0 1;
    }
    """

    def __init__(self, phase: int, step: int, step_name: str) -> None:
        super().__init__()
        self._phase = phase
        self._step = step
        self._step_name = step_name

    def compose(self) -> ComposeResult:
        with Vertical(id="confirm-dialog"):
            yield Label("⚡ Confirm Execution", id="confirm-title")
            yield Label(
                f"Run Phase {self._phase} Step {self._step}?\n({self._step_name})",
                id="confirm-message",
            )
            with Horizontal(id="confirm-buttons"):
                yield Button("Yes, Run", variant="success", id="confirm-yes")
                yield Button("Cancel", variant="error", id="confirm-no")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "confirm-yes":
            self.dismiss(True)
        else:
            self.dismiss(False)


class TUIApp(App[None]):
    """LAIDA Mission Control TUI Application."""

    TITLE = "LAIDA Mission Control"
    SUB_TITLE = "Agent-executable Architecture Methodology"

    CSS = """
    Screen {
        background: #0f0a1a;
    }

    Footer {
        background: #1e1b4b;
    }

    #main-layout {
        height: 1fr;
    }

    #sidebar {
        width: 30%;
        border-right: solid #8b5cf6;
        padding: 1 0;
    }

    #main-panel {
        width: 70%;
        padding: 1 0;
    }

    TelemetryBar {
        dock: bottom;
        height: 1;
        background: #1e1b4b;
        color: #e2e8f0;
        padding: 0 1;
    }
    """

    BINDINGS = [
        Binding("ctrl+q", "quit", "^Q Quit", show=True, priority=True),
        Binding("ctrl+h", "help", "^H Help", show=True, priority=True),
        Binding("ctrl+r", "run_next", "^R Run", show=True, priority=True),
        Binding("ctrl+s", "status", "^S Status", show=True, priority=True),
        Binding("ctrl+e", "edit_context", "^E Edit", show=True, priority=True),
        Binding("ctrl+v", "validate", "^V Validate", show=True, priority=True),
        Binding("ctrl+l", "login", "^L Login", show=True, priority=True),
        Binding("ctrl+c", "stop", "^C Stop", show=True, priority=True),
    ]

    def __init__(self, project_path: Path | None = None) -> None:
        super().__init__()
        self._project_path = project_path or Path.cwd()
        self._telemetry_service = TelemetryService()
        self._state_service = DetectStateService()
        self._live_feed: LiveFeedWidget | None = None
        self._is_executing = False
        self._current_process: subprocess.Popen[str] | None = None
        self._stop_requested = False

    def compose(self) -> ComposeResult:
        yield BannerWidget()

        with Horizontal(id="main-layout"):
            with Vertical(id="sidebar"):
                yield FileTreeWidget(project_path=self._project_path)
                yield GitStatusWidget(project_path=self._project_path)
                yield ProgressPanel(project_path=self._project_path)

            with Vertical(id="main-panel"):
                self._live_feed = LiveFeedWidget()
                yield self._live_feed
                yield QuickActionsWidget()

        yield TelemetryBar(telemetry_service=self._telemetry_service)
        yield Footer()

    def on_mount(self) -> None:
        if self._live_feed:
            self._live_feed.show_idle_message()

    def action_help(self) -> None:
        self.notify(
            "🚀 Ctrl+R  Run next step\n"
            "📊 Ctrl+S  Status\n"
            "📝 Ctrl+E  Edit context\n"
            "✅ Ctrl+V  Validate artifacts\n"
            "🔐 Ctrl+L  Login to Claude\n"
            "🛑 Ctrl+C  Stop process\n"
            "🚪 Ctrl+Q  Quit\n\n"
            "📋 Shift+Mouse to select text",
            title="⚡ Keyboard Shortcuts",
            timeout=10,
        )

    def action_stop(self) -> None:
        """Stop current running process."""
        if not self._is_executing:
            self.notify("No process running.", title="Stop", timeout=2)
            return

        self._stop_requested = True

        if self._current_process:
            try:
                self._current_process.terminate()
                if self._live_feed:
                    self._live_feed.write(
                        "\n[bold #f87171]🛑 Process stopped by user (Ctrl+C)[/]\n"
                    )
                self.notify("Process stopped.", title="🛑 Stopped", timeout=3)
            except Exception as e:
                self.notify(f"Error stopping: {e}", title="Error", timeout=5)
        else:
            if self._live_feed:
                self._live_feed.write(
                    "\n[bold #fbbf24]⚠ Stop requested...[/]\n"
                )

    def action_edit_context(self) -> None:
        """Open project_context.yaml in editor."""
        inputs_dir = self._project_path / "inputs"
        context_file = inputs_dir / "project_context.yaml"

        # Template locations (in order of preference)
        template_locations = [
            inputs_dir / "project_context_template.yaml",
            self._project_path / ".laida" / "templates" / "project_context_template.yaml",
            self._project_path / "project_context_template.yaml",
        ]

        template_file = None
        for loc in template_locations:
            if loc.exists():
                template_file = loc
                break

        # Create inputs directory if needed
        if not inputs_dir.exists():
            inputs_dir.mkdir(parents=True)

        # Copy template if context file doesn't exist OR if it's minimal (< 500 bytes)
        should_copy = not context_file.exists()
        if context_file.exists() and context_file.stat().st_size < 500:
            # File exists but is minimal placeholder - replace with real template
            should_copy = True

        if should_copy:
            if template_file:
                shutil.copy(template_file, context_file)
                self.notify(
                    "Created project_context.yaml from template.\n"
                    "Fill in your project details (219 lines) and save.",
                    title="Context File Created",
                    timeout=5,
                )
            else:
                self.notify(
                    "Template not found!\n"
                    "Run 'laida init --force' to reset project.",
                    title="Error",
                    timeout=8,
                )
                return
        else:
            self.notify(
                "Opening project_context.yaml...\n"
                "Edit your project details and save.",
                title="Edit Context",
                timeout=3,
            )

        # Open in editor
        self._open_in_editor(context_file)

    def _open_in_editor(self, file_path: Path) -> None:
        """Open a file in the system's preferred editor.

        Tries in order: VSCode, then system default.

        Args:
            file_path: Path to the file to open
        """
        try:
            # Try VSCode first (most common for developers)
            vscode_cmd = "code"
            if platform.system() == "Windows":
                # On Windows, might need full path or use 'code.cmd'
                vscode_cmd = "code.cmd"

            try:
                subprocess.Popen(
                    [vscode_cmd, str(file_path)],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                )
                return
            except FileNotFoundError:
                pass  # VSCode not available, try alternatives

            # Fall back to system default
            system = platform.system()
            if system == "Windows":
                os.startfile(str(file_path))  # type: ignore[attr-defined]
            elif system == "Darwin":  # macOS
                subprocess.Popen(["open", str(file_path)])
            else:  # Linux and others
                # Try xdg-open, then common editors
                for cmd in ["xdg-open", "gedit", "kate", "nano", "vim"]:
                    try:
                        subprocess.Popen(
                            [cmd, str(file_path)],
                            stdout=subprocess.DEVNULL,
                            stderr=subprocess.DEVNULL,
                        )
                        return
                    except FileNotFoundError:
                        continue

                self.notify(
                    f"Could not find an editor.\nFile: {file_path}",
                    title="Editor Not Found",
                    timeout=8,
                )

        except Exception as e:
            self.notify(
                f"Failed to open editor: {e}",
                title="Error",
                timeout=8,
            )

    def action_run_next(self) -> None:
        """Run the next step with confirmation."""
        if self._is_executing:
            self.notify("Execution in progress...", title="Wait", timeout=2)
            return

        state = self._state_service.detect(self._project_path)

        if state.is_complete:
            self.notify("Project complete!\nAll phases finished.", title="Complete", timeout=5)
            return

        step_name = self._state_service.get_step_name(state.next_phase, state.next_step)

        def handle_confirm(confirmed: bool | None) -> None:
            if confirmed:
                self._start_execution(state.next_phase, state.next_step, step_name)

        self.push_screen(
            ConfirmRunModal(state.next_phase, state.next_step, step_name),
            handle_confirm,
        )

    def _start_execution(self, phase: int, step: int, step_name: str) -> None:
        """Start execution: first verify auth, then run."""
        self._is_executing = True

        if self._live_feed:
            self._live_feed.clear()
            self._live_feed.write("[#a78bfa]Checking Claude Code authentication...[/]\n")

        # Run auth check and execution in background
        self.run_worker(
            self._check_auth_and_execute(phase, step, step_name),
            exclusive=True,
            thread=True,
        )

    async def _check_auth_and_execute(self, phase: int, step: int, step_name: str) -> None:  # noqa: ARG002
        """Check authentication and then execute phase."""
        try:
            adapter = ClaudeCodeSubprocessAdapter()

            # Step 1: Check authentication
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    "[#5eead4]▶ Verifying Claude Code connection...[/]\n",
                )

            is_authenticated = adapter.check_auth()

            if not is_authenticated:
                self._handle_auth_failure()
                return

            # Step 2: Authentication OK, proceed with execution
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    "[#10b981]✔ Authenticated[/]\n\n",
                )
                self.call_from_thread(
                    self._live_feed.write,
                    f"[bold #a78bfa]▶ Starting Phase {phase} Step {step}...[/]\n\n",
                )

            self._telemetry_service.start_task()

            # Step 3: Execute the phase
            service = ExecutePhaseService(adapter)

            def on_output(line: str) -> None:
                if self._live_feed:
                    self.call_from_thread(self._live_feed.append_line, line)

            result = service.execute_streaming(
                phase=phase,
                step=step,
                project_path=str(self._project_path),
                on_output=on_output,
            )

            # Step 4: Handle result
            self._telemetry_service.end_task(result.success, phase, step)

            if result.success:
                if self._live_feed:
                    self.call_from_thread(
                        self._live_feed.write,
                        "\n[bold #10b981]✔ Execution completed successfully![/]\n",
                    )
                self.call_from_thread(
                    self.notify,
                    f"Phase {phase} Step {step} completed!",
                    title="Success",
                    timeout=5,
                )
            else:
                error_msg = result.error_message or "Unknown error"
                if self._live_feed:
                    self.call_from_thread(
                        self._live_feed.write,
                        f"\n[bold #f87171]✘ Execution failed[/]\n[#64748b]{error_msg}[/]\n",
                    )
                self.call_from_thread(
                    self.notify,
                    f"Failed: {error_msg}",
                    title="Error",
                    timeout=8,
                )

        except ClaudeCodeAuthError:
            self._handle_auth_failure()

        except Exception as e:
            self._telemetry_service.reset_task()
            error_str = str(e)
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    f"\n[bold #f87171]✘ Unexpected Error[/]\n[#64748b]{error_str}[/]\n",
                )
            self.call_from_thread(
                self.notify,
                f"Error: {error_str}",
                title="Error",
                timeout=8,
            )

        finally:
            self._is_executing = False

    def _handle_auth_failure(self) -> None:
        """Handle authentication failure."""
        self._telemetry_service.reset_task()

        if self._live_feed:
            self.call_from_thread(
                self._live_feed.write,
                "\n[bold #f87171]✘ Not authenticated with Claude Code[/]\n\n"
                "[#e2e8f0]To authenticate, run in terminal:[/]\n"
                "[bold #5eead4]  laida login[/]\n\n"
                "[#64748b]Or run 'claude' directly to start authentication.[/]\n",
            )

        self.call_from_thread(
            self.notify,
            "Not authenticated.\n\nRun 'laida login' in terminal.",
            title="Authentication Required",
            timeout=10,
        )

        self._is_executing = False

    def action_status(self) -> None:
        state = self._state_service.detect(self._project_path)

        if state.is_complete:
            status_msg = "[bold green]Complete[/]\nAll 20 steps finished"
        elif not state.has_started:
            status_msg = "[yellow]Not started[/]\nPress R to begin"
        else:
            completed = (state.last_completed_phase or 0) * 4 + (state.last_completed_step or 0) + 1
            status_msg = (
                f"[cyan]In Progress[/]\n"
                f"Completed: {completed}/20 steps\n"
                f"Next: Phase {state.next_phase} Step {state.next_step}"
            )

        self.notify(status_msg, title=f"Project: {self._project_path.name}", timeout=8)

    def action_login(self) -> None:
        """Launch Claude Code authentication with streaming output."""
        if self._is_executing:
            self.notify("Execution in progress...", title="Wait", timeout=2)
            return

        self._is_executing = True

        if self._live_feed:
            self._live_feed.clear()
            self._live_feed.write(
                "[bold #a78bfa]🔐 Claude Code Authentication[/]\n\n"
                "[#e2e8f0]Starting Claude Code...[/]\n"
                "[#64748b]The login URL will appear below - click it to authenticate.[/]\n\n"
                "[#5eead4]─────────────────────────────────────[/]\n\n"
            )

        self.notify(
            "Starting authentication...\n"
            "Watch the Live Feed for the login URL.",
            title="🔐 Login",
            timeout=5,
        )

        # Run authentication in background thread
        thread = threading.Thread(target=self._stream_claude_login, daemon=True)
        thread.start()

    def _stream_claude_login(self) -> None:
        """Stream Claude Code output for login using pseudo-terminal."""
        self._stop_requested = False
        master_fd = None
        slave_fd = None

        def log(msg: str) -> None:
            """Helper to write to live feed from thread."""
            if self._live_feed:
                self.call_from_thread(self._live_feed.write, msg)

        try:
            log("[#5eead4]Creating terminal session...[/]\n")

            # Create pseudo-terminal pair
            master_fd, slave_fd = pty.openpty()

            log("[#5eead4]Starting Claude Code process...[/]\n")

            # Start claude with the slave as its terminal
            self._current_process = subprocess.Popen(
                ["claude"],
                stdin=slave_fd,
                stdout=slave_fd,
                stderr=slave_fd,
                preexec_fn=os.setsid,
            )

            # Close slave in parent - child owns it now
            os.close(slave_fd)
            slave_fd = None

            log(f"[#5eead4]Process started (PID: {self._current_process.pid})[/]\n")
            log("[#64748b]Waiting for output...[/]\n\n")

            buffer = ""
            timeout_seconds = 180  # 3 minute timeout
            start_time = time.time()
            lines_received = 0

            # Set master to non-blocking
            flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
            fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

            while True:
                # Check for stop request
                if self._stop_requested:
                    if self._current_process:
                        self._current_process.terminate()
                    break

                # Check timeout
                elapsed = time.time() - start_time
                if elapsed > timeout_seconds:
                    if self._current_process:
                        self._current_process.terminate()
                    log(
                        "\n[#f87171]⏱ Login timed out after 3 minutes.[/]\n"
                        "[#64748b]Press Ctrl+L to try again.[/]\n"
                    )
                    break

                # Check if process has terminated
                if self._current_process and self._current_process.poll() is not None:
                    log(f"\n[#64748b]Process exited (code: {self._current_process.returncode})[/]\n")
                    # Read any remaining output
                    try:
                        while True:
                            data = os.read(master_fd, 4096)
                            if not data:
                                break
                            text = data.decode("utf-8", errors="replace")
                            buffer += text
                    except (OSError, BlockingIOError):
                        pass
                    break

                # Read available data from pty
                try:
                    ready, _, _ = select.select([master_fd], [], [], 0.2)
                    if ready:
                        data = os.read(master_fd, 4096)
                        if data:
                            text = data.decode("utf-8", errors="replace")
                            buffer += text

                            # Process complete lines
                            while "\n" in buffer:
                                line, buffer = buffer.split("\n", 1)
                                lines_received += 1
                                sanitized = self._sanitize_claude_output(line)
                                if sanitized:
                                    log(sanitized + "\n")

                            # Process partial lines with URLs immediately
                            if buffer and ("http://" in buffer or "https://" in buffer):
                                sanitized = self._sanitize_claude_output(buffer)
                                if sanitized:
                                    log(sanitized + "\n")
                                buffer = ""

                except (OSError, BlockingIOError):
                    pass

            # Process remaining buffer
            if buffer.strip():
                for line in buffer.split("\n"):
                    if line.strip():
                        sanitized = self._sanitize_claude_output(line)
                        if sanitized:
                            log(sanitized + "\n")

            # Show completion message
            if not self._stop_requested:
                log(
                    "\n[#5eead4]─────────────────────────────────────[/]\n\n"
                    f"[bold #10b981]✔ Session completed ({lines_received} lines received)[/]\n"
                    "[#64748b]Press Ctrl+R to run next step.[/]\n"
                )
                self.call_from_thread(
                    self.notify,
                    "Session completed!",
                    title="✔ Done",
                    timeout=5,
                )

        except FileNotFoundError:
            log(
                "\n[bold #f87171]❌ Claude Code CLI not found![/]\n"
                "[#e2e8f0]Install from: https://claude.ai/code[/]\n"
            )

        except Exception as e:
            log(f"\n[#f87171]Error: {type(e).__name__}: {e}[/]\n")

        finally:
            self._is_executing = False

            # Clean up process
            if self._current_process:
                with contextlib.suppress(Exception):
                    self._current_process.terminate()
                    self._current_process.wait(timeout=1)
                self._current_process = None

            # Clean up file descriptors
            if master_fd is not None:
                with contextlib.suppress(OSError):
                    os.close(master_fd)
            if slave_fd is not None:
                with contextlib.suppress(OSError):
                    os.close(slave_fd)

    def _sanitize_claude_output(self, line: str) -> str:
        """Sanitize Claude Code output for TUI display.

        Removes ANSI codes and formats URLs for clickability.

        Args:
            line: Raw output line from Claude Code

        Returns:
            Sanitized line for Rich markup display
        """
        # Remove ANSI escape codes
        ansi_pattern = re.compile(r"\x1b\[[0-9;]*m|\x1b\[[0-9;]*[A-Za-z]|\x1b\].*?\x07")
        clean = ansi_pattern.sub("", line)

        # Remove other control characters
        clean = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", clean)

        # Skip empty lines
        if not clean.strip():
            return ""

        # Highlight URLs (make them stand out) - BEFORE escaping brackets
        url_pattern = re.compile(r"(https?://[^\s\x00-\x1f]+)")
        if url_pattern.search(clean):
            # First escape any brackets NOT in URLs
            parts = url_pattern.split(clean)
            result_parts = []
            for i, part in enumerate(parts):
                if i % 2 == 0:
                    # Not a URL - escape brackets
                    result_parts.append(part.replace("[", "\\[").replace("]", "\\]"))
                else:
                    # This is a URL - format it as a link
                    result_parts.append(f"[bold #5eead4 link={part}]{part}[/]")
            return f"[#fbbf24]>>> [/]{''.join(result_parts)}"

        # Escape Rich markup characters in regular text
        clean = clean.replace("[", "\\[").replace("]", "\\]")

        return f"[#94a3b8]{clean}[/]"

    def action_validate(self) -> None:
        """Validate project artifacts."""
        if self._is_executing:
            self.notify("Execution in progress...", title="Wait", timeout=2)
            return

        if self._live_feed:
            self._live_feed.clear()
            self._live_feed.write("[bold #a78bfa]✅ Validating Project Artifacts[/]\n\n")

        # Run validation in background
        self.run_worker(
            self._run_validation(),
            exclusive=True,
            thread=True,
        )

    async def _run_validation(self) -> None:
        """Run artifact validation."""
        try:
            from laida_cli.services.validate_artifacts import ValidateArtifactsService

            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    "[#5eead4]▶ Checking artifact structure...[/]\n",
                )

            service = ValidateArtifactsService()
            result = service.validate(self._project_path)

            if result.is_valid:
                if self._live_feed:
                    self.call_from_thread(
                        self._live_feed.write,
                        f"\n[bold #10b981]✔ Validation passed![/]\n"
                        f"[#e2e8f0]Artifacts: {result.artifact_count}[/]\n"
                        f"[#e2e8f0]Phases checked: {result.phases_checked}[/]\n",
                    )
                self.call_from_thread(
                    self.notify,
                    f"Validation passed!\n{result.artifact_count} artifacts valid.",
                    title="✅ Valid",
                    timeout=5,
                )
            else:
                errors = "\n".join(f"• {e}" for e in result.errors[:5])
                if self._live_feed:
                    self.call_from_thread(
                        self._live_feed.write,
                        f"\n[bold #f87171]✘ Validation failed[/]\n"
                        f"[#e2e8f0]Errors found: {len(result.errors)}[/]\n\n"
                        f"[#64748b]{errors}[/]\n",
                    )
                self.call_from_thread(
                    self.notify,
                    f"Validation failed!\n{len(result.errors)} errors found.",
                    title="❌ Invalid",
                    timeout=8,
                )

        except ImportError:
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    "[#fbbf24]Validation service not available.[/]\n"
                    "[#64748b]Run 'laida validate' from terminal.[/]\n",
                )
        except Exception as e:
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    f"\n[#f87171]Validation error: {e}[/]\n",
                )

    @property
    def project_path(self) -> Path:
        return self._project_path

    @property
    def telemetry_service(self) -> TelemetryService:
        return self._telemetry_service
