import json
import re
import shlex
import subprocess
import time
import uuid
from collections import deque
from pathlib import Path
from threading import Thread

from rich.markdown import Markdown
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Footer, Header, Input, RichLog, Static

from armorer.config import ConfigManager, resolve_armorer_config_dir
from armorer.messaging import InboxOutboxRelay
from armorer.messaging.bridge import (
    _sanitize_bridge_user_text,
    build_local_cli_task_metadata,
    format_queue_cleanup_note,
)


class ArmorerChatApp(App):
    """Textual chat UI for Armorer Core."""

    TITLE = "Armorer Chat"
    SUB_TITLE = "Secure Agent Orchestrator"
    BINDINGS = [
        ("ctrl+l", "clear_chat", "Clear"),
        ("ctrl+r", "focus_input", "Input"),
        ("ctrl+c", "quit", "Quit"),
    ]
    _SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]

    CSS = """
    Screen {
        layout: vertical;
        background: #0b0f14;
        color: #d2dae3;
    }
    Header {
        background: #111923;
        color: #8fd3ff;
    }
    Footer {
        background: #111923;
        color: #7fb1d6;
    }
    #hero {
        padding: 1 2;
        margin: 0 1 1 1;
        background: #101720;
        color: #8fd3ff;
        border: round #1d3348;
    }
    #main {
        height: 1fr;
        margin: 0 1;
    }
    #chatlog {
        height: 1fr;
        min-width: 60;
        border: round #2c4f6d;
        padding: 1;
        background: #0e141c;
    }
    #sidebar {
        width: 34;
        min-width: 28;
        border: round #2a3d52;
        background: #0e141c;
        padding: 1;
        color: #9fb4c8;
    }
    #statusbar {
        margin: 1 1 0 1;
        padding: 0 1;
        background: #101720;
        border: round #2a3d52;
        color: #b8c7d6;
    }
    #input {
        margin: 1 1 1 1;
        border: round #2c4f6d;
        background: #0e141c;
        color: #e7f1fb;
    }
    #input:focus {
        border: round #59b8ff;
    }
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Must exist before compose(), which renders sidebar immediately.
        self._activity_lines: deque[str] = deque(maxlen=8)
        self.cli_metadata = build_local_cli_task_metadata()
        self.pending_ui_action: dict | None = None

    def compose(self) -> ComposeResult:
        yield Header(show_clock=True, icon="🛡")
        with Vertical():
            yield Static(
                "[bold #8fd3ff]Armorer Core[/bold #8fd3ff]  "
                "[#7fb1d6]Secure interactive session[/#7fb1d6]\n"
                "[dim]Type requests naturally. Use 'yes'/'no' for approvals. Ctrl+L clears chat.[/dim]",
                id="hero",
            )
            with Horizontal(id="main"):
                yield RichLog(id="chatlog", wrap=True, highlight=True, markup=True)
                yield Static(self._render_sidebar(), id="sidebar")
            yield Static("Status: Ready", id="statusbar")
            yield Input(placeholder="Type a message (or 'exit')...", id="input")
        yield Footer()

    def on_mount(self) -> None:
        self.config_dir = resolve_armorer_config_dir()
        self.inbox = self.config_dir / "inbox"
        self.outbox = self.config_dir / "outbox"
        self.status_dir = self.config_dir / "status"
        self.inbox.mkdir(parents=True, exist_ok=True)
        self.outbox.mkdir(parents=True, exist_ok=True)
        self.status_dir.mkdir(parents=True, exist_ok=True)
        cm = ConfigManager()
        self.relay = InboxOutboxRelay(
            config_dir=self.config_dir,
            signing_key=cm.get_key("ARMORER_TASK_SIGNING_KEY"),
        )
        self.cli_metadata = build_local_cli_task_metadata()
        self.busy = False
        self._busy_started_at = 0.0
        self._busy_message = ""
        self._spinner_idx = 0
        self._activity_lines: deque[str] = deque(maxlen=8)
        self.pending_ui_action: dict | None = None
        log = self.query_one("#chatlog", RichLog)
        log.write("[bold #8fd3ff]Welcome to Armorer Chat[/bold #8fd3ff]")
        log.write("[dim]Connected to Armorer Core. Ask for install, run, repair, or hardening tasks.[/dim]\n")
        self.query_one("#input", Input).focus()
        self.set_interval(0.15, self._tick_busy_status)

    def _render_sidebar(self) -> str:
        activity = "\n".join(f"[dim]• {line}[/dim]" for line in self._activity_lines) or "[dim]No activity yet[/dim]"
        return (
            "[bold]Session Guide[/bold]\n\n"
            "[#8fd3ff]•[/#8fd3ff] [bold]Onboard[/bold]: first-time account/channel setup\n"
            "[#8fd3ff]•[/#8fd3ff] [bold]Configure[/bold]: provider/model/env setup\n"
            "[#8fd3ff]•[/#8fd3ff] [bold]Doctor[/bold]: diagnostics and quick fixes\n\n"
            "[bold]Commands[/bold]\n"
            "[dim]exit[/dim]  quit chat\n"
            "[dim]yes/no[/dim]  approval replies\n"
            "[dim]Ctrl+R[/dim]  focus input\n"
            "[dim]Ctrl+L[/dim]  clear log\n\n"
            "[bold]Live Activity[/bold]\n"
            f"{activity}"
        )

    def _record_activity(self, text: str) -> None:
        line = " ".join(str(text or "").split())
        if not line:
            return
        if not self._activity_lines or self._activity_lines[-1] != line:
            self._activity_lines.append(line[:120])
            self.query_one("#sidebar", Static).update(self._render_sidebar())

    def _tick_busy_status(self) -> None:
        if not self.busy:
            return
        self._spinner_idx = (self._spinner_idx + 1) % len(self._SPINNER_FRAMES)
        elapsed = int(max(0, time.time() - self._busy_started_at))
        frame = self._SPINNER_FRAMES[self._spinner_idx]
        detail = self._busy_message or "Armorer is thinking..."
        self.query_one("#statusbar", Static).update(
            f"Status: {frame} {detail}  [dim]({elapsed}s)[/dim]"
        )

    def _append(self, role: str, text: str) -> None:
        log = self.query_one("#chatlog", RichLog)
        role_style = {
            "user": "[black on #8fd3ff] YOU [/black on #8fd3ff]",
            "assistant": "[black on #7cf1c4] ARMORER [/black on #7cf1c4]",
            "system": "[black on #ffd580] SYSTEM [/black on #ffd580]",
        }.get(role, "[bold] INFO [/bold]")
        log.write(f"{role_style}:")
        if role == "assistant":
            log.write(Markdown(text))
        else:
            log.write(text)
        log.write("")
        log.scroll_end(animate=False)

    def _set_busy(self, busy: bool, placeholder: str = "") -> None:
        self.busy = busy
        inp = self.query_one("#input", Input)
        status = self.query_one("#statusbar", Static)
        inp.disabled = busy
        if placeholder:
            inp.placeholder = placeholder
            self._busy_message = placeholder
            self._record_activity(placeholder)
        if busy and self._busy_started_at == 0.0:
            self._busy_started_at = time.time()
            self._spinner_idx = 0
        if not busy:
            inp.placeholder = "Type a message (or 'exit')..."
            status.update("Status: Ready")
            self._busy_started_at = 0.0
            self._busy_message = ""
            inp.focus()

    def action_clear_chat(self) -> None:
        self.query_one("#chatlog", RichLog).clear()
        self._append("system", "Chat log cleared.")

    def action_focus_input(self) -> None:
        self.query_one("#input", Input).focus()

    def on_input_submitted(self, event: Input.Submitted) -> None:
        if self.busy:
            return
        raw_text = event.value or ""
        text = raw_text.strip()
        self.query_one("#input", Input).value = ""
        if self.pending_ui_action:
            self._handle_ui_action_input(raw_text)
            return
        if not text:
            return
        if text.lower() in {"exit", "quit", "bye"}:
            self.exit()
            return
        self._append("user", text)
        self._set_busy(True, "Armorer is thinking...")
        Thread(target=self._process_prompt, args=(text,), daemon=True).start()

    @staticmethod
    def _is_sensitive_text_ui_action(action: dict) -> bool:
        if str(action.get("type", "")).strip().lower() != "text":
            return False
        if bool(action.get("sensitive")):
            return True
        haystacks = [
            str(action.get("message", "") or ""),
            str(action.get("label", "") or ""),
            str(action.get("placeholder", "") or ""),
        ]
        sensitive_terms = ("token", "secret", "password", "api key", "api_key")
        if any(term in text.lower() for text in haystacks for term in sensitive_terms):
            return True
        default = str(action.get("default", "") or "").strip()
        if not default:
            return False
        low_default = default.lower()
        if low_default.startswith(("sk-", "xoxb-", "ghp_", "gho_", "ghu_", "ghs_", "pat_", "api_", "tok_")):
            return True
        if re.fullmatch(r"(?i)AIza[0-9A-Za-z\-_]{20,}", default):
            return True
        if ":" in default:
            left, right = default.split(":", 1)
            if left.isdigit() and len(right.strip()) >= 8:
                return True
        compact = default.strip()
        if " " not in compact and len(compact) >= 24 and all(ch.isalnum() or ch in "._:-" for ch in compact):
            return True
        return False

    @staticmethod
    def _ui_action_prompt_message(action: dict) -> str:
        raw_message = action.get("message")
        if raw_message is None:
            return "Input required"
        message = str(raw_message).strip()
        return message or "Input required"

    def _submit_followup(self, prompt: str, *, display_text: str | None = None) -> None:
        safe_display = _sanitize_bridge_user_text(str(display_text or prompt))
        self._append("user", safe_display)
        self._set_busy(True, "Armorer is thinking...")
        Thread(target=self._process_prompt, args=(prompt,), daemon=True).start()

    def _set_input_secret_mode(self, enabled: bool) -> None:
        try:
            self.query_one("#input", Input).password = bool(enabled)
        except Exception:
            return

    def _set_pending_action(self, action: dict) -> None:
        self.pending_ui_action = action
        self._set_input_secret_mode(False)
        atype = str(action.get("type", "")).strip().lower()
        msg = _sanitize_bridge_user_text(self._ui_action_prompt_message(action))

        if atype == "select":
            opts = action.get("options", [])
            if isinstance(opts, list) and opts:
                safe_lines = [
                    f"{i+1}. {_sanitize_bridge_user_text(str(opt))[:120]}"
                    for i, opt in enumerate(opts)
                ]
                self._append(
                    "system",
                    f"{msg}\n\n"
                    + "\n".join(safe_lines)
                    + "\n\nType option number or exact value.\nType `/cancel` to abort.",
                )
                self._set_busy(False, "Select an option")
                return
        if atype == "confirm":
            self._append("system", f"{msg}\n\nType `yes` or `no`.\nType `/cancel` to abort.")
            self._set_busy(False, "Confirmation required")
            return
        if atype == "text":
            sensitive = self._is_sensitive_text_ui_action(action)
            self._set_input_secret_mode(sensitive)
            default = str(action.get("default", "") or "").strip()
            if default and not sensitive:
                self._append("system", f"{msg}\n[dim]Default: {default}[/dim]\n[dim]Type `/cancel` to abort.[/dim]")
            elif default and sensitive:
                self._append(
                    "system",
                    f"{msg}\n[dim]Stored value available; leave blank to keep it.[/dim]\n[dim]Type `/cancel` to abort.[/dim]",
                )
            else:
                self._append("system", f"{msg}\n[dim]Type `/cancel` to abort.[/dim]")
            self._set_busy(False, "Input required")
            return
        if atype == "interactive_command":
            cmd = str(action.get("command", "")).strip()
            reason = _sanitize_bridge_user_text(str(action.get("reason", "")).strip())
            display_cmd = _sanitize_bridge_user_text(cmd)
            if not cmd:
                self.pending_ui_action = None
                self._append("system", "Interactive command could not be run because no command was provided.")
                self._set_busy(False)
                return
            hint = f"\n[dim]{reason}[/dim]" if reason else ""
            self._append(
                "system",
                f"This step requires a real terminal session.{hint}\n\nRun now?\nType `yes` to run:\n[bold]{display_cmd}[/bold]\nType `no` to cancel.\nType `/cancel` to abort.",
            )
            self._set_busy(False, "Interactive command pending")
            return

        # Unknown action: clear and continue safely.
        self.pending_ui_action = None
        self._append("system", f"Unsupported ui_action: {atype}. Please continue with plain text input.")
        self._set_busy(False)

    def _handle_ui_action_input(self, text: str) -> None:
        action = self.pending_ui_action or {}
        self.pending_ui_action = None
        self._set_input_secret_mode(False)
        atype = str(action.get("type", "")).strip().lower()
        value = text.strip()
        if value.lower() == "/cancel":
            if atype == "interactive_command":
                self._append("system", "Interactive command canceled.")
                return
            self._submit_followup("cancel", display_text="/cancel")
            return

        if atype == "select":
            opts = action.get("options", [])
            selected = ""
            if isinstance(opts, list):
                if value.isdigit():
                    idx = int(value) - 1
                    if 0 <= idx < len(opts):
                        selected = str(opts[idx])
                else:
                    for opt in opts:
                        opt_text = str(opt)
                        if value == opt_text or value.lower() == opt_text.lower():
                            selected = opt_text
                            break
            if not selected:
                self.pending_ui_action = action
                self._append("system", "Please choose one of the listed options.")
                self._set_busy(False, "Select an option")
                return
            self._submit_followup(f"UI selection: {selected}")
            return

        if atype == "confirm":
            normalized = value.lower()
            if normalized in {"y", "yes", "true", "1", "ok", "confirm"}:
                self._submit_followup("UI confirmation: yes")
                return
            if normalized in {"n", "no", "false", "0", "cancel"}:
                self._submit_followup("UI confirmation: no")
                return
            self.pending_ui_action = action
            self._append("system", "Please reply `yes` or `no`.")
            self._set_busy(False, "Confirmation required")
            return

        if atype == "text":
            if not value:
                default = str(action.get("default", "") or "").strip()
                if default:
                    value = "skip" if self._is_sensitive_text_ui_action(action) else default
                else:
                    self._set_pending_action(action)
                    return
            prompt = f"UI input: {value}"
            display_text = prompt if prompt.lower() == "ui input: skip" else (
                "UI input: [redacted]" if self._is_sensitive_text_ui_action(action) else prompt
            )
            self._submit_followup(prompt, display_text=display_text)
            return

        if atype == "interactive_command":
            normalized = value.lower()
            if normalized in {"n", "no", "cancel", "stop"}:
                self._append("system", "Interactive command canceled.")
                return
            if normalized not in {"y", "yes", "run", "ok"}:
                self.pending_ui_action = action
                self._append("system", "Please reply `yes` to run or `no` to cancel.")
                self._set_busy(False, "Interactive command pending")
                return
            cmd = str(action.get("command", "")).strip()
            if not cmd:
                self._append("system", "Interactive command could not be run because no command was provided.")
                return
            self._run_interactive_command_and_resume(cmd)
            return

        self._submit_followup(value)

    def _run_interactive_command_and_resume(self, command: str) -> None:
        self._append("system", f"Launching interactive command in terminal: {_sanitize_bridge_user_text(command)}")
        self._set_busy(True, "Running interactive command...")

        def worker():
            rc = 1
            summary = ""
            try:
                args = shlex.split(command)
            except ValueError as exc:
                safe_detail = _sanitize_bridge_user_text(str(exc).strip() or exc.__class__.__name__)[:300]
                summary = f"Interactive command failed to parse: {safe_detail}"
            except Exception as e:
                safe_detail = _sanitize_bridge_user_text(str(e).strip() or e.__class__.__name__)[:300]
                summary = f"Interactive command failed: {safe_detail}"
            else:
                # Temporarily suspend Textual UI and hand over terminal control.
                try:
                    with self.suspend():
                        rc = subprocess.run(args).returncode
                    summary = f"Interactive command finished with exit code {rc}."
                except Exception as e:
                    safe_detail = _sanitize_bridge_user_text(str(e).strip() or e.__class__.__name__)[:300]
                    summary = f"Interactive command failed: {safe_detail}"

            safe_summary = _sanitize_bridge_user_text(summary)[:300] if summary else ""
            self.call_from_thread(self._set_busy, False)
            self.call_from_thread(self._append, "system", safe_summary)
            self.call_from_thread(self._record_activity, safe_summary)

        Thread(target=worker, daemon=True).start()

    def _process_prompt(self, prompt: str) -> None:
        try:
            task_id = self.relay.submit_task(prompt=prompt, source="cli", metadata=self.cli_metadata)
            cleanup_note = format_queue_cleanup_note(getattr(self.relay, "last_submit_cleanup", []))
            if cleanup_note:
                self.call_from_thread(self._append, "system", cleanup_note)
                self.call_from_thread(self._record_activity, cleanup_note)
            result = self.relay.wait_for_response(
                task_id,
                timeout=300,
                poll_interval=0.5,
                status_callback=lambda msg: self.call_from_thread(
                    self._set_busy,
                    True,
                    f"{_sanitize_bridge_user_text(str(msg or ''))[:120]}",
                ),
            )
            raw_result = result.get("raw")
            if isinstance(raw_result, dict):
                raw = raw_result.get("response", "{}")
                parsed = None
                if isinstance(raw, dict):
                    parsed = raw
                else:
                    try:
                        parsed = json.loads(raw)
                    except Exception:
                        parsed = None
                if isinstance(parsed, dict):
                    raw_message = parsed.get("message")
                    if raw_message is None or (isinstance(raw_message, str) and not raw_message.strip()):
                        raw_message = parsed.get("error")
                    if raw_message is None or (isinstance(raw_message, str) and not raw_message.strip()):
                        raw_message = result.get("message") or "Input required."
                    message = str(raw_message)
                    requires_confirmation = bool(parsed.get("requires_confirmation"))
                    ui_action = parsed.get("ui_action") if isinstance(parsed.get("ui_action"), dict) else None
                else:
                    if isinstance(parsed, str):
                        parsed_text = parsed.strip()
                        message = parsed_text or str(result.get("message") or "Input required.")
                    elif isinstance(parsed, (int, float, bool)):
                        message = str(parsed)
                    elif isinstance(parsed, list):
                        message = json.dumps(parsed)
                    else:
                        raw_text = str(raw or "").strip()
                        if raw_text in {"", "{}", "null"}:
                            message = str(result.get("message") or "Input required.")
                        else:
                            message = raw_text
                    requires_confirmation = False
                    ui_action = None
            else:
                message = str(result.get("message") or "Timed out waiting for Armorer Core response.")
                requires_confirmation = False
                ui_action = None
            safe_message = _sanitize_bridge_user_text(str(message or ""))
            self.call_from_thread(self._append, "assistant", safe_message)
            if ui_action:
                self.call_from_thread(self._set_pending_action, ui_action)
                return
            if requires_confirmation:
                self.call_from_thread(
                    self._append,
                    "system",
                    "This action requires confirmation. Reply with 'yes' to proceed or 'no' to cancel.",
                )
        except Exception as e:
            safe_detail = _sanitize_bridge_user_text(str(e).strip() or e.__class__.__name__)[:300]
            self.call_from_thread(self._append, "system", f"Failed to contact Armorer Core: {safe_detail}")
        finally:
            self.call_from_thread(self._set_busy, False)
