"""Sibilance TUI application.

The main Textual App that manages screens, API connection,
WebSocket events, and global state.
"""

from __future__ import annotations

from pathlib import Path

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.reactive import reactive
from textual.widgets import Footer, Header, LoadingIndicator

from sibilance.api.client import ApiClient, AuthError
from sibilance.api.types import CreatureStatus, RecentEvent
from sibilance.api.websocket import SibilanceWebSocket, WSEvent, WSEventType
from sibilance.auth.credentials import (
    get_server_url,
    has_valid_credentials,
    read_credentials,
)
from sibilance.commands import SibilanceCommands
from sibilance.styles.theme_manager import (
    DEFAULT_THEME,
    SIBILANCE_THEMES,
    theme_for_period,
)
from sibilance.screens.account import AccountScreen
from sibilance.screens.conversation import ConversationScreen
from sibilance.screens.creatures import CreaturesScreen
from sibilance.screens.dashboard import DashboardScreen
from sibilance.screens.den import DenScreen
from sibilance.screens.friends import FriendsScreen
from sibilance.screens.hatching import HatchingScreen
from sibilance.screens.journal import JournalScreen
from sibilance.screens.login import LoginScreen
from sibilance.screens.sleep import SleepScreen
from sibilance.screens.world import WorldScreen

STYLES_DIR = Path(__file__).parent / "styles" / "themes"

# Toast icons by event type for visual distinction
_EVENT_ICONS: dict[str, str] = {
    "location_update": "📍",
    "conversation_update": "💬",
    "journal_entry": "📖",
    "mystery_clue": "🔮",
    "rare_event": "✦",
    "world_state_update": "🌍",
}


class SibilanceApp(App):
    """A living creature in your terminal."""

    TITLE = "sibilance"
    CSS_PATH = [STYLES_DIR / "default.tcss"]
    ENABLE_COMMAND_PALETTE = True
    COMMANDS = {SibilanceCommands}

    SCREENS: dict[str, type] = {
        "dashboard": DashboardScreen,
        "conversation": ConversationScreen,
        "journal": JournalScreen,
        "friends": FriendsScreen,
        "world": WorldScreen,
        "den": DenScreen,
        "sleep": SleepScreen,
        "hatching": HatchingScreen,
        "login": LoginScreen,
        "account": AccountScreen,
        "creatures": CreaturesScreen,
    }

    BINDINGS = [
        Binding("q", "quit", "Quit", show=True),
        Binding("escape", "go_back", "Back", show=False),
        Binding("d", "switch_screen('dashboard')", "Dashboard", show=True),
        Binding("c", "switch_screen('conversation')", "Chat", show=True),
        Binding("j", "switch_screen('journal')", "Journal", show=True),
        Binding("f", "switch_screen('friends')", "Friends", show=True),
        Binding("w", "switch_screen('world')", "World", show=True),
        Binding("t", "switch_screen('den')", "Den", show=False),
        Binding("s", "switch_screen('sleep')", "Sleep", show=False),
        Binding("a", "switch_screen('account')", "Account", show=False),
        Binding("?", "toggle_help", "Help", show=True),
    ]

    # ── Reactive state ───────────────────────────────────────────────────

    creature: reactive[CreatureStatus | None] = reactive(None)
    connected: reactive[bool] = reactive(False)
    loading: reactive[bool] = reactive(True)
    _screen_history: list[str]

    def __init__(self, replay_hatching: bool = False) -> None:
        super().__init__()
        server_url = get_server_url()
        self.api = ApiClient(server_url)
        self._ws: SibilanceWebSocket | None = None
        self._ws_task = None
        self._screen_history: list[str] = []
        self._current_period = ""
        self._replay_hatching = replay_hatching

        # Register Sibilance custom themes
        for theme in SIBILANCE_THEMES.values():
            self.register_theme(theme)
        self.theme = DEFAULT_THEME

    # ── Lifecycle ────────────────────────────────────────────────────────

    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        yield LoadingIndicator()
        yield Footer()

    async def on_mount(self) -> None:
        """Initialize the app: check auth, load creature, connect WebSocket."""
        if self._replay_hatching:
            self.loading = False
            from sibilance.screens.hatching import HatchingScreen
            self.push_screen(HatchingScreen(demo=True))
            return

        self.loading = True

        if not has_valid_credentials():
            self.loading = False
            self.push_screen("login")
            return

        # Try token refresh
        refreshed = await self.api.try_refresh()
        if not refreshed and not has_valid_credentials():
            self.loading = False
            self.push_screen("login")
            return

        # Load creature data
        try:
            creatures = await self.api.get_my_creatures()
            if creatures:
                self.creature = creatures[0]
                self._start_websocket()
                self.push_screen("dashboard")
            else:
                # No creature — start hatching ceremony
                self.push_screen("hatching")
        except AuthError:
            self.push_screen("login")
        except Exception as exc:
            # Unwrap exception chain to show root cause
            root = exc
            while root.__cause__ is not None:
                root = root.__cause__
            detail = str(root) if root is not exc else str(exc)
            self.notify(
                f"Connection error ({self.api._server_url}): {detail}",
                severity="error",
                timeout=8,
            )
            self.push_screen("login")

        self.loading = False

    async def _post_login(self) -> None:
        """Called by LoginScreen after successful authentication.

        Loads creature data and transitions to dashboard or hatching.
        """
        try:
            await self.api.try_refresh()
            creatures = await self.api.get_my_creatures()
            if creatures:
                self.creature = creatures[0]
                self._start_websocket()
                self.switch_screen("dashboard")
            else:
                self.switch_screen("hatching")
        except Exception as exc:
            self.notify(f"Error: {exc}", severity="error", timeout=5)

    def _start_websocket(self) -> None:
        """Start the WebSocket connection for real-time events."""
        if self.creature is None:
            return

        creds = read_credentials()
        if creds is None:
            return

        self._ws = SibilanceWebSocket(
            server_url=get_server_url(),
            agent_id=self.creature.agent_id,
            on_event=self._on_ws_event,
            on_connected=lambda: setattr(self, "connected", True),
            on_disconnected=lambda: setattr(self, "connected", False),
        )
        self._ws_task = self.run_worker(
            self._ws.run(), name="websocket", exclusive=True
        )

    def _on_ws_event(self, event: WSEvent) -> None:
        """Handle incoming WebSocket events.

        event.data is the full JSON message dict from the server.
        Server message shapes:
          - agent_state_update: { type }
          - location_update: { type, data: { location } }
          - conversation_update: { type, data: { content, emotional_tone } }
          - journal_entry: { type, data: { content, tags, is_private } }
          - mystery_clue: { type, data: [...] }
          - rare_event: { type, data: [...] }
          - world_state_update: { type, data: { ... } }
        """
        icon = _EVENT_ICONS.get(event.type.value, "")

        # WebSocket worker runs in the same event loop as the app,
        # so we can call methods directly (no call_from_thread needed).
        if event.type == WSEventType.AGENT_STATE_UPDATE:
            self.run_worker(
                self._refresh_creature(), name="refresh", exclusive=True
            )
        elif event.type == WSEventType.LOCATION_UPDATE:
            inner = event.data.get("data", {})
            location = inner.get("location", "") if isinstance(inner, dict) else ""
            if self.creature and location:
                self.notify(f"{icon} Moved to {location}", timeout=3)
                self.run_worker(
                    self._refresh_creature(), name="refresh", exclusive=True
                )
                self._push_event_to_log(
                    "location_update",
                    f"Moved to {location}",
                )
        elif event.type == WSEventType.CONVERSATION_UPDATE:
            inner = event.data.get("data", {})
            content = inner.get("content", "") if isinstance(inner, dict) else ""
            if content:
                preview = content[:80] + ("..." if len(content) > 80 else "")
                self.notify(f"{icon} {preview}", timeout=5)
                self.run_worker(
                    self._refresh_creature(), name="refresh", exclusive=True
                )
                tone = ""
                if isinstance(inner, dict):
                    et = inner.get("emotional_tone", {})
                    if isinstance(et, dict):
                        tone = et.get("primary", "")
                self._push_event_to_log(
                    "conversation_update",
                    preview,
                    tone,
                )
        elif event.type == WSEventType.JOURNAL_ENTRY:
            inner = event.data.get("data", {})
            content = inner.get("content", "") if isinstance(inner, dict) else ""
            is_private = inner.get("is_private", False) if isinstance(inner, dict) else False
            if content and not is_private:
                preview = content[:80] + ("..." if len(content) > 80 else "")
                self.notify(f"{icon} New journal entry", timeout=4)
                self._push_event_to_log(
                    "journal_entry",
                    preview,
                )
        elif event.type == WSEventType.WORLD_STATE_UPDATE:
            self.run_worker(
                self._refresh_world(), name="refresh-world", exclusive=True
            )
        elif event.type == WSEventType.MYSTERY_CLUE:
            clues = event.data.get("data", [])
            if isinstance(clues, list) and clues:
                self.notify(f"{icon} Mystery clue: {clues[0]}", timeout=5)
                self._push_event_to_log(
                    "mystery_clue",
                    str(clues[0]),
                )
        elif event.type == WSEventType.RARE_EVENT:
            rare = event.data.get("data", [])
            if isinstance(rare, list) and rare:
                self.notify(f"{icon} Rare event: {rare[0]}", timeout=8)
                self._push_event_to_log(
                    "rare_event",
                    str(rare[0]),
                )

    def _push_event_to_log(
        self,
        event_type: str,
        description: str,
        emotional_color: str = "",
    ) -> None:
        """Push a real-time event to the dashboard EventLog if visible."""
        from sibilance.screens.dashboard import DashboardScreen
        from sibilance.widgets.event_log import EventLog

        screen = self.screen
        if not isinstance(screen, DashboardScreen):
            return
        try:
            event_log = screen.query_one("#event-log", EventLog)
            event = RecentEvent(
                original_event_type=event_type,
                creature_description=description,
                share_willingness=1.0,
                emotional_color=emotional_color,
            )
            event_log.add_event(event)
        except Exception:
            pass

    async def _refresh_creature(self) -> None:
        """Reload creature state from the API."""
        try:
            creatures = await self.api.get_my_creatures()
            if creatures:
                self.creature = creatures[0]
        except Exception as exc:
            self.log.error("Failed to refresh creature: %s", exc)

    async def _refresh_world(self) -> None:
        """Reload world state from the API and update the active screen."""
        try:
            world = await self.api.get_world_state()
            screen = self.screen
            if hasattr(screen, "refresh_from_world"):
                screen.refresh_from_world(world)
            # Day/night theme cycle
            period = world.time.period
            if period != self._current_period:
                self._current_period = period
                target = theme_for_period(period)
                if self.theme != target:
                    self.theme = target
        except Exception as exc:
            self.log.error("Failed to refresh world: %s", exc)

    def watch_creature(self, creature: CreatureStatus | None) -> None:
        """When creature state changes, update the active screen."""
        if creature is None:
            return
        screen = self.screen
        if hasattr(screen, "refresh_from_creature"):
            screen.refresh_from_creature(creature)

    async def action_switch_screen(self, screen_name: str) -> None:
        """Handle screen switching keybindings."""
        if screen_name in self.SCREENS:
            # Track history for back-navigation (capped to prevent unbounded growth)
            current = self.screen.__class__
            for name, cls in self.SCREENS.items():
                if cls is current:
                    self._screen_history.append(name)
                    break
            if len(self._screen_history) > 20:
                self._screen_history = self._screen_history[-20:]
            if self.screen.id == "_default":
                self.push_screen(screen_name)
            else:
                self.switch_screen(screen_name)
        else:
            self.notify(f"Screen '{screen_name}' coming soon", timeout=2)

    def action_go_back(self) -> None:
        """Navigate to the previous screen, or dashboard if no history.

        Blocked during hatching ceremony and login to prevent
        navigating away before the flow completes.
        """
        from sibilance.screens.hatching import HatchingScreen
        from sibilance.screens.login import LoginScreen

        if isinstance(self.screen, (HatchingScreen, LoginScreen)):
            return
        if self._screen_history:
            prev = self._screen_history.pop()
            self.switch_screen(prev)
        elif self.screen.id == "_default":
            # Still on Textual's default screen — push instead of switch
            self.push_screen("dashboard")
        else:
            self.switch_screen("dashboard")

    def action_toggle_help(self) -> None:
        """Show help overlay."""
        self.notify(
            "d=Dashboard  c=Chat  j=Journal  f=Friends  "
            "w=World  t=Den  s=Sleep  a=Account  Esc=Back  q=Quit",
            timeout=5,
        )

    def watch_connected(self, is_connected: bool) -> None:
        """Update connection status indicator on active screen."""
        from sibilance.screens.dashboard import DashboardScreen
        from sibilance.widgets.connection_status import ConnectionStatus

        screen = self.screen
        if isinstance(screen, DashboardScreen):
            try:
                status = screen.query_one(ConnectionStatus)
                status.connected = is_connected
            except Exception:
                pass

    # ── Command palette actions ────────────────────────────────────────

    async def _cmd_feed(self) -> None:
        """Feed the creature (command palette action)."""
        try:
            result = await self.api.feed_creature()
            self.notify(
                f"{result.creature_name}: {result.reaction[:200]}",
                timeout=8,
            )
            self.run_worker(
                self._refresh_creature(), name="refresh", exclusive=True
            )
        except Exception as exc:
            self.notify(f"Error: {exc}", severity="error", timeout=5)

    async def _cmd_gift(self) -> None:
        """Quick gift from command palette — switches to conversation screen."""
        self.switch_screen("conversation")
        self.notify("Use the Gift button or /gift <description> in Chat", timeout=5)

    async def _cmd_guide(self) -> None:
        """Quick guide from command palette — switches to conversation screen."""
        self.switch_screen("conversation")
        self.notify("Use the Guide button or /guide <suggestion> in Chat", timeout=5)

    async def _cmd_world(self) -> None:
        """Show current world state as a notification."""
        try:
            world = await self.api.get_world_state()
            t = world.time
            h = str(t.hour).zfill(2)
            m = str(t.minute).zfill(2)
            self.notify(
                f"Day {t.day} · {t.day_of_week} · {h}:{m} {t.period}\n"
                f"{t.season.capitalize()} · {world.weather_description or world.weather}",
                timeout=8,
            )
        except Exception as exc:
            self.notify(f"Error: {exc}", severity="error", timeout=5)

    def _cmd_quit(self) -> None:
        """Quit the app."""
        self.exit()

    async def on_unmount(self) -> None:
        """Clean up connections on exit."""
        if self._ws:
            await self._ws.close()
        await self.api.close()
