"""Tunnel manager for dev tunnels.

This module provides the TunnelManager class for managing
cloudflared tunnels that expose container ports to the internet.
"""

from __future__ import annotations

import os
import uuid

import yaml

from gaol.config import get_data_dir
from gaol.store import get_container_store
from gaol.tunnel.cloudflared import CloudflaredBackend
from gaol.tunnel.exceptions import (
    TunnelAlreadyExistsError,
    TunnelNotFoundError,
)
from gaol.tunnel.models import ActiveTunnel, TunnelStatus


class TunnelManager:
    """Manage dev tunnels for containers.

    Handles starting, stopping, and tracking cloudflared tunnels
    that expose container ports to the internet.

    Example:
        >>> manager = TunnelManager()
        >>> tunnel = manager.start("myapp", 8080)
        >>> print(tunnel.public_url)
        https://random-words.trycloudflare.com
        >>> manager.stop(tunnel.id)
    """

    def __init__(self) -> None:
        """Initialize the tunnel manager."""
        self._store_path = get_data_dir() / "tunnels.yaml"
        self._backend = CloudflaredBackend()

    def start(
        self,
        container_name: str,
        port: int,
        host_port: int | None = None,
    ) -> ActiveTunnel:
        """Start a tunnel for a container port.

        Args:
            container_name: Name of the container.
            port: Port inside the container to expose.
            host_port: Port on host (auto-detected from port mapping if not specified).

        Returns:
            ActiveTunnel record with public URL and tunnel ID.

        Raises:
            TunnelAlreadyExistsError: If tunnel already exists for this container/port.
            TunnelStartError: If tunnel fails to start.
            CloudflaredNotFoundError: If cloudflared is not installed.
        """
        # Check for existing tunnel
        existing = self._find_tunnel(container_name, port)
        if existing and self._is_process_running(existing.process_pid):
            raise TunnelAlreadyExistsError(container_name, port)

        # Clean up stale tunnel record if exists
        if existing:
            self._remove_tunnel(existing.id)

        # Determine host port
        if host_port is None:
            host_port = self._get_mapped_port(container_name, port)

        # Start cloudflared
        proc, public_url = self._backend.start_quick_tunnel("localhost", host_port)

        # Create tunnel record
        tunnel = ActiveTunnel(
            id=str(uuid.uuid4())[:8],
            container_name=container_name,
            container_port=port,
            host_port=host_port,
            public_url=public_url,
            process_pid=proc.pid,
        )

        self._save_tunnel(tunnel)
        return tunnel

    def stop(self, tunnel_id: str) -> None:
        """Stop a tunnel by ID.

        Args:
            tunnel_id: Tunnel ID to stop.

        Raises:
            TunnelNotFoundError: If tunnel is not found.
        """
        tunnel = self._get_tunnel(tunnel_id)
        if not tunnel:
            raise TunnelNotFoundError(tunnel_id)

        self._backend.stop(tunnel.process_pid)
        self._remove_tunnel(tunnel_id)

    def stop_all(self, container_name: str | None = None) -> int:
        """Stop all tunnels, optionally filtered by container.

        Args:
            container_name: If specified, only stop tunnels for this container.

        Returns:
            Number of tunnels stopped.
        """
        tunnels = self.list(container_name)
        count = 0
        for tunnel in tunnels:
            try:
                self._backend.stop(tunnel.process_pid)
                self._remove_tunnel(tunnel.id)
                count += 1
            except Exception:
                pass
        return count

    def list(self, container_name: str | None = None) -> list[ActiveTunnel]:
        """List active tunnels.

        Args:
            container_name: If specified, filter by container name.

        Returns:
            List of active tunnels.
        """
        tunnels = self._load_tunnels()

        # Filter out dead processes and update status
        active = []
        for tunnel in tunnels:
            if self._is_process_running(tunnel.process_pid):
                tunnel.status = TunnelStatus.RUNNING
                active.append(tunnel)
            else:
                # Clean up stale record
                self._remove_tunnel(tunnel.id)

        if container_name:
            active = [t for t in active if t.container_name == container_name]

        return active

    def get(self, tunnel_id: str) -> ActiveTunnel | None:
        """Get a tunnel by ID.

        Args:
            tunnel_id: Tunnel ID.

        Returns:
            ActiveTunnel if found and running, None otherwise.
        """
        tunnel = self._get_tunnel(tunnel_id)
        if tunnel and self._is_process_running(tunnel.process_pid):
            return tunnel
        return None

    def _get_mapped_port(self, container_name: str, container_port: int) -> int:
        """Get the host port mapped to a container port.

        Args:
            container_name: Container name.
            container_port: Container port.

        Returns:
            Host port (defaults to container_port if no mapping found).
        """
        store = get_container_store()
        stored = store.get(container_name)

        if stored and stored.config and stored.config.ports:
            for mapping in stored.config.ports:
                if mapping.container_port == container_port:
                    return mapping.host_port

        # If no mapping, assume container port = host port
        return container_port

    def _is_process_running(self, pid: int) -> bool:
        """Check if a process is running.

        Args:
            pid: Process ID.

        Returns:
            True if process is running.
        """
        try:
            os.kill(pid, 0)
            return True
        except (ProcessLookupError, PermissionError):
            return False

    def _find_tunnel(self, container_name: str, port: int) -> ActiveTunnel | None:
        """Find tunnel by container and port.

        Args:
            container_name: Container name.
            port: Container port.

        Returns:
            ActiveTunnel if found, None otherwise.
        """
        tunnels = self._load_tunnels()
        for t in tunnels:
            if t.container_name == container_name and t.container_port == port:
                return t
        return None

    def _get_tunnel(self, tunnel_id: str) -> ActiveTunnel | None:
        """Get tunnel by ID.

        Args:
            tunnel_id: Tunnel ID.

        Returns:
            ActiveTunnel if found, None otherwise.
        """
        tunnels = self._load_tunnels()
        for t in tunnels:
            if t.id == tunnel_id:
                return t
        return None

    def _save_tunnel(self, tunnel: ActiveTunnel) -> None:
        """Save tunnel to store.

        Args:
            tunnel: Tunnel to save.
        """
        tunnels = self._load_tunnels()
        tunnels = [t for t in tunnels if t.id != tunnel.id]
        tunnels.append(tunnel)
        self._write_store(tunnels)

    def _remove_tunnel(self, tunnel_id: str) -> None:
        """Remove tunnel from store.

        Args:
            tunnel_id: Tunnel ID to remove.
        """
        tunnels = self._load_tunnels()
        tunnels = [t for t in tunnels if t.id != tunnel_id]
        self._write_store(tunnels)

    def _load_tunnels(self) -> list[ActiveTunnel]:
        """Load tunnels from store.

        Returns:
            List of tunnels.
        """
        if not self._store_path.exists():
            return []

        try:
            with open(self._store_path) as f:
                data = yaml.safe_load(f)
            if not data or "tunnels" not in data:
                return []
            return [ActiveTunnel.model_validate(t) for t in data["tunnels"]]
        except Exception:
            return []

    def _write_store(self, tunnels: list[ActiveTunnel]) -> None:
        """Write tunnels to store.

        Args:
            tunnels: List of tunnels to write.
        """
        self._store_path.parent.mkdir(parents=True, exist_ok=True)
        data = {"tunnels": [t.model_dump(mode="json") for t in tunnels]}
        with open(self._store_path, "w") as f:
            yaml.safe_dump(data, f, default_flow_style=False)
