"""AiCippY Textual TUI — dark-themed interactive agent with session memory and auto-execution."""

from __future__ import annotations

import asyncio
import json
import platform
import re
import subprocess
import time
from pathlib import Path
from typing import Any

from textual import on, work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.css.query import NoMatches
from textual.reactive import reactive
from textual.widgets import Button, Static, TextArea

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DISPLAY_NAMES: dict[str, str] = {"odin": "OdiN", "arjuna": "ArjunA"}
MEMORY_FILE = Path.home() / ".aicippy" / "session_memory.json"
MAX_AUTO_EXEC_TURNS = 20
SHELL_BLOCK_RE = re.compile(
    r"```(?:powershell|bash|sh|shell|cmd)\n(.*?)```",
    re.DOTALL,
)


def _dn(key: str) -> str:
    return DISPLAY_NAMES.get(key.lower(), key) if key else "OdiN"


# ---------------------------------------------------------------------------
# Session Memory — persists across restarts
# ---------------------------------------------------------------------------


class SessionMemory:
    """Persistent key-value memory for agent environment awareness."""

    def __init__(self) -> None:
        self._data: dict[str, Any] = {}
        self._load()

    def _load(self) -> None:
        try:
            if MEMORY_FILE.exists():
                self._data = json.loads(MEMORY_FILE.read_text("utf-8"))
        except Exception:
            self._data = {}

    def save(self) -> None:
        try:
            MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
            MEMORY_FILE.write_text(json.dumps(self._data, indent=2), "utf-8")
        except Exception:
            pass

    def get(self, key: str, default: Any = None) -> Any:
        return self._data.get(key, default)

    def set(self, key: str, value: Any) -> None:
        self._data[key] = value
        self.save()

    def get_env_context(self) -> str:
        """Return environment context string for the agent."""
        parts: list[str] = []
        for k, v in self._data.items():
            if k.startswith("_"):
                continue
            parts.append(f"  {k}: {v}")
        return "\n".join(parts) if parts else "  (no stored context)"


def _detect_environment(mem: SessionMemory) -> None:
    """Auto-detect machine environment and store in permanent memory."""
    mem.set("os", f"{platform.system()} {platform.release()} ({platform.machine()})")
    mem.set("python", platform.python_version())
    mem.set("computer", platform.node())
    mem.set("user", str(Path.home().name))
    mem.set("cwd", str(Path.cwd()))
    mem.set("project", Path.cwd().name)

    # Check AWS
    try:
        r = subprocess.run(
            ["aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text"],
            capture_output=True, text=True, timeout=10, check=False,
        )
        if r.returncode == 0 and r.stdout.strip():
            mem.set("aws_account", r.stdout.strip())
            mem.set("aws_configured", True)
        else:
            mem.set("aws_configured", False)
    except Exception:
        mem.set("aws_configured", False)

    # Check gcloud
    try:
        r = subprocess.run(
            ["gcloud", "config", "get-value", "project"],
            capture_output=True, text=True, timeout=10, check=False,
        )
        if r.returncode == 0 and r.stdout.strip():
            mem.set("gcp_project", r.stdout.strip())
            mem.set("gcp_configured", True)
        else:
            mem.set("gcp_configured", False)
    except Exception:
        mem.set("gcp_configured", False)

    mem.save()


# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------

APP_CSS = """
Screen { background: #0d1117; }

#header-bar {
    dock: top; height: 3; background: #161b22;
    border-bottom: solid #30363d; padding: 0 1;
}
#header-left { width: 1fr; height: 3; content-align: left middle; padding: 0 1; }
#header-right { width: auto; height: 3; content-align: right middle; padding: 0 1; }

#info-bar {
    dock: top; height: 1; background: #0d1117; color: #6b7280; padding: 0 2;
}

#chat-scroll { background: #0d1117; padding: 0 1; }

.msg-user {
    margin: 1 0 0 4; padding: 1 2;
    background: #1a3a5c; color: #e5e7eb; border: round #264d73;
}
.msg-agent {
    margin: 1 4 0 0; padding: 1 2;
    background: #1c1c2e; color: #d1d5db; border: round #30363d;
}
.msg-system { margin: 0 0; padding: 0 2; color: #a855f7; }
.msg-success { color: #00ff88; padding: 0 2; }
.msg-error { color: #ef4444; padding: 0 2; }
.msg-info { color: #6b7280; padding: 0 2; }
.msg-action { margin: 0 2; padding: 0 2; color: #f59e0b; }
.msg-exec {
    margin: 0 2; padding: 0 2; color: #22d3ee;
    background: #0f1923; border: round #1e3a50;
}

#input-area {
    dock: bottom; height: auto; max-height: 12;
    background: #161b22; border-top: solid #30363d; padding: 1 1;
}
#input-row { height: auto; min-height: 3; max-height: 8; padding: 0 1; }
#prompt-input {
    width: 1fr; min-height: 3; max-height: 6;
    border: round #30363d; background: #0d1117; color: #e5e7eb;
}
#prompt-input:focus { border: round #6366f1; }
#send-btn {
    width: 8; min-width: 8; height: 3; margin: 0 0 0 1;
    background: #6366f1; color: #fff; text-style: bold; border: none;
}
#send-btn:hover { background: #4f46e5; }

#chips-row { height: 1; padding: 0 2; }
.chip {
    width: auto; margin: 0 1 0 0; padding: 0 1;
    background: #1e293b; color: #9ca3af; border: round #30363d;
}
.chip:hover { background: #334155; color: #22d3ee; }

#status-bar {
    dock: bottom; height: 1; background: #080b10; color: #6b7280; padding: 0 2;
}
"""


# ---------------------------------------------------------------------------
# Widgets
# ---------------------------------------------------------------------------


class HeaderBar(Horizontal):
    """Title bar."""
    def __init__(self, model: str = "OdiN", user: str = "") -> None:
        super().__init__(id="header-bar")
        self._model = model
        self._user = user

    def compose(self) -> ComposeResult:
        try:
            from aicippy import __version__
        except Exception:
            __version__ = "?"
        brand = "[bold #00ff88]Ai[/][bold #22d3ee]Cipp[/][bold #00ff88]Y[/]"
        left = f"{brand} [#6b7280]v{__version__}[/]  Agent: [bold #f59e0b]{self._model}[/]"
        right_parts = ""
        if self._user:
            right_parts += f"[#22d3ee]{self._user}[/]  "
        right_parts += "[bold #00ff88]● ACTIVE[/]  [bold #22d3ee]╭◉‿◉╮[/]✨"
        yield Static(left, id="header-left")
        yield Static(right_parts, id="header-right")


class InfoBar(Static):
    """Info line."""
    def __init__(self, mem: SessionMemory) -> None:
        proj = mem.get("project", Path.cwd().name)
        cwd = mem.get("cwd", str(Path.cwd()))
        home = str(Path.home())
        short = cwd.replace(home, "~", 1) if cwd.startswith(home) else cwd
        aws = f"  [bold #f59e0b]AWS:{mem.get('aws_account', '?')}[/]" if mem.get("aws_configured") else ""
        markup = f"[#6b7280]project:[/] [bold #22d3ee]{proj}[/]  [#6b7280]CWD:[/] [bold #ffd93d]{short}[/]{aws}"
        super().__init__(markup, id="info-bar")


class ChatMessage(Static):
    """Chat bubble."""
    pass


class CommandChip(Button):
    """Chip."""
    def __init__(self, cmd: str) -> None:
        super().__init__(cmd, classes="chip")


class StatusBar(Static):
    """Status bar."""
    tokens: reactive[int] = reactive(0)
    latency: reactive[int] = reactive(0)
    session_start: reactive[float] = reactive(0.0)
    turns: reactive[int] = reactive(0)

    def __init__(self) -> None:
        super().__init__(id="status-bar")
        self.session_start = time.time()

    def render(self) -> str:
        dur = int(time.time() - self.session_start)
        m, s = divmod(dur, 60)
        h, m = divmod(m, 60)
        ds = f"{h}h{m}m" if h else f"{m}m{s}s"
        lat = f"{self.latency}ms" if self.latency > 0 else "--"
        return (
            f"  F1 Help  Ctrl+L Clear  Ctrl+C Quit"
            f"    ⚡ Tokens: {self.tokens:,}"
            f"  Latency: {lat}"
            f"  Turns: {self.turns}"
            f"  Session: {ds}"
        )


# ---------------------------------------------------------------------------
# Main App
# ---------------------------------------------------------------------------


class AiCippyApp(App):
    """TUI App."""
    TITLE = "AiCippY"
    CSS = APP_CSS
    BINDINGS: list[Binding] = [  # noqa: RUF012
        Binding("ctrl+c", "quit", "Quit", show=False),
        Binding("ctrl+l", "clear_chat", "Clear"),
        Binding("escape", "focus_input", "Focus", show=False),
    ]

    def __init__(
        self, model: str = "odin", user_email: str = "",
        initial_prompt: str = "",
    ) -> None:
        super().__init__()
        self._model = _dn(model)
        self._user_email = user_email
        self._initial_prompt = initial_prompt
        self._agent_client: Any = None
        self._tokens = 0
        self._turns = 0
        self._conversation: list[dict[str, str]] = []
        self._memory = SessionMemory()

    def compose(self) -> ComposeResult:
        yield HeaderBar(model=self._model, user=self._user_email)
        yield InfoBar(self._memory)

        with VerticalScroll(id="chat-scroll"):
            ts = time.strftime("%H:%M:%S")
            _init = f"{self._model} Agent initialized"
            _m = f"[#4b5563]{ts}[/]  [bold #a855f7]●[/]"
            yield ChatMessage(f"{_m} [#a855f7]{_init}[/]", classes="msg-system")
            _os = self._memory.get("os", f"{platform.system()}")
            _py = platform.python_version()
            yield ChatMessage(
                f"[#4b5563]{ts}[/]  [#6b7280]├─ {_os} | Py {_py}[/]",
                classes="msg-info",
            )
            if self._memory.get("aws_configured"):
                _aid = self._memory.get("aws_account", "?")
                yield ChatMessage(
                    f"[#4b5563]{ts}[/]  [bold #00ff88]✓ AWS: {_aid}[/]",
                    classes="msg-success",
                )
            if self._memory.get("gcp_configured"):
                _gid = self._memory.get("gcp_project", "?")
                yield ChatMessage(
                    f"[#4b5563]{ts}[/]  [bold #00ff88]✓ GCP: {_gid}[/]",
                    classes="msg-success",
                )
            yield ChatMessage(f"[#4b5563]{ts}[/]  [bold #00ff88]✓[/] [#00ff88]Ready[/]", classes="msg-success")

        with Vertical(id="input-area"):
            with Horizontal(id="input-row"):
                yield TextArea(id="prompt-input", language=None, theme="monokai", show_line_numbers=False)
                yield Button("SEND", id="send-btn", variant="primary")
            with Horizontal(id="chips-row"):
                for cmd in ["/build", "/deploy", "/test", "/status", "/clear"]:
                    yield CommandChip(cmd)

        yield StatusBar()

    def on_mount(self) -> None:
        ta = self.query_one("#prompt-input", TextArea)
        ta.focus()
        # Auto-detect environment in background
        self._detect_env()
        if self._initial_prompt:
            self.call_after_refresh(self._send_initial)

    @work(thread=True)
    def _detect_env(self) -> None:
        _detect_environment(self._memory)
        # Refresh info bar with detected env
        try:
            info = self.query_one("#info-bar", InfoBar)
            proj = self._memory.get("project", "?")
            cwd = self._memory.get("cwd", "?")
            home = str(Path.home())
            short = cwd.replace(home, "~", 1) if cwd.startswith(home) else cwd
            _aid = self._memory.get("aws_account", "")
            aws = f"  [bold #f59e0b]AWS:{_aid}[/]" if _aid else ""
            info.update(
                f"[#6b7280]project:[/] [bold #22d3ee]{proj}[/]"
                f"  [#6b7280]CWD:[/] [bold #ffd93d]{short}[/]{aws}"
            )
        except NoMatches:
            pass

    def _send_initial(self) -> None:
        ta = self.query_one("#prompt-input", TextArea)
        ta.load_text(self._initial_prompt)
        self._handle_send()

    def _handle_send(self) -> None:
        ta = self.query_one("#prompt-input", TextArea)
        text = ta.text.strip()
        if not text:
            return
        ta.clear()

        if text.lower() in {"quit", "exit", "q", "/quit", "/exit"}:
            self.exit()
            return

        self._add_user_msg(text)
        self._conversation.append({"role": "user", "content": text})
        self._invoke_agent(text)

    # Key binding: Ctrl+Enter to send (Enter for newline)
    def on_key(self, event: Any) -> None:
        if event.key in {"ctrl+j", "ctrl+enter"}:
            self._handle_send()
            event.prevent_default()
            event.stop()

    @on(Button.Pressed, "#send-btn")
    def _on_send_click(self) -> None:
        self._handle_send()

    @on(Button.Pressed, ".chip")
    def _on_chip_click(self, event: Button.Pressed) -> None:
        cmd = str(event.button.label)
        if cmd == "/clear":
            self.action_clear_chat()
        elif cmd == "/status":
            ctx = self._memory.get_env_context()
            self._add_system_msg(f"Environment:\n{ctx}")
        else:
            ta = self.query_one("#prompt-input", TextArea)
            ta.load_text(cmd + " ")
            ta.focus()

    # -- Message rendering --

    @staticmethod
    def _esc(text: str) -> str:
        """Escape Rich markup in text to prevent parse errors."""
        return text.replace("[", "\\[")

    def _add_user_msg(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        scroll = self.query_one("#chat-scroll", VerticalScroll)
        safe = self._esc(text)
        msg = ChatMessage(
            f"[bold #86efac]You ❯[/]  {safe}\n[#4b5563 italic]{ts}[/]",
            classes="msg-user",
        )
        scroll.mount(msg)
        msg.scroll_visible()

    def _add_agent_msg(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        safe = self._esc(text)
        scroll = self.query_one("#chat-scroll", VerticalScroll)
        msg = ChatMessage(
            f"[bold #22d3ee]{self._model} ❯[/]  {safe}\n[#4b5563 italic]{ts}[/]",
            classes="msg-agent",
        )
        scroll.mount(msg)
        msg.scroll_visible()

    def _add_system_msg(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        safe = self._esc(text)
        scroll = self.query_one("#chat-scroll", VerticalScroll)
        msg = ChatMessage(
            f"[#4b5563]{ts}[/]  [bold #a855f7]●[/] [#a855f7]{safe}[/]",
            classes="msg-system",
        )
        scroll.mount(msg)
        msg.scroll_visible()

    def _add_exec_msg(self, cmd: str, output: str, success: bool) -> None:
        ts = time.strftime("%H:%M:%S")
        scroll = self.query_one("#chat-scroll", VerticalScroll)
        icon = "[bold #00ff88]✓[/]" if success else "[bold #ef4444]✗[/]"
        safe_cmd = self._esc(cmd[:80])
        safe_out = self._esc(output[:300]) if output else "(no output)"
        msg = ChatMessage(
            f"[#4b5563]{ts}[/]  {icon} [bold #22d3ee]$ {safe_cmd}[/]\n[#9ca3af]{safe_out}[/]",
            classes="msg-exec",
        )
        scroll.mount(msg)
        msg.scroll_visible()

    # -- Agent invocation with auto-execution loop --

    @work(thread=True)
    def _invoke_agent(self, text: str) -> None:
        """Call agent, detect commands in response, execute, feed back — loop."""
        try:
            from aicippy.agents.bedrock_agent import BedrockAgentClient

            if self._agent_client is None:
                self._agent_client = BedrockAgentClient()

            self.call_from_thread(self._add_system_msg, "Processing...")

            # Build context with environment awareness
            env_ctx = self._memory.get_env_context()
            context_prefix = (
                f"[Environment]\n{env_ctx}\n\n"
                f"[Working Directory] {self._memory.get('cwd', str(Path.cwd()))}\n\n"
            )

            # Include recent conversation for context
            recent = self._conversation[-10:]  # Last 10 messages
            prompt = context_prefix + text

            turn = 0
            current_prompt = prompt

            while turn < MAX_AUTO_EXEC_TURNS:
                turn += 1
                self._turns += 1
                start_t = time.time()

                # Call agent
                loop = asyncio.new_event_loop()
                try:
                    result = loop.run_until_complete(
                        self._agent_client.invoke(current_prompt)
                    )
                finally:
                    loop.close()

                elapsed = int((time.time() - start_t) * 1000)
                self._tokens += len(result.split()) if result else 0

                # Update status
                try:
                    sb = self.query_one(StatusBar)
                    sb.tokens = self._tokens
                    sb.latency = elapsed
                    sb.turns = self._turns
                except NoMatches:
                    pass

                if not result:
                    self.call_from_thread(self._add_system_msg, "No response.")
                    break

                # Show agent response
                self.call_from_thread(self._add_agent_msg, result)
                self._conversation.append({"role": "assistant", "content": result})

                # Detect shell blocks to auto-execute
                shell_blocks = SHELL_BLOCK_RE.findall(result)
                if not shell_blocks:
                    # No commands — agent is done talking
                    break

                # Execute commands
                all_outputs: list[str] = []
                for block in shell_blocks:
                    cmds = [c.strip() for c in block.strip().split("\n") if c.strip() and not c.strip().startswith("#")]
                    for cmd in cmds:
                        self.call_from_thread(self._add_system_msg, f"Executing: {cmd[:80]}")
                        try:
                            if platform.system() == "Windows":
                                r = subprocess.run(
                                    ["powershell", "-ExecutionPolicy", "Bypass", "-NoProfile", "-Command", cmd],
                                    capture_output=True, text=True, timeout=30, check=False,
                                )
                            else:
                                r = subprocess.run(
                                    ["bash", "-c", cmd],
                                    capture_output=True, text=True, timeout=30, check=False,
                                )
                            out = (r.stdout or "").strip()
                            err = (r.stderr or "").strip()
                            output = out if out else err
                            success = r.returncode == 0
                            self.call_from_thread(self._add_exec_msg, cmd, output, success)
                            all_outputs.append(f"$ {cmd}\nExit: {r.returncode}\n{output[:500]}")
                        except subprocess.TimeoutExpired:
                            self.call_from_thread(self._add_exec_msg, cmd, "TIMEOUT (30s)", False)
                            all_outputs.append(f"$ {cmd}\nTIMEOUT")
                        except Exception as exc:
                            self.call_from_thread(self._add_exec_msg, cmd, str(exc), False)
                            all_outputs.append(f"$ {cmd}\nERROR: {exc}")

                # Feed results back to agent for next turn
                feedback = "\n\n".join(all_outputs)
                current_prompt = f"[Command Results]\n{feedback}\n\nContinue. If all done, say DONE."
                self._conversation.append({"role": "user", "content": current_prompt})

                # Check if agent said DONE
                if "DONE" in result.upper() and not shell_blocks:
                    break

        except Exception as exc:
            self.call_from_thread(self._add_system_msg, f"Error: {exc}")

        # Refocus
        self.call_from_thread(self._focus_input)

    def _focus_input(self) -> None:
        try:
            self.query_one("#prompt-input", TextArea).focus()
        except NoMatches:
            pass

    def action_clear_chat(self) -> None:
        scroll = self.query_one("#chat-scroll", VerticalScroll)
        scroll.remove_children()
        self._conversation.clear()
        self._add_system_msg("Chat cleared. Session memory preserved.")

    def action_focus_input(self) -> None:
        self._focus_input()

    def action_quit(self) -> None:
        self._memory.save()
        self.exit()


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------


async def start_tui_session(
    model: str = "odin", user_email: str = "",
    initial_prompt: str = "", **_kwargs: object,
) -> None:
    app = AiCippyApp(model=model, user_email=user_email, initial_prompt=initial_prompt)
    await app.run_async()
