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

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

import fcntl
import logging
import os
import platform
import pty
import re
import select
import shutil
import struct
import subprocess
import termios
import time
from pathlib import Path

from rich.text import Text
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical
from textual.events import Key
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

# Setup logging for debugging - logs to /tmp/laida_tui.log
logging.basicConfig(
    filename="/tmp/laida_tui.log",
    level=logging.DEBUG,
    format="%(asctime)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)


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),
    ]

    # Key mappings for PTY (arrow keys, special keys)
    PTY_KEY_MAP = {
        "up": "\x1b[A",
        "down": "\x1b[B",
        "right": "\x1b[C",
        "left": "\x1b[D",
        "home": "\x1b[H",
        "end": "\x1b[F",
        "delete": "\x1b[3~",
        "backspace": "\x7f",
        "tab": "\t",
        "enter": "\r",
        "escape": "\x1b",
    }

    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"
            "Durante ejecución:\n"
            "  Flechas, Enter, letras → van al PTY\n"
            "  Interactúa con Claude normalmente\n\n"
            "📋 Shift+Mouse to select text",
            title="⚡ Keyboard Shortcuts",
            timeout=10,
        )

    def on_key(self, event: Key) -> None:
        """Handle all key events - forward to PTY when executing.

        During execution, all keyboard input is forwarded to the PTY
        allowing the user to interact with Claude Code's permission
        dialogs (arrows, enter, typing, etc.)
        """
        # Only forward keys when executing and PTY is active
        if not self._is_executing or self._pty_master_fd is None:
            return  # Let normal key handling proceed

        # Don't intercept Ctrl key combos (let bindings handle them)
        if event.key.startswith("ctrl+"):
            return

        # Get the key to send
        key_name = event.key
        data: str | None = None
        if key_name in self.PTY_KEY_MAP:
            # Special key (arrow, enter, etc.)
            data = self.PTY_KEY_MAP[key_name]
        elif event.character and len(event.character) == 1:
            # Regular character
            data = event.character

        if not data:
            # Unknown key, ignore
            return

        # Send to PTY
        try:
            os.write(self._pty_master_fd, data.encode())
            event.stop()  # Prevent default handling
        except OSError:
            pass  # PTY closed

    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

        # Send Ctrl+C to PTY if running
        if self._pty_master_fd is not None:
            try:
                os.write(self._pty_master_fd, b"\x03")  # Ctrl+C
                if self._live_feed:
                    self._live_feed.write(
                        "\n[bold #f87171]🛑 Stop signal sent (Ctrl+C)[/]\n"
                    )
                self.notify("Stop signal sent.", title="🛑 Stopping", timeout=3)
            except OSError as e:
                self.notify(f"Error stopping: {e}", title="Error", timeout=5)
        elif 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 with PTY streaming integrated in TUI.

        This runs Claude Code in a pseudo-terminal and streams output
        to the LiveFeedWidget in real-time. User can see all output
        and respond to permission requests.
        """
        # Guard against duplicate execution
        if self._is_executing:
            logger.warning("Attempted to start execution while already executing")
            return

        self._is_executing = True
        self._stop_requested = False
        self._pty_master_fd: int | None = None

        logger.info(f"Starting execution: Phase {phase} Step {step}")

        if self._live_feed:
            self._live_feed.clear()
            self._live_feed.write(
                f"[bold #a78bfa]▶ Starting Phase {phase} Step {step}: {step_name}[/]\n\n"
            )

        # Run execution in background thread
        # Use lambda to wrap the function call for run_worker
        self.run_worker(
            lambda: self._execute_with_pty(phase, step, step_name),
            exclusive=True,
            thread=True,
        )

    def _execute_with_pty(self, phase: int, step: int, step_name: str) -> None:
        """Execute Claude Code in a PTY with output streaming to TUI.

        This method runs in a background thread and:
        1. Creates a pseudo-terminal
        2. Runs Claude Code in INTERACTIVE mode
        3. Sends a short command telling Claude what step to run
        4. Claude reads CLAUDE.md and the step prompt file
        5. Streams output to LiveFeedWidget in real-time
        6. User can interact with permission dialogs via keyboard

        Args:
            phase: Phase number (0-4)
            step: Step number (0-3)
            step_name: Human-readable step name
        """
        logger.info(f"Starting PTY execution for Phase {phase} Step {step}")

        # Verify prompt file exists
        prompts_dir = self._project_path / ".laida" / "prompts" / f"phase{phase}"
        prompt_file = None

        for pattern in [f"{step_name}.yaml", f"step{step}_*.yaml", f"step{step}.yaml"]:
            matches = list(prompts_dir.glob(pattern))
            if matches:
                prompt_file = matches[0]
                break

        if not prompt_file or not prompt_file.exists():
            logger.error(f"Prompt file not found in: {prompts_dir}")
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    f"[bold #f87171]✘ Prompt file not found for Phase {phase} Step {step}[/]\n"
                    f"[#64748b]Searched in: {prompts_dir}[/]\n",
                )
            self._is_executing = False
            return

        logger.info(f"Found prompt file: {prompt_file}")

        # Get relative path for display
        try:
            prompt_relative = prompt_file.relative_to(self._project_path)
        except ValueError:
            prompt_relative = prompt_file

        # Simple command - Claude reads CLAUDE.md and knows what to do
        # The CLAUDE.md file has all the instructions for each step
        run_command = f"Run LAIDA Phase {phase} Step {step} ({step_name}). Read {prompt_relative} for the step instructions."

        if self._live_feed:
            self.call_from_thread(
                self._live_feed.write,
                f"[bold #a78bfa]Phase {phase} Step {step}:[/] [#e2e8f0]{step_name}[/]\n"
                f"[#5eead4]Prompt:[/] {prompt_relative}\n\n"
                "[#64748b]Interactúa con Claude usando el teclado.[/]\n"
                "[#64748b]Ctrl+C para parar.[/]\n\n"
                "[#94a3b8]─" * 50 + "[/]\n\n",
            )

        self._telemetry_service.start_task()

        try:
            logger.info("Creating PTY...")
            master_fd, slave_fd = pty.openpty()
            self._pty_master_fd = master_fd
            logger.info(f"PTY created: master_fd={master_fd}, slave_fd={slave_fd}")

            # Set terminal size (larger for better display)
            winsize = struct.pack("HHHH", 50, 150, 0, 0)  # rows, cols
            fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, winsize)

            # Start Claude in interactive mode
            logger.info("Starting Claude process in interactive mode...")
            process = subprocess.Popen(
                ["claude"],  # No flags = interactive mode
                stdin=slave_fd,
                stdout=slave_fd,
                stderr=slave_fd,
                cwd=self._project_path,
                close_fds=True,
                env={**os.environ, "TERM": "xterm-256color"},
            )
            logger.info(f"Claude process started: PID={process.pid}")

            os.close(slave_fd)  # Close slave in parent

            # Wait for Claude to initialize and show its prompt ">"
            # Keep reading until we see the prompt, then send our command
            logger.info("Waiting for Claude prompt...")
            prompt_seen = False
            initial_buffer = ""
            max_wait = 30  # seconds

            start_time = time.time()
            while not prompt_seen and (time.time() - start_time) < max_wait:
                if self._stop_requested:
                    break

                try:
                    ready, _, _ = select.select([master_fd], [], [], 0.5)
                    if ready:
                        data = os.read(master_fd, 8192)
                        if data:
                            text = data.decode("utf-8", errors="replace")
                            initial_buffer += text
                            logger.debug(f"Initial read: {len(data)} bytes")

                            # Show to user
                            if self._live_feed:
                                rich_text = Text.from_ansi(text)
                                self.call_from_thread(
                                    self._live_feed.write, rich_text
                                )

                            # Check if we see the prompt
                            if "> " in text or ">" in text[-10:]:
                                prompt_seen = True
                                logger.info("Claude prompt detected!")
                except OSError as e:
                    logger.warning(f"Error reading initial output: {e}")
                    break

            if not prompt_seen:
                logger.warning("Prompt not detected, sending command anyway...")

            # Small delay to ensure prompt is ready
            time.sleep(0.3)

            # Send the command to Claude
            logger.info(f"Sending command: {run_command[:50]}...")
            try:
                os.write(master_fd, (run_command + "\n").encode())
                logger.info("Command sent successfully")
            except OSError as e:
                logger.error(f"Failed to send command: {e}")

            # Read output in a loop
            output_buffer = ""
            read_count = 0
            logger.info("Starting output reading loop...")
            while True:
                if self._stop_requested:
                    logger.info("Stop requested, terminating process")
                    process.terminate()
                    break

                # Check if process has ended
                ret = process.poll()
                if ret is not None:
                    logger.info(f"Process ended with return code: {ret}")
                    # Read any remaining output
                    try:
                        while True:
                            ready, _, _ = select.select([master_fd], [], [], 0.1)
                            if not ready:
                                break
                            data = os.read(master_fd, 4096)
                            if not data:
                                break
                            text = data.decode("utf-8", errors="replace")
                            logger.debug(f"Final read: {len(data)} bytes")
                            output_buffer += text
                    except OSError:
                        pass
                    break

                # Read available output
                try:
                    ready, _, _ = select.select([master_fd], [], [], 0.1)
                    if ready:
                        data = os.read(master_fd, 4096)
                        if data:
                            read_count += 1
                            text = data.decode("utf-8", errors="replace")
                            logger.debug(f"Read #{read_count}: {len(data)} bytes")
                            output_buffer += text

                            # Process and display output immediately
                            if self._live_feed:
                                # Convert ANSI to Rich Text object
                                rich_text = Text.from_ansi(text)
                                logger.debug(f"Writing {len(text)} bytes to feed")
                                self.call_from_thread(
                                    self._live_feed.write, rich_text
                                )
                            output_buffer = ""  # We showed it all
                except OSError as e:
                    logger.error(f"OSError reading from PTY: {e}")
                    break

            logger.info(f"Output loop finished. Total reads: {read_count}")

            # Write any remaining buffer
            if output_buffer.strip():
                sanitized = self._sanitize_claude_output(output_buffer)
                if sanitized and self._live_feed:
                    self.call_from_thread(self._live_feed.write, sanitized + "\n")

            os.close(master_fd)
            self._pty_master_fd = None

            # Check result
            exit_code = process.returncode
            logger.info(f"Final exit code: {exit_code}")
            expected_dir = self._project_path / "outputs" / f"phase{phase}"
            step_dirs = list(expected_dir.glob(f"step{step}_*"))
            has_output = any((d / "_index.yaml").exists() for d in step_dirs) if step_dirs else False

            success = exit_code == 0 or has_output
            self._telemetry_service.end_task(success, phase, step)

            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    "\n[#94a3b8]─" * 50 + "[/]\n\n",
                )
                if success:
                    self.call_from_thread(
                        self._live_feed.write,
                        f"[bold #10b981]✔ Phase {phase} Step {step} completed![/]\n",
                    )
                else:
                    self.call_from_thread(
                        self._live_feed.write,
                        f"[bold #f87171]✘ Phase {phase} Step {step} failed (exit: {exit_code})[/]\n",
                    )

            self.call_from_thread(self._refresh_after_execution, phase, step)

        except FileNotFoundError as e:
            logger.error(f"FileNotFoundError: {e}")
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    "\n[bold #f87171]✘ Claude Code CLI not found![/]\n"
                    "[#64748b]Install from: https://claude.ai/code[/]\n",
                )
        except Exception as e:
            logger.exception(f"Exception in PTY execution: {e}")
            self._telemetry_service.reset_task()
            if self._live_feed:
                self.call_from_thread(
                    self._live_feed.write,
                    f"\n[bold #f87171]✘ Error: {e}[/]\n",
                )
        finally:
            self._is_executing = False
            self._pty_master_fd = None
            logger.info("PTY execution finished")

    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 - suspends TUI for interactive session."""
        if self._is_executing:
            self.notify("Execution in progress...", title="Wait", timeout=2)
            return

        self._run_claude_interactive()

    def _run_claude_interactive(self) -> None:
        """Run Claude Code interactively by suspending the TUI.

        This gives the user full control of the terminal to interact
        with Claude Code (login, configuration, etc). When Claude exits,
        the TUI resumes automatically.
        """
        with self.suspend():
            try:
                # Clear screen and show instructions
                print("\033[2J\033[H")  # Clear screen, move cursor to top
                print("=" * 60)
                print("  CLAUDE CODE - Interactive Session")
                print("  Exit Claude (Ctrl+C or /exit) to return to LAIDA")
                print("=" * 60)
                print()

                # Run claude interactively - user has full control
                result = subprocess.run(["claude"], check=False)

                print()
                print("=" * 60)
                print(f"  Session ended (exit code: {result.returncode})")
                print("  Returning to LAIDA...")
                print("=" * 60)
                time.sleep(1)

            except FileNotFoundError:
                print("\n❌ Claude Code CLI not found!")
                print("Install from: https://claude.ai/code")
                print("\nPress Enter to continue...")
                input()
            except KeyboardInterrupt:
                print("\n\nSession interrupted. Returning to LAIDA...")
                time.sleep(0.5)

    def _refresh_after_execution(self, phase: int, step: int) -> None:
        """Refresh TUI components after execution completes.

        Updates progress panel, file tree, and shows completion message
        in the live feed.

        Args:
            phase: Phase that was executed
            step: Step that was executed
        """
        # Show completion message in live feed
        if self._live_feed:
            state = self._state_service.detect(self._project_path)
            if state.is_complete:
                self._live_feed.write(
                    f"\n[bold #10b981]✔ Phase {phase} Step {step} completed![/]\n"
                    "[#a78bfa]🎉 All phases complete! Project finished.[/]\n"
                )
            elif state.has_started:
                next_name = self._state_service.get_step_name(
                    state.next_phase, state.next_step
                )
                self._live_feed.write(
                    f"\n[bold #10b981]✔ Phase {phase} Step {step} completed![/]\n"
                    f"[#5eead4]Next: Phase {state.next_phase} Step {state.next_step} "
                    f"({next_name})[/]\n\n"
                    "[#64748b]Press Ctrl+R to continue[/]\n"
                )
            else:
                self._live_feed.write(
                    f"\n[#fbbf24]Phase {phase} Step {step} may not have completed.[/]\n"
                    "[#64748b]Check outputs/ directory for results.[/]\n"
                )

        # Trigger refresh of widgets
        try:
            # Refresh file tree
            file_tree = self.query_one(FileTreeWidget)
            if file_tree:
                file_tree._refresh_tree()
        except Exception:
            pass  # Widget may not exist

        try:
            # Refresh progress panel
            progress = self.query_one(ProgressPanel)
            if progress:
                progress._refresh_state()
        except Exception:
            pass  # Widget may not exist

    def _strip_ansi(self, text: str) -> str:
        """Remove ANSI escape codes from text.

        Args:
            text: Text potentially containing ANSI codes

        Returns:
            Clean text without ANSI codes
        """
        # Comprehensive ANSI escape pattern
        ansi_pattern = re.compile(
            r"\x1b\["  # CSI
            r"[0-9;]*"  # Parameters
            r"[A-Za-z]"  # Command
            r"|\x1b\][^\x07]*\x07"  # OSC sequences
            r"|\x1b[PX^_][^\x1b]*\x1b\\"  # DCS/SOS/PM/APC
            r"|\x1b."  # Other escape sequences
            r"|\x9b[0-9;]*[A-Za-z]"  # 8-bit CSI
        )
        clean = ansi_pattern.sub("", text)

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

        # Handle carriage returns (keep last segment)
        if "\r" in clean:
            lines = []
            for line in clean.split("\n"):
                if "\r" in line:
                    parts = line.split("\r")
                    lines.append(parts[-1])
                else:
                    lines.append(line)
            clean = "\n".join(lines)

        # Escape Rich markup brackets
        clean = clean.replace("[", "\\[").replace("]", "\\]")

        return clean

    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 ALL ANSI escape sequences (comprehensive pattern)
        ansi_pattern = re.compile(
            r"\x1b["  # ESC followed by:
            r"\[\??[0-9;]*[A-Za-z]|"  # CSI sequences: ESC[...X or ESC[?...X
            r"\][^\x07]*\x07|"  # OSC sequences: ESC]...BEL
            r"\([A-Z0-9]|"  # Character set: ESC(B, ESC(0
            r"\)[A-Z0-9]|"  # Character set: ESC)B, ESC)0
            r"[=>]|"  # Keypad modes: ESC= ESC>
            r"[78]|"  # Cursor save/restore: ESC7 ESC8
            r"P[^\x1b]*\x1b\\|"  # DCS sequences: ESCP...ESC\
            r"[A-Z]"  # Simple escapes: ESCD, ESCE, ESCM, etc.
            r"]"
            r"|\x9b[0-9;]*[A-Za-z]"  # 8-bit CSI (0x9B instead of ESC[)
        )
        clean = ansi_pattern.sub("", line)

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

        # Handle carriage return (overwrites line from start)
        if "\r" in clean:
            parts = clean.split("\r")
            clean = parts[-1]  # Keep only the last part after \r

        # Sanitize sensitive paths and tokens
        clean = self._sanitize_sensitive_content(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 _sanitize_sensitive_content(self, text: str) -> str:
        """Remove sensitive paths and tokens from output.

        Args:
            text: Text that may contain sensitive information

        Returns:
            Sanitized text with paths and tokens redacted
        """
        import os

        # Redact home directory paths
        home = os.path.expanduser("~")
        if home and home != "~":
            text = text.replace(home, "~")

        # Redact common token patterns
        patterns = [
            (r"(sk-[a-zA-Z0-9]{20,})", "[REDACTED_SK]"),  # OpenAI/Anthropic style
            (r"(pypi-[a-zA-Z0-9]{20,})", "[REDACTED_PYPI]"),  # PyPI tokens
            (r"(ghp_[a-zA-Z0-9]{20,})", "[REDACTED_GH]"),  # GitHub PAT
            (r"(gho_[a-zA-Z0-9]{20,})", "[REDACTED_GH]"),  # GitHub OAuth
            (r"(github_pat_[a-zA-Z0-9_]{20,})", "[REDACTED_GH]"),  # GitHub fine-grained
            (r"(xox[baprs]-[a-zA-Z0-9-]{20,})", "[REDACTED_SLACK]"),  # Slack tokens
            (r"(AKIA[A-Z0-9]{16})", "[REDACTED_AWS]"),  # AWS Access Key
        ]

        for pattern, replacement in patterns:
            text = re.sub(pattern, replacement, text)

        return text

    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
