from __future__ import annotations

import platform
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

from kagan.core.models.enums import CardIndicator, TaskStatus, TaskType
from kagan.git_utils import has_git_repo
from kagan.services.runtime import AutoOutputMode
from kagan.services.workspaces import RepoWorkspaceInput
from kagan.ui.screen_result import await_screen_result

if TYPE_CHECKING:
    from kagan.acp import Agent
    from kagan.adapters.db.schema import Task
    from kagan.config import AgentConfig
    from kagan.mcp.global_config import GlobalMcpSpec
    from kagan.services.runtime import AutoOutputReadiness
    from kagan.ui.screens.kanban.screen import KanbanScreen

VALID_PAIR_LAUNCHERS = {"tmux", "vscode", "cursor"}


@dataclass(frozen=True, slots=True)
class WorkspaceProvisionResult:
    success: bool
    path: Path | None = None
    error: str | None = None


@dataclass(frozen=True, slots=True)
class PairBackendReadiness:
    success: bool
    backend: str | None = None
    error: str | None = None


class KanbanSessionController:
    def __init__(self, screen: KanbanScreen) -> None:
        self.screen = screen

    async def provision_workspace_for_active_repo(self, task: Task) -> WorkspaceProvisionResult:
        active_repo_id = self.screen.ctx.active_repo_id
        if active_repo_id is None:
            self.screen.notify("Select a repository to start a session", severity="warning")
            return WorkspaceProvisionResult(success=False, error="No active repository selected")

        repo_details = await self.screen.ctx.project_service.get_project_repo_details(
            task.project_id
        )
        repo = next((item for item in repo_details if item["id"] == active_repo_id), None)
        if repo is None:
            self.screen.notify("Active repository not part of this project", severity="error")
            return WorkspaceProvisionResult(success=False, error="Active repo not in project")

        repo_path = Path(repo["path"])
        if not await has_git_repo(repo_path):
            self.screen.notify(
                f"Not a git repository: {repo_path}. Run git init first.",
                severity="error",
            )
            return WorkspaceProvisionResult(success=False, error="Repository has no git metadata")

        self.screen.notify("Creating workspace...", severity="information")
        try:
            await self.screen.ctx.workspace_service.provision(
                task_id=task.id,
                repos=[
                    RepoWorkspaceInput(
                        repo_id=repo["id"],
                        repo_path=repo["path"],
                        target_branch=repo["default_branch"],
                    )
                ],
            )
        except Exception as exc:
            self.screen.notify(f"Failed to create workspace: {exc}", severity="error")
            return WorkspaceProvisionResult(success=False, error=str(exc))

        wt_path = await self.screen.ctx.workspace_service.get_path(task.id)
        if wt_path is None:
            self.screen.notify("Failed to provision workspace", severity="error")
            return WorkspaceProvisionResult(success=False, error="Workspace path missing")
        return WorkspaceProvisionResult(success=True, path=wt_path)

    async def ensure_mcp_installed(self, agent_config: AgentConfig, spec: GlobalMcpSpec) -> bool:
        from kagan.ui.modals.mcp_install import McpInstallModal

        result = await await_screen_result(
            self.screen.app, McpInstallModal(agent_config=agent_config, spec=spec)
        )
        return result is True

    async def handle_missing_agent(self, task: Task, agent_config: AgentConfig) -> str:
        from kagan.builtin_agents import get_builtin_agent, list_available_agents
        from kagan.ui.modals.agent_choice import AgentChoiceModal, AgentChoiceResult

        builtin = get_builtin_agent(agent_config.short_name)
        available = list_available_agents()

        result = await await_screen_result(
            self.screen.app,
            AgentChoiceModal(
                missing_agent=builtin,
                available_agents=available,
                task_title=task.title,
            ),
        )
        return result or AgentChoiceResult.CANCELLED

    async def update_task_agent(self, task: Task, agent_short_name: str) -> None:
        current_agent = task.get_agent_config(self.screen.kagan_app.config).short_name
        self.screen.notify(
            f"Using {agent_short_name} instead of {current_agent}",
            title="Agent Changed",
            timeout=3,
        )

    async def ask_confirmation(self, title: str, message: str) -> bool:
        from kagan.ui.modals import ConfirmModal

        result = await await_screen_result(
            self.screen.app, ConfirmModal(title=title, message=message)
        )
        return result is True

    async def confirm_start_auto_task(self, task: Task) -> bool:
        from kagan.constants import NOTIFICATION_TITLE_MAX_LENGTH

        title = task.title[:NOTIFICATION_TITLE_MAX_LENGTH]
        return await self.ask_confirmation(
            "Start Agent?",
            f"Start agent for '{title}' and open output stream?",
        )

    async def confirm_attach_pair_session(self, task: Task) -> bool:
        from kagan.constants import NOTIFICATION_TITLE_MAX_LENGTH

        title = task.title[:NOTIFICATION_TITLE_MAX_LENGTH]
        return await self.ask_confirmation(
            "Attach Session?",
            f"Attach to session for '{title}'?",
        )

    async def open_auto_output_for_task(
        self,
        task: Task,
        *,
        wait_for_running: bool = False,
    ) -> bool:
        automation = self.screen.ctx.automation_service
        runtime_service = self.screen.ctx.runtime_service
        attached_agent: Agent | None = None

        if wait_for_running:
            attached_agent = await automation.wait_for_running_agent(task.id, timeout=6.0)

        readiness: AutoOutputReadiness = await runtime_service.prepare_auto_output(task)
        if attached_agent is not None and readiness.running_agent is None:
            readiness.running_agent = attached_agent
            readiness.is_running = True
            readiness.can_open_output = True
            readiness.output_mode = AutoOutputMode.LIVE
        if readiness.output_mode is AutoOutputMode.WAITING and not readiness.is_running:
            recovery = await runtime_service.recover_stale_auto_output(task)
            if recovery.message:
                self.screen.notify(
                    recovery.message,
                    severity="information" if recovery.success else "warning",
                )
            if recovery.success:
                readiness = await runtime_service.prepare_auto_output(task)

        if readiness.message:
            severity = "warning" if not readiness.can_open_output else "information"
            self.screen.notify(readiness.message, severity=severity)
        if not readiness.can_open_output:
            return False

        await self.screen._review.open_review_for_task(
            task,
            read_only=True,
            initial_tab="review-agent-output",
            include_running_output=True,
            auto_output_readiness=readiness,
        )
        return True

    async def open_session_flow(self, task: Task) -> None:
        refreshed = await self.screen.ctx.task_service.get_task(task.id)
        if refreshed is not None:
            task = refreshed

        if task.status in (TaskStatus.REVIEW, TaskStatus.DONE):
            await self.screen._review.open_review_for_task(
                task, read_only=task.status == TaskStatus.DONE
            )
            return

        if task.task_type == TaskType.AUTO:
            if task.status == TaskStatus.BACKLOG:
                if not await self.confirm_start_auto_task(task):
                    return
                await self.start_agent_flow(task)
                refreshed_after_start = await self.screen.ctx.task_service.get_task(task.id)
                await self.open_auto_output_for_task(
                    refreshed_after_start or task,
                    wait_for_running=True,
                )
            elif task.status == TaskStatus.IN_PROGRESS:
                runtime_view = self.screen.ctx.runtime_service.get(task.id)
                is_running = runtime_view.is_running if runtime_view is not None else False
                opened = await self.open_auto_output_for_task(
                    task,
                    wait_for_running=is_running,
                )
                if opened or is_running:
                    return
                if not await self.confirm_start_auto_task(task):
                    return
                await self.start_agent_flow(task)
                refreshed_after_start = await self.screen.ctx.task_service.get_task(task.id)
                await self.open_auto_output_for_task(
                    refreshed_after_start or task,
                    wait_for_running=True,
                )
            return

        agent_config = task.get_agent_config(self.screen.kagan_app.config)
        from kagan.agents.installer import check_agent_installed

        if not check_agent_installed(agent_config.short_name):
            from kagan.ui.modals.agent_choice import AgentChoiceResult

            result = await self.handle_missing_agent(task, agent_config)
            if result == AgentChoiceResult.CANCELLED:
                return
            if result == AgentChoiceResult.INSTALLED:
                pass
            elif fallback_agent := AgentChoiceResult.parse_fallback(result):
                await self.update_task_agent(task, fallback_agent)
                refreshed = await self.screen.ctx.task_service.get_task(task.id)
                if refreshed:
                    task = refreshed
                    agent_config = task.get_agent_config(self.screen.kagan_app.config)

        from kagan.mcp.global_config import get_global_mcp_spec, is_global_mcp_configured

        if not is_global_mcp_configured(agent_config.short_name):
            spec = get_global_mcp_spec(agent_config.short_name)
            if spec:
                installed = await self.ensure_mcp_installed(agent_config, spec)
                if not installed:
                    return

        wt_path = await self.screen.ctx.workspace_service.get_path(task.id)
        if wt_path is None:
            provision_result = await self.provision_workspace_for_active_repo(task)
            if not provision_result.success or provision_result.path is None:
                return
            wt_path = provision_result.path

        backend_readiness = await self.ensure_pair_terminal_backend_ready(task)
        if not backend_readiness.success or backend_readiness.backend is None:
            return

        terminal_backend = backend_readiness.backend

        if not await self.screen.ctx.session_service.session_exists(task.id):
            self.screen.notify("Creating session...", severity="information")
            await self.screen.ctx.session_service.create_session(task, wt_path)

        if task.status == TaskStatus.IN_PROGRESS and not await self.confirm_attach_pair_session(
            task
        ):
            return

        if not self.screen.kagan_app.config.ui.skip_pair_instructions:
            from kagan.ui.modals.tmux_gateway import PairInstructionsModal

            result = await await_screen_result(
                self.screen.app,
                PairInstructionsModal(
                    task.id,
                    task.title,
                    terminal_backend,
                    self.startup_prompt_path_hint(wt_path),
                ),
            )
            if result is None:
                return
            if result == "skip_future":
                self.screen.kagan_app.config.ui.skip_pair_instructions = True
                self.screen.run_worker(
                    self.save_pair_instructions_preference(skip=True),
                    exclusive=True,
                    exit_on_error=False,
                )
            await self.do_open_pair_session(task, wt_path, terminal_backend)
            return

        await self.do_open_pair_session(task, wt_path, terminal_backend)

    def resolve_pair_terminal_backend(self, task: Task) -> str:
        task_backend = getattr(task, "terminal_backend", None)
        if isinstance(task_backend, str):
            normalized = task_backend.strip().lower()
            if normalized in VALID_PAIR_LAUNCHERS:
                return normalized

        configured = getattr(
            self.screen.kagan_app.config.general,
            "default_pair_terminal_backend",
            "tmux",
        )
        if isinstance(configured, str):
            normalized = configured.strip().lower()
            if normalized in VALID_PAIR_LAUNCHERS:
                return normalized

        return "tmux"

    async def ensure_pair_terminal_backend_ready(self, task: Task) -> PairBackendReadiness:
        from kagan.terminals.installer import check_terminal_installed, first_available_pair_backend
        from kagan.ui.modals.terminal_install import TerminalInstallModal

        backend = self.resolve_pair_terminal_backend(task)
        is_windows = platform.system() == "Windows"

        if backend in {"vscode", "cursor"}:
            if check_terminal_installed(backend):
                return PairBackendReadiness(success=True, backend=backend)
            self.screen.notify(
                f"{backend} is not installed. Install it and retry.",
                severity="warning",
            )
            return PairBackendReadiness(
                success=False,
                error=f"{backend} launcher is not installed",
            )

        if backend == "tmux":
            if check_terminal_installed("tmux"):
                return PairBackendReadiness(success=True, backend=backend)
            if is_windows:
                fallback = first_available_pair_backend(windows=True)
                if fallback is not None:
                    await self.screen.ctx.task_service.update_fields(
                        task.id, terminal_backend=fallback
                    )
                    self.screen.notify(
                        f"tmux not found on Windows. Using {fallback} for this task.",
                        severity="information",
                    )
                    return PairBackendReadiness(success=True, backend=fallback)
                self.screen.notify(
                    "PAIR cancelled: install VS Code or Cursor to continue on Windows.",
                    severity="warning",
                )
                return PairBackendReadiness(
                    success=False,
                    error="No supported PAIR launcher available on Windows",
                )

            installed_tmux = await await_screen_result(
                self.screen.app,
                TerminalInstallModal("tmux"),
            )
            if installed_tmux and check_terminal_installed("tmux"):
                return PairBackendReadiness(success=True, backend=backend)

            fallback = first_available_pair_backend(windows=False)
            if fallback is not None:
                await self.screen.ctx.task_service.update_fields(task.id, terminal_backend=fallback)
                self.screen.notify(
                    "tmux not installed. Using fallback launcher "
                    f"{fallback}. VS Code: https://code.visualstudio.com/download "
                    "Cursor: https://cursor.com/downloads",
                    severity="information",
                )
                return PairBackendReadiness(success=True, backend=fallback)
            self.screen.notify(
                "PAIR cancelled: install tmux (recommended), or install VS Code/Cursor "
                "for external development.",
                severity="warning",
            )
            return PairBackendReadiness(
                success=False,
                error="No supported PAIR launcher available",
            )

        self.screen.notify(f"Unsupported PAIR launcher: {backend}", severity="warning")
        return PairBackendReadiness(success=False, error=f"Unsupported PAIR launcher: {backend}")

    @staticmethod
    def startup_prompt_path_hint(workspace_path: Path) -> Path:
        return workspace_path / ".kagan" / "start_prompt.md"

    async def do_open_pair_session(
        self,
        task: Task,
        workspace_path: Path | None = None,
        terminal_backend: str | None = None,
    ) -> None:
        try:
            if task.status == TaskStatus.BACKLOG:
                await self.screen.ctx.task_service.update_fields(
                    task.id, status=TaskStatus.IN_PROGRESS
                )
                await self.screen._board.refresh_board()

            with self.screen.app.suspend():
                attached = await self.screen.ctx.session_service.attach_session(task.id)

            backend = terminal_backend or self.resolve_pair_terminal_backend(task)
            if not attached:
                self.screen.notify("Failed to open PAIR session", severity="warning")
                return

            if backend != "tmux":
                prompt_path = (
                    self.startup_prompt_path_hint(workspace_path)
                    if workspace_path is not None
                    else Path(".kagan/start_prompt.md")
                )
                self.screen.notify(
                    f"Workspace opened externally. Use startup prompt: {prompt_path}",
                    severity="information",
                )
                return

            session_still_exists = await self.screen.ctx.session_service.session_exists(task.id)
            if session_still_exists:
                return

            from kagan.ui.modals.confirm import ConfirmModal

            confirmed = await await_screen_result(
                self.screen.app, ConfirmModal("Session Complete", "Move task to REVIEW?")
            )
            if confirmed:
                await self.screen.ctx.task_service.update_fields(task.id, status=TaskStatus.REVIEW)
                await self.screen._board.refresh_board()

        except Exception as e:
            from kagan.tmux import TmuxError

            if isinstance(e, TmuxError):
                self.screen.notify(f"Tmux error: {e}", severity="error")

    async def start_agent_flow(self, task: Task) -> None:
        if task.task_type == TaskType.PAIR:
            return

        runtime_view = self.screen.ctx.runtime_service.get(task.id)
        is_running = runtime_view.is_running if runtime_view is not None else False
        if is_running:
            self.screen.notify("Agent already running for this task (press Enter to open output)")
            return

        wt_path = await self.screen.ctx.workspace_service.get_path(task.id)
        if wt_path is None:
            provision_result = await self.provision_workspace_for_active_repo(task)
            if not provision_result.success or provision_result.path is None:
                return
            wt_path = provision_result.path

        if task.status == TaskStatus.BACKLOG:
            await self.screen.ctx.task_service.move(task.id, TaskStatus.IN_PROGRESS)
            refreshed = await self.screen.ctx.task_service.get_task(task.id)
            if refreshed:
                task = refreshed
            await self.screen._board.refresh_board()

        self.screen.notify("Starting agent...", severity="information")

        result = self.screen.ctx.automation_service.spawn_for_task(task)

        if hasattr(result, "__await__"):
            spawned = await result
        else:
            spawned = result

        if spawned:
            self.screen._board.set_card_indicator(task.id, CardIndicator.RUNNING, is_active=True)
            self.screen.notify(f"Agent started: {task.id[:8]}", severity="information")
        else:
            self.screen.notify("Failed to start agent (at capacity?)", severity="warning")

    async def apply_global_agent_selection(self, selected: str) -> None:
        from kagan.builtin_agents import get_builtin_agent

        config = self.screen.kagan_app.config
        current_agent = config.general.default_worker_agent
        if not selected or selected == current_agent:
            return

        config.general.default_worker_agent = selected
        if config.get_agent(selected) is None:
            if builtin := get_builtin_agent(selected):
                config.agents[selected] = builtin.config.model_copy(deep=True)

        await config.save(self.screen.kagan_app.config_path)
        self.screen.header.update_agent_from_config(config)
        self.screen.notify(f"Global agent set to: {selected}", severity="information")

    async def save_pair_instructions_preference(self, skip: bool = True) -> None:
        try:
            await self.screen.kagan_app.config.update_ui_preferences(
                self.screen.kagan_app.config_path,
                skip_pair_instructions=skip,
            )
        except Exception as e:
            self.screen.notify(f"Failed to save preference: {e}", severity="error")
