"""Port availability and conflict detection."""

from __future__ import annotations

import shutil
import socket
import subprocess
from typing import TYPE_CHECKING

from gaol.networking.models import PortMapping, Protocol

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime


class PortError(Exception):
    """Base exception for port-related errors."""

    pass


class PortInUseError(PortError):
    """Raised when a port is already in use."""

    def __init__(
        self, port: int, protocol: Protocol, process: str | None = None
    ) -> None:
        self.port = port
        self.protocol = protocol
        self.process = process
        msg = f"Port {port}/{protocol.value} is already in use"
        if process:
            msg += f" by {process}"
        super().__init__(msg)


class PortConflictError(PortError):
    """Raised when ports conflict with another container."""

    def __init__(self, port: int, protocol: Protocol, container: str) -> None:
        self.port = port
        self.protocol = protocol
        self.container = container
        super().__init__(
            f"Port {port}/{protocol.value} conflicts with container '{container}'"
        )


def is_port_available(
    port: int,
    protocol: Protocol = Protocol.TCP,
    host: str = "0.0.0.0",
) -> bool:
    """Check if a port is available for binding.

    Args:
        port: Port number to check
        protocol: TCP, UDP, or BOTH
        host: Host address to bind (default 0.0.0.0)

    Returns:
        True if the port is available
    """

    def check_socket(sock_type: socket.SocketKind) -> bool:
        try:
            # Determine address family
            family = socket.AF_INET6 if ":" in host else socket.AF_INET
            with socket.socket(family, sock_type) as sock:
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                sock.bind((host, port))
                return True
        except OSError:
            return False

    if protocol == Protocol.TCP:
        return check_socket(socket.SOCK_STREAM)
    elif protocol == Protocol.UDP:
        return check_socket(socket.SOCK_DGRAM)
    else:  # BOTH
        return check_socket(socket.SOCK_STREAM) and check_socket(socket.SOCK_DGRAM)


def check_port_conflicts(
    mappings: list[PortMapping],
    runtime: ContainerRuntime | None = None,
    exclude_container: str | None = None,
) -> list[PortConflictError]:
    """Check for port conflicts with running containers.

    Args:
        mappings: Port mappings to check
        runtime: Runtime to check containers on (checks all if None)
        exclude_container: Container name to exclude from check

    Returns:
        List of PortConflictError for each conflict found
    """
    from gaol.runtimes import get_runtime, list_available_runtimes

    conflicts: list[PortConflictError] = []

    # Get running containers
    runtimes_to_check: list[ContainerRuntime] = []
    if runtime:
        runtimes_to_check = [runtime]
    else:
        for name, available in list_available_runtimes():
            if available:
                try:
                    runtimes_to_check.append(get_runtime(name))
                except Exception:
                    continue

    for rt in runtimes_to_check:
        try:
            containers = rt.list()
        except Exception:
            continue

        for container in containers:
            if exclude_container and container.name == exclude_container:
                continue

            # Skip non-running containers
            if container.status.value not in ("running",):
                continue

            for mapping in mappings:
                # Check if container uses this port
                if mapping.host_port in container.ports:
                    conflicts.append(
                        PortConflictError(
                            mapping.host_port,
                            mapping.protocol,
                            container.name,
                        )
                    )

    return conflicts


def validate_port_mappings(
    mappings: list[PortMapping],
    check_availability: bool = True,
    check_conflicts: bool = True,
    runtime: ContainerRuntime | None = None,
    exclude_container: str | None = None,
) -> list[PortError]:
    """Validate port mappings for availability and conflicts.

    Args:
        mappings: Port mappings to validate
        check_availability: Check if ports are available on host
        check_conflicts: Check for conflicts with running containers
        runtime: Runtime for conflict checking
        exclude_container: Container to exclude from conflict check

    Returns:
        List of errors found (empty if all valid)
    """
    errors: list[PortError] = []

    if check_availability:
        for mapping in mappings:
            if not is_port_available(
                mapping.host_port, mapping.protocol, mapping.host_ip
            ):
                errors.append(PortInUseError(mapping.host_port, mapping.protocol))

    if check_conflicts:
        errors.extend(check_port_conflicts(mappings, runtime, exclude_container))

    return errors


def find_available_port(
    start: int = 8000,
    end: int = 9000,
    protocol: Protocol = Protocol.TCP,
    host: str = "0.0.0.0",
) -> int | None:
    """Find an available port in a range.

    Args:
        start: Start of port range (inclusive)
        end: End of port range (inclusive)
        protocol: Protocol to check
        host: Host address

    Returns:
        Available port number, or None if none found
    """
    for port in range(start, end + 1):
        if is_port_available(port, protocol, host):
            return port
    return None


def get_port_process(port: int, protocol: Protocol = Protocol.TCP) -> str | None:
    """Get the process name using a port (Linux only).

    Args:
        port: Port number
        protocol: Protocol (TCP or UDP)

    Returns:
        Process name or None if not found/not available
    """
    # Try ss first (modern), then netstat
    if shutil.which("ss"):
        proto_flag = "-t" if protocol == Protocol.TCP else "-u"
        try:
            result = subprocess.run(
                ["ss", proto_flag, "-lnp", f"sport = :{port}"],
                capture_output=True,
                text=True,
            )
            if result.returncode == 0 and result.stdout:
                # Parse ss output to find process name
                for line in result.stdout.splitlines():
                    # Extract process info from users:(("name",pid=..))
                    if f":{port}" in line and "users:" in line:
                        start = line.find('(("') + 3
                        end = line.find('"', start)
                        if start > 2 and end > start:
                            return line[start:end]
        except Exception:
            pass

    return None
