"""Main TUI application — UnfamiliarApp.

Composes all widgets into the investigation layout:
- InvestigationHeader (top)
- Horizontal container:
  - ContentArea (main, flexible)
  - CommandReference (right, fixed width)
- CommandInput (bottom)

Handles command routing and dispatch.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Horizontal

from unfamiliar.campaign import reopen_campaign
from unfamiliar.campaign.missions import (
    CampaignStatusError,
    MissionStatusError,
    complete_mission,
    start_mission,
    unlock_mission,
)
from unfamiliar.constants import CampaignStatus, MissionStatus
from unfamiliar.db import CampaignDB, open_campaign_db
from unfamiliar.tui.commands import CommandRouter
from unfamiliar.tui.queries import (
    load_actionable_missions,
    load_briefing_data,
    load_header_data,
    load_in_progress_missions,
    load_mission_detail,
    load_status_data,
)
from unfamiliar.tui.screens import ConfirmModal
from unfamiliar.tui.themes.film_noir.theme import FILM_NOIR_THEME
from unfamiliar.tui.themes.registry import ThemeRegistry
from unfamiliar.tui.views import CompletionData, MissionSummary
from unfamiliar.tui.widgets import (
    CommandInput,
    CommandReference,
    CommandSubmitted,
    ContentArea,
    InvestigationHeader,
)

if TYPE_CHECKING:
    from pathlib import Path

    from unfamiliar.models import Campaign


class UnfamiliarApp(App):
    """Main TUI application for the Unfamiliar investigation tool.

    Layout:
    - Header: Campaign info and progress
    - Main area: ContentArea (left) + CommandReference (right)
    - Input: Command input at bottom

    Attributes:
        campaign_dir: Path to the campaign directory.
        campaign_id: ID of the campaign to load.
        db: Open campaign database.
        campaign: Loaded campaign model.
        theme: Theme configuration.
        router: Command router for parsing input.
        current_mission_id: ID of the currently selected/in-progress mission.
        _mission_list_cache: Cache of last displayed mission list for selection.
    """

    BINDINGS = [
        Binding("q", "quit", "Quit", priority=True),
        Binding("?", "help", "Help", priority=True),
        Binding("escape", "cancel", "Cancel", priority=True),
        Binding("slash", "focus_input", "Focus input", show=False),
    ]

    def __init__(
        self,
        campaign_dir: Path,
        campaign_id: int,
        *,
        theme_registry: ThemeRegistry,
        theme_name: str = "film_noir",
    ) -> None:
        """Initialize the TUI application.

        Args:
            campaign_dir: Path to the campaign directory.
            campaign_id: ID of the campaign to load.
            theme_name: Name of the theme to use (default: film_noir).
        """
        # Load theme BEFORE super().__init__() so CSS_PATH is set before compose()
        self.theme_config = theme_registry.get(theme_name)
        self.CSS_PATH = self.theme_config.css_path
        self.router = CommandRouter(self.theme_config)

        super().__init__()

        self.campaign_dir = campaign_dir
        self.campaign_id = campaign_id

        # Will be set in on_mount
        self.db: CampaignDB | None = None
        self.campaign: Campaign | None = None
        self.current_mission_id: int | None = None

        # Mission list cache for selection by number
        self._mission_list_cache: list[MissionSummary] = []

    def compose(self) -> ComposeResult:
        """Compose the application layout."""
        yield InvestigationHeader(theme=self.theme_config, id="main-header")
        with Container(id="main-container"), Horizontal(id="content-row"):
            yield ContentArea(theme=self.theme_config, id="main-content")
            yield CommandReference(theme=self.theme_config, id="command-ref")
        yield CommandInput(id="command-input")

    def on_mount(self) -> None:
        """Initialize the application on mount."""
        # Open database
        self.db = open_campaign_db(self.campaign_dir)
        # Long-lived DB connection — managed manually, not via context manager
        # (Textual widget lifecycle doesn't support `with` blocks)

        # Load campaign
        self.campaign = self.db.campaign_repo.get(self.campaign_id)
        if self.campaign is None:
            self._show_error(f"Campaign {self.campaign_id} not found")
            return

        # Update header
        header_data = load_header_data(self.db, self.campaign)
        self.query_one(InvestigationHeader).update_data(header_data)

        # Update command reference
        commands = self.router.get_commands()
        self.query_one(CommandReference).update_commands(commands)

        # Resume flow: check for in-progress missions
        in_progress = load_in_progress_missions(self.db, self.campaign_id)
        if in_progress:
            # Set the most recent in-progress mission as current
            self.current_mission_id = in_progress[0].id
            mission_alias = self.theme_config.alias("mission")
            complete_alias = self.theme_config.alias("complete")
            self.query_one(ContentArea).show_text(
                f"[bold]Welcome back, Detective.[/]\n\n"
                f"You were investigating: [italic]{in_progress[0].title}[/]\n\n"
                f"Type [bold]{mission_alias}[/] to review, "
                f"or [bold]{complete_alias}[/] to close it."
            )
        elif header_data.available > 0:
            next_alias = self.theme_config.alias("next")
            mission_term = self.theme_config.vocab("mission")
            self.query_one(ContentArea).show_text(
                f"[bold]Welcome back, Detective.[/]\n\n"
                f"You have {header_data.available} {mission_term}s waiting.\n"
                f"Type [bold]{next_alias}[/] to see them."
            )
        else:
            status_alias = self.theme_config.alias("status")
            mission_term = self.theme_config.vocab("mission")
            completed_term = self.theme_config.vocab("completed")
            locked_term = self.theme_config.vocab("locked")
            self.query_one(ContentArea).show_text(
                f"[bold]Welcome back, Detective.[/]\n\n"
                f"All {mission_term}s are {completed_term} or {locked_term}.\n"
                f"Type [bold]{status_alias}[/] to see the case board."
            )

    def on_unmount(self) -> None:
        """Clean up on unmount."""
        if self.db is not None:
            self.db.conn.close()

    def on_command_submitted(self, event: CommandSubmitted) -> None:
        """Handle command submission.

        Args:
            event: The command submitted event.
        """
        # Parse the command
        canonical, args = self.router.parse(event.command)

        # Dispatch to handler
        handler_name = f"_handle_{canonical.replace(' ', '_')}"
        handler = getattr(self, handler_name, None)

        if handler:
            handler(args)
        else:
            self._handle_unknown(event.command)

    # --- Command Handlers ---

    def _handle_next(self, args: list[str]) -> None:
        """Handle 'next' command — show actionable missions or start one by number."""
        if not self._require_db():
            return

        if not args:
            # No args: show actionable missions (in-progress + available)
            missions = load_actionable_missions(self.db, self.campaign_id)
            self._mission_list_cache = missions
            self.query_one(ContentArea).show_missions(missions)
        else:
            # With number: start the selected mission
            try:
                index = int(args[0]) - 1  # Convert 1-indexed to 0-indexed
            except ValueError:
                self.query_one(ContentArea).show_error(
                    f"Invalid number: {args[0]}. Use a number from the list."
                )
                return

            if not self._mission_list_cache:
                next_alias = self.theme_config.alias("next")
                mission_term = self.theme_config.vocab("mission")
                self.query_one(ContentArea).show_error(
                    f"Run [bold]{next_alias}[/] first to see available {mission_term}s."
                )
                return

            if index < 0 or index >= len(self._mission_list_cache):
                self.query_one(ContentArea).show_error(
                    f"Number {args[0]} out of range. "
                    f"Valid range: 1–{len(self._mission_list_cache)}"
                )
                return

            mission = self._mission_list_cache[index]
            self._start_mission(mission.id)

    def _start_mission(self, mission_id: int) -> None:
        """Start a mission and set it as current.

        If the mission is already in progress, just set it as current
        and show detail (resume behavior).

        Args:
            mission_id: ID of the mission to start/resume.
        """
        if not self._require_db():
            return

        # Check if mission is already in progress
        mission = self.db.mission_repo.get(mission_id)
        if mission is None:
            mission_term = self.theme_config.vocab("mission")
            self.query_one(ContentArea).show_error(f"{mission_term.title()} not found.")
            return

        # If already in progress, just resume (no status change needed)
        if mission.status == MissionStatus.IN_PROGRESS:
            self.current_mission_id = mission_id
            try:
                detail = load_mission_detail(self.db, mission_id)
                self.query_one(ContentArea).show_mission_detail(detail)
                self.notify(
                    f"Resumed: {detail.title}",
                    title="Back on the case",
                    severity="information",
                )
            except ValueError as e:
                self.query_one(ContentArea).show_error(str(e))
            return

        # Otherwise, start the mission normally
        try:
            start_mission(self.db, mission_id)
        except MissionStatusError as e:
            self.query_one(ContentArea).show_error(str(e))
            return

        self.current_mission_id = mission_id

        # Load and show mission detail
        try:
            detail = load_mission_detail(self.db, mission_id)
            self.query_one(ContentArea).show_mission_detail(detail)
            self.notify(
                f"Lead started: {detail.title}",
                title="On the case",
                severity="information",
            )
        except ValueError as e:
            self.query_one(ContentArea).show_error(str(e))
            return

        # Update header
        progress = load_header_data(self.db, self.campaign)
        self.query_one(InvestigationHeader).update_data(progress)

        # Invalidate cache since availability may have changed
        self._mission_list_cache = []

    def _handle_complete(self, args: list[str]) -> None:
        """Handle 'complete' command — complete current mission."""
        if not self._require_db():
            return

        if self.current_mission_id is None:
            next_alias = self.theme_config.alias("next")
            mission_term = self.theme_config.vocab("mission")
            self.query_one(ContentArea).show_error(
                f"No {mission_term} in progress. "
                f"Use '{next_alias}' to pick a {mission_term}."
            )
            return

        try:
            result = complete_mission(self.db, self.current_mission_id)
        except MissionStatusError as e:
            self.query_one(ContentArea).show_error(str(e))
            return

        # Get completed mission detail for outro (before iterating unlocks)
        completed_mission = self.db.mission_repo.get(self.current_mission_id)
        if completed_mission is None:
            return

        # Get unlocked mission titles and notify about each
        unlocked_titles: list[str] = []
        unlocked_missions = self.db.mission_repo.get_by_ids(result.unlocked_ids)
        for unlocked in unlocked_missions:
            unlocked_titles.append(unlocked.title)
            mission_term = self.theme_config.vocab("mission").title()
            self.notify(
                f"{mission_term} unlocked: {unlocked.title}",
                title="New lead",
                severity="information",
            )

        # Get progress
        progress = load_header_data(self.db, self.campaign)

        # Show completion
        completion_data = CompletionData(
            mission_title=completed_mission.title,
            narrative_outro=completed_mission.narrative_outro or "Lead closed.",
            unlocked_titles=unlocked_titles,
            new_available=len(result.unlocked_ids),
            total_completed=progress.completed,
            total_missions=progress.total,
        )
        self.query_one(ContentArea).show_completion(completion_data)

        # Notify about unlocks
        if result.unlocked_ids:
            self.notify(
                f"{len(result.unlocked_ids)} new lead(s) unlocked",
                title="Lock picked",
                severity="information",
            )

        # Clear current mission
        self.current_mission_id = None

        # Invalidate cache
        self._mission_list_cache = []

        # Update header
        self.query_one(InvestigationHeader).update_data(progress)

    def _handle_status(self, args: list[str]) -> None:
        """Handle 'status' command — show case board."""
        if not self._require_db():
            return

        status_data = load_status_data(self.db, self.campaign)
        self.query_one(ContentArea).show_status(status_data)

    def _handle_briefing(self, args: list[str]) -> None:
        """Handle 'briefing' command — show case file."""
        if not self._require_db():
            return

        briefing_data = load_briefing_data(self.db, self.campaign_id)
        self.query_one(ContentArea).show_briefing(briefing_data)

    def _handle_mission(self, args: list[str]) -> None:
        """Handle 'mission' command — show current or numbered mission details."""
        if not self._require_db():
            return

        if not args:
            # No args: show current mission
            if self.current_mission_id is None:
                next_alias = self.theme_config.alias("next")
                mission_term = self.theme_config.vocab("mission")
                self.query_one(ContentArea).show_error(
                    f"No active {mission_term}. "
                    f"Use '{next_alias}' to see available {mission_term}s."
                )
                return

            try:
                detail = load_mission_detail(self.db, self.current_mission_id)
                self.query_one(ContentArea).show_mission_detail(detail)
            except ValueError as e:
                self.query_one(ContentArea).show_error(str(e))
        else:
            # With number: view mission detail without starting
            try:
                index = int(args[0]) - 1  # Convert 1-indexed to 0-indexed
            except ValueError:
                self.query_one(ContentArea).show_error(
                    f"Invalid number: {args[0]}. Use a number from the list."
                )
                return

            if not self._mission_list_cache:
                next_alias = self.theme_config.alias("next")
                mission_term = self.theme_config.vocab("mission")
                self.query_one(ContentArea).show_error(
                    f"Run [bold]{next_alias}[/] first to see available {mission_term}s."
                )
                return

            if index < 0 or index >= len(self._mission_list_cache):
                self.query_one(ContentArea).show_error(
                    f"Number {args[0]} out of range. "
                    f"Valid range: 1–{len(self._mission_list_cache)}"
                )
                return

            mission = self._mission_list_cache[index]
            try:
                detail = load_mission_detail(self.db, mission.id)
                self.query_one(ContentArea).show_mission_detail(detail)
            except ValueError as e:
                self.query_one(ContentArea).show_error(str(e))

    def _handle_notebook(self, args: list[str]) -> None:
        """Handle 'notebook' command — print notebook path (v1.1 placeholder)."""
        notebook_path = self.campaign_dir / "notebook.md"
        self.query_one(ContentArea).show_text(
            f"[dim]Notebook (v1.1 feature):[/]\n{notebook_path}"
        )

    def _handle_unlock(self, args: list[str]) -> None:
        """Handle 'unlock' command — force unlock a mission."""
        if not self._require_db():
            return

        if not args:
            self.query_one(ContentArea).show_error(
                "Usage: unlock <mission_id>\nExample: unlock 42"
            )
            return

        try:
            mission_id = int(args[0])
        except ValueError:
            self.query_one(ContentArea).show_error(
                f"Invalid mission ID: {args[0]}. Must be a number."
            )
            return

        try:
            unlock_mission(self.db, mission_id)
            mission = self.db.mission_repo.get(mission_id)
            if mission:
                self.notify(
                    f"Unlocked: {mission.title}",
                    title="Lock picked",
                    severity="information",
                )
                # Update header
                if self.campaign:
                    progress = load_header_data(self.db, self.campaign)
                    self.query_one(InvestigationHeader).update_data(progress)
                # Invalidate cache
                self._mission_list_cache = []
        except MissionStatusError as e:
            self.query_one(ContentArea).show_error(str(e))

    def _handle_help(self, args: list[str]) -> None:
        """Handle 'help' command — show all commands."""
        commands = self.router.get_commands()
        self.query_one(ContentArea).show_help(commands)

    def _handle_quit(self, args: list[str]) -> None:
        """Handle 'quit' command — exit app with optional confirmation."""
        self._confirm_quit()

    def _handle_reopen(self, args: list[str]) -> None:
        """Handle 'reopen' command — reopen a closed case."""
        if not self._require_db():
            return

        try:
            reopen_campaign(self.db, self.campaign_id)
            campaign_term = self.theme_config.vocab("campaign")
            self.notify(
                f"{campaign_term.title()} reopened. Continue your investigation.",
                title="Case reopened",
                severity="information",
            )
            # Update header
            if self.campaign:
                progress = load_header_data(self.db, self.campaign)
                self.query_one(InvestigationHeader).update_data(progress)
        except (CampaignStatusError, MissionStatusError) as e:
            self.query_one(ContentArea).show_error(str(e))

    def _handle_end_campaign(self, args: list[str]) -> None:
        """Handle 'end campaign' command — close campaign and exit."""
        if not self._require_db():
            return

        if isinstance(self.screen, ConfirmModal):
            return  # Already showing confirmation

        # Get themed terms
        campaign_term = self.theme_config.vocab("campaign")
        complete_term = self.theme_config.vocab("complete")

        def on_confirm(confirmed: bool) -> None:
            """Callback for confirmation modal."""
            if confirmed:
                self._close_campaign()
            else:
                self.notify(
                    f"{campaign_term.title()} remains open.", severity="information"
                )

        cancel_alias = self.theme_config.vocab("cancel").title()
        self.push_screen(
            ConfirmModal(
                message=f"{complete_term.title()} the {campaign_term}? "
                f"This ends the investigation.",
                confirm_label=f"{complete_term.title()} {campaign_term.title()}",
                cancel_label=cancel_alias,
            ),
            on_confirm,
        )

    def _close_campaign(self) -> None:
        """Close the campaign and show summary."""
        if not self._require_db():
            return

        # Update campaign status to CLOSED
        self.db.campaign_repo.update_status(self.campaign_id, CampaignStatus.CLOSED)

        # Get final progress
        progress = load_header_data(self.db, self.campaign)

        # Get themed terms
        campaign_term = self.theme_config.vocab("campaign")
        mission_term = self.theme_config.vocab("mission")
        completed_term = self.theme_config.vocab("completed")
        accent_color = self.theme_config.color("accent")

        # Show summary
        self.query_one(ContentArea).show_text(
            f"[bold {accent_color}]{campaign_term.title()} "
            f"{completed_term.title()}[/]\n\n"
            f"{mission_term.title()}s {completed_term}: "
            f"{progress.completed}/{progress.total}\n\n"
            "The investigation has concluded.\n"
            "Thank you for your service, Detective."
        )

        # Exit after a moment
        self.set_timer(3.0, self.exit)

    def _handle_unknown(self, raw: str) -> None:
        """Handle unknown command."""
        help_alias = self.theme_config.alias("help")
        self.query_one(ContentArea).show_error(
            f"Unknown command: '{raw}'\n\n"
            f"Type '{help_alias}' to see available commands."
        )

    # --- Action Handlers ---

    def action_quit(self) -> None:
        """Handle quit action (q key)."""
        self._confirm_quit()

    def _confirm_quit(self) -> None:
        """Show quit confirmation if mission in progress, else exit."""
        if isinstance(self.screen, ConfirmModal):
            return  # Already showing confirmation

        if self.current_mission_id is None:
            self.exit()
            return

        # Get themed terms
        mission_term = self.theme_config.vocab("mission")
        in_progress_term = self.theme_config.vocab("in_progress")

        def on_confirm(confirmed: bool) -> None:
            """Callback for confirmation modal."""
            if confirmed:
                self.exit()
            else:
                stay_msg = self.theme_config.vocab("stay")
                self.notify(stay_msg, severity="information")

        quit_alias = self.theme_config.alias("quit").title()
        cancel_alias = self.theme_config.vocab("cancel").title()
        self.push_screen(
            ConfirmModal(
                message=f"You have an {in_progress_term} {mission_term}. "
                f"Quit anyway? Your progress is saved.",
                confirm_label=quit_alias,
                cancel_label=cancel_alias,
            ),
            on_confirm,
        )

    def action_help(self) -> None:
        """Handle help action (? key)."""
        self._handle_help([])

    def action_cancel(self) -> None:
        """Handle escape — dismiss top modal or defocus command input."""
        if isinstance(self.screen, ConfirmModal):
            self.screen.dismiss(False)
        else:
            self.query_one(CommandInput).blur()

    def action_focus_input(self) -> None:
        """Handle slash key — focus command input."""
        self.query_one(CommandInput).focus()

    # --- Helper Methods ---

    def _show_error(self, message: str) -> None:
        """Display an error message in the content area.

        Args:
            message: Error message to display.
        """
        self.query_one(ContentArea).show_error(message)

    def _require_db(self) -> bool:
        """Check that database and campaign are available, show error if not.

        Returns:
            True if self.db and self.campaign are both available.
            False if either is None (also shows error in ContentArea).

        Invariants:
            - Never raises an exception
            - Always shows user-visible error when returning False
        """
        if self.db is None or self.campaign is None:
            self._show_error("Database not available. Please restart the application.")
            return False
        return True


def _create_theme_registry() -> ThemeRegistry:
    registry = ThemeRegistry()
    registry.register(FILM_NOIR_THEME)
    return registry


def run_tui(campaign_dir: Path, campaign_id: int) -> None:
    """Launch the TUI application.

    Args:
        campaign_dir: Path to the campaign directory.
        campaign_id: ID of the campaign to load.
    """
    registry = _create_theme_registry()
    app = UnfamiliarApp(
        campaign_dir=campaign_dir, campaign_id=campaign_id, theme_registry=registry
    )
    app.run()


__all__ = ["UnfamiliarApp", "run_tui"]
