"""Declarative container specification models.

This module provides user-friendly models for defining containers
in YAML or TOML files. The spec format supports human-readable
values like "4G" for memory and "8080:80/tcp" for ports.
"""

from __future__ import annotations

import re
from typing import Annotated, Any

from pydantic import BaseModel, Field, field_validator, model_validator

from gaol.models.container import (
    ContainerConfig,
    NetworkConfig,
    NetworkMode,
    ResourceLimits,
    ServiceConfig,
    UserConfig,
)


def parse_memory(value: str | int | None) -> int | None:
    """Parse a memory value with units to MB.

    Supports: 512M, 1G, 2g, 1024, etc.
    """
    if value is None:
        return None
    if isinstance(value, int):
        return value

    value = value.strip().upper()
    match = re.match(r"^(\d+(?:\.\d+)?)\s*(B|K|KB|M|MB|G|GB|T|TB)?$", value)
    if not match:
        raise ValueError(f"Invalid memory format: {value}")

    num = float(match.group(1))
    unit = match.group(2) or "MB"

    multipliers = {
        "B": 1 / (1024 * 1024),
        "K": 1 / 1024,
        "KB": 1 / 1024,
        "M": 1,
        "MB": 1,
        "G": 1024,
        "GB": 1024,
        "T": 1024 * 1024,
        "TB": 1024 * 1024,
    }

    return int(num * multipliers[unit])


def parse_cpu(value: str | float | int | None) -> float | None:
    """Parse a CPU value.

    Supports: 2, 1.5, "2", "1.5cpu", "500m" (millicores), etc.
    """
    if value is None:
        return None
    if isinstance(value, (int, float)):
        return float(value)

    value = value.strip().lower()

    # Handle millicores (e.g., 500m = 0.5 cpu)
    if value.endswith("m"):
        return float(value[:-1]) / 1000

    # Handle cpu suffix
    if value.endswith("cpu"):
        return float(value[:-3])

    return float(value)


def parse_port_mapping(value: str | dict) -> dict:
    """Parse a port mapping string or dict.

    Supports:
    - "8080:80" -> {"host": 8080, "container": 80, "protocol": "tcp"}
    - "8080:80/tcp"
    - "53:53/udp"
    - "0.0.0.0:8080:80/tcp"
    - {"host": 8080, "container": 80, "protocol": "udp"}
    """
    if isinstance(value, dict):
        return {
            "host": value.get("host") or value.get("host_port"),
            "container": value.get("container") or value.get("container_port"),
            "protocol": value.get("protocol", "tcp"),
            "host_ip": value.get("host_ip", "0.0.0.0"),
        }

    # Parse string format: [host_ip:]host_port:container_port[/protocol]
    protocol = "tcp"
    if "/" in value:
        value, protocol = value.rsplit("/", 1)

    parts = value.split(":")
    if len(parts) == 2:
        host_port, container_port = parts
        host_ip = "0.0.0.0"
    elif len(parts) == 3:
        host_ip, host_port, container_port = parts
    else:
        raise ValueError(f"Invalid port mapping format: {value}")

    return {
        "host": int(host_port),
        "container": int(container_port),
        "protocol": protocol.lower(),
        "host_ip": host_ip,
    }


def parse_volume(value: str | dict) -> dict:
    """Parse a volume mount string or dict.

    Supports:
    - "~/projects:/workspace"
    - "~/.ssh:/home/user/.ssh:ro"
    - {"host": "~/data", "container": "/data", "readonly": true}
    """
    if isinstance(value, dict):
        return {
            "host": value.get("host") or value.get("source"),
            "container": value.get("container") or value.get("target"),
            "readonly": value.get("readonly", False) or value.get("ro", False),
        }

    # Parse string format: host_path:container_path[:ro]
    parts = value.split(":")

    # Handle Windows paths (C:\...)
    if len(parts) >= 2 and len(parts[0]) == 1 and parts[0].isalpha():
        # First part is a drive letter
        host_path = f"{parts[0]}:{parts[1]}"
        parts = parts[2:]
        if not parts:
            raise ValueError(f"Invalid volume format: {value}")
        container_path = parts[0]
        readonly = len(parts) > 1 and parts[1].lower() == "ro"
    elif len(parts) == 2:
        host_path, container_path = parts
        readonly = False
    elif len(parts) == 3:
        host_path, container_path, mode = parts
        readonly = mode.lower() == "ro"
    else:
        raise ValueError(f"Invalid volume format: {value}")

    return {
        "host": host_path,
        "container": container_path,
        "readonly": readonly,
    }


class SpecResources(BaseModel):
    """Resource limits in user-friendly format."""

    memory: str | int | None = None  # "4G", "512M", 1024
    cpus: str | float | int | None = None  # 2, 1.5, "500m"
    cpu_shares: int | None = None
    disk: str | int | None = None  # "10G", "500M"

    def to_resource_limits(self) -> ResourceLimits:
        """Convert to internal ResourceLimits model."""
        return ResourceLimits(
            memory_mb=parse_memory(self.memory) if self.memory else None,
            cpu_count=parse_cpu(self.cpus) if self.cpus else None,
            cpu_shares=self.cpu_shares,
            disk_mb=parse_memory(self.disk) if self.disk else None,
        )


class SpecNetwork(BaseModel):
    """Network configuration in user-friendly format."""

    mode: str = "shared"  # shared, bridged, host, none
    ports: list[str | dict] = Field(default_factory=list)
    dns: list[str] = Field(default_factory=list)
    hostname: str | None = None

    @field_validator("mode")
    @classmethod
    def validate_mode(cls, v: str) -> str:
        """Validate network mode."""
        valid = {"shared", "bridged", "host", "none"}
        if v.lower() not in valid:
            raise ValueError(f"Invalid network mode: {v}. Must be one of: {valid}")
        return v.lower()

    def to_network_config(self) -> NetworkConfig:
        """Convert to internal NetworkConfig model."""
        # Parse port mappings to simple dict (host -> container)
        ports_dict: dict[int, int] = {}
        for p in self.ports:
            parsed = parse_port_mapping(p)
            ports_dict[parsed["host"]] = parsed["container"]

        return NetworkConfig(
            mode=NetworkMode(self.mode),
            ports=ports_dict,
            dns=self.dns,
            hostname=self.hostname,
        )

    def get_port_mappings(self) -> list[dict]:
        """Get parsed port mappings with full details."""
        return [parse_port_mapping(p) for p in self.ports]


class SpecUser(BaseModel):
    """User configuration in user-friendly format."""

    name: Annotated[str, Field(min_length=1, max_length=32)]
    uid: int | None = None
    gid: int | None = None
    groups: list[str] = Field(default_factory=list)
    home: str | None = None
    shell: str = "/bin/bash"
    create_home: bool = True

    def to_user_config(self) -> UserConfig:
        """Convert to internal UserConfig model."""
        return UserConfig(
            name=self.name,
            uid=self.uid,
            gid=self.gid,
            groups=self.groups,
            home=self.home,
            shell=self.shell,
            create_home=self.create_home,
        )


class SpecService(BaseModel):
    """Service configuration in user-friendly format."""

    name: Annotated[str, Field(min_length=1, max_length=64)]
    command: str
    user: str = "root"
    workdir: str | None = None
    description: str | None = None
    restart: str = "on-failure"
    environment: dict[str, str] = Field(default_factory=dict)
    enabled: bool = True

    def to_service_config(self) -> ServiceConfig:
        """Convert to internal ServiceConfig model."""
        return ServiceConfig(
            name=self.name,
            command=self.command,
            user=self.user,
            workdir=self.workdir,
            description=self.description,
            restart=self.restart,
            environment=self.environment,
            enabled=self.enabled,
        )


class SpecTor(BaseModel):
    """Tor proxy configuration for specs.

    Routes container traffic through the Tor network for anonymity.
    """

    enabled: bool = True
    """Enable Tor proxy for this container."""

    transparent: bool = False
    """Enable transparent proxy mode (routes ALL traffic through Tor including DNS)."""

    socks_port: int = 9050
    """SOCKS5 proxy port."""

    exit_nodes: list[str] = Field(default_factory=list)
    """Preferred exit node country codes (e.g., ['US', 'DE', 'SE'])."""

    exclude_nodes: list[str] = Field(default_factory=list)
    """Country codes to exclude from circuits (e.g., ['RU', 'CN'])."""

    bridge_enabled: bool = False
    """Enable bridge mode for censored networks."""

    bridges: list[str] = Field(default_factory=list)
    """Bridge addresses for censorship circumvention."""


class SpecVPN(BaseModel):
    """VPN gateway configuration for specs.

    Routes container traffic through a VPN provider.
    """

    enabled: bool = True
    """Enable VPN for this container."""

    provider: str = "wireguard"
    """VPN provider type: 'wireguard' or 'openvpn'."""

    config_file: str
    """Path to VPN configuration file (.conf for WireGuard, .ovpn for OpenVPN)."""

    kill_switch: bool = True
    """Block all traffic if VPN connection drops (prevents leaks)."""

    dns_servers: list[str] = Field(default_factory=lambda: ["1.1.1.1", "1.0.0.1"])
    """DNS servers to use when connected (default: Cloudflare)."""

    @field_validator("provider")
    @classmethod
    def validate_provider(cls, v: str) -> str:
        """Validate VPN provider."""
        valid = {"wireguard", "openvpn"}
        if v.lower() not in valid:
            raise ValueError(f"Invalid VPN provider: {v}. Must be one of: {valid}")
        return v.lower()


class SpecOnion(BaseModel):
    """Tor hidden service configuration for specs.

    Exposes container as a .onion address on the Tor network.
    """

    enabled: bool = True
    """Enable hidden service for this container."""

    port: int
    """Port inside the container to expose."""

    onion_port: int = 80
    """Port to expose on the .onion address."""

    version: int = 3
    """Hidden service version (3 recommended, 2 deprecated)."""


class SpecTunnel(BaseModel):
    """Cloudflare tunnel configuration for specs.

    Exposes container to the internet via a public URL.
    """

    enabled: bool = True
    """Enable tunnel for this container."""

    port: int
    """Port inside the container to expose."""

    protocol: str = "http"
    """Protocol: 'http', 'https', or 'tcp'."""

    @field_validator("protocol")
    @classmethod
    def validate_protocol(cls, v: str) -> str:
        """Validate tunnel protocol."""
        valid = {"http", "https", "tcp"}
        if v.lower() not in valid:
            raise ValueError(f"Invalid protocol: {v}. Must be one of: {valid}")
        return v.lower()


class SpecPrivacy(BaseModel):
    """Privacy and exposure configuration for containers.

    Configure Tor routing, VPN connections, hidden services, and tunnels.
    These features are mutually exclusive for routing (tor vs vpn), but
    onion and tunnel can be combined with either.
    """

    tor: SpecTor | None = None
    """Tor proxy configuration for anonymous browsing."""

    vpn: SpecVPN | None = None
    """VPN gateway configuration."""

    onion: SpecOnion | None = None
    """Tor hidden service to expose container as .onion."""

    tunnel: SpecTunnel | None = None
    """Cloudflare tunnel to expose container publicly."""

    @model_validator(mode="after")
    def validate_mutual_exclusion(self) -> SpecPrivacy:
        """Validate that tor and vpn aren't both enabled for routing."""
        if self.tor and self.vpn:
            if self.tor.enabled and self.vpn.enabled:
                raise ValueError(
                    "Cannot enable both Tor and VPN for routing. "
                    "Use one or the other for traffic routing."
                )
        return self


class ContainerSpec(BaseModel):
    """Declarative container specification.

    This is the main model for YAML/TOML container definitions.
    It provides a user-friendly interface while converting to
    the internal ContainerConfig model.
    """

    # Required
    name: Annotated[str, Field(min_length=1, max_length=64)]

    # Basic settings
    runtime: str = "docker"
    distro: str = "trixie"
    image: str | None = None

    # Profiles
    profiles: list[str] = Field(default_factory=list)

    # Resources
    resources: SpecResources = Field(default_factory=SpecResources)

    # Network
    network: SpecNetwork = Field(default_factory=SpecNetwork)

    # Volumes - list of strings or dicts
    volumes: list[str | dict] = Field(default_factory=list)

    # Environment variables
    environment: dict[str, str] = Field(default_factory=dict)
    env_file: str | None = None  # Path to .env file

    # Command and working directory
    command: list[str] | str | None = None
    working_dir: str | None = None

    # Security
    privileged: bool = False
    read_only: bool = False
    private_users: bool = True  # Set False for GUI containers needing host socket access
    capabilities_add: list[str] = Field(default_factory=list)
    capabilities_drop: list[str] = Field(default_factory=list)

    # Lifecycle
    restart: str | None = None  # no, always, on-failure, unless-stopped
    depends_on: list[str] = Field(default_factory=list)  # For future container groups

    # Users and services
    users: list[SpecUser] = Field(default_factory=list)
    services: list[SpecService] = Field(default_factory=list)

    # Packages and setup
    packages: list[str] = Field(default_factory=list)
    setup_commands: list[str] = Field(default_factory=list)

    # Privacy and exposure
    privacy: SpecPrivacy | None = None

    @field_validator("command", mode="before")
    @classmethod
    def normalize_command(cls, v: Any) -> list[str] | None:
        """Convert string command to list."""
        if v is None:
            return None
        if isinstance(v, str):
            # Simple shell-style split (not full shell parsing)
            import shlex

            return shlex.split(v)
        return list(v)

    @model_validator(mode="after")
    def validate_name_pattern(self) -> ContainerSpec:
        """Validate container name pattern."""
        import re

        if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*$", self.name):
            raise ValueError(
                f"Invalid container name: {self.name}. "
                "Must start with alphanumeric and contain only alphanumeric, _, ., -"
            )
        return self

    def get_volumes_dict(self) -> dict[str, str]:
        """Get volumes as host->container dict."""
        result: dict[str, str] = {}
        for v in self.volumes:
            parsed = parse_volume(v)
            result[parsed["host"]] = parsed["container"]
        return result

    def get_volumes_detailed(self) -> list[dict]:
        """Get volumes with full details including readonly flag."""
        return [parse_volume(v) for v in self.volumes]

    def to_container_config(self) -> ContainerConfig:
        """Convert to internal ContainerConfig model."""
        return ContainerConfig(
            name=self.name,
            runtime=self.runtime,
            distro=self.distro,
            image=self.image,
            profiles=self.profiles,
            network=self.network.to_network_config(),
            resources=self.resources.to_resource_limits(),
            volumes=self.get_volumes_dict(),
            environment=self.environment,
            working_dir=self.working_dir,
            command=self.command,
            privileged=self.privileged,
            read_only=self.read_only,
            private_users=self.private_users,
            users=[u.to_user_config() for u in self.users],
            services=[s.to_service_config() for s in self.services],
            packages=self.packages,
            setup_commands=self.setup_commands,
        )

    @classmethod
    def from_container_config(cls, config: ContainerConfig) -> ContainerSpec:
        """Create a spec from an existing ContainerConfig."""
        # Convert ports to string format
        ports = [f"{h}:{c}" for h, c in config.network.ports.items()]

        # Convert volumes to string format
        volumes = [f"{h}:{c}" for h, c in config.volumes.items()]

        # Format resources
        resources = SpecResources()
        if config.resources.memory_mb:
            if config.resources.memory_mb >= 1024:
                resources.memory = f"{config.resources.memory_mb // 1024}G"
            else:
                resources.memory = f"{config.resources.memory_mb}M"
        if config.resources.cpu_count:
            resources.cpus = config.resources.cpu_count
        if config.resources.cpu_shares:
            resources.cpu_shares = config.resources.cpu_shares
        if config.resources.disk_mb:
            if config.resources.disk_mb >= 1024:
                resources.disk = f"{config.resources.disk_mb // 1024}G"
            else:
                resources.disk = f"{config.resources.disk_mb}M"

        return cls(
            name=config.name,
            runtime=config.runtime,
            distro=config.distro,
            image=config.image,
            profiles=config.profiles,
            resources=resources,
            network=SpecNetwork(
                mode=config.network.mode.value,
                ports=ports,
                dns=config.network.dns,
                hostname=config.network.hostname,
            ),
            volumes=volumes,
            environment=config.environment,
            working_dir=config.working_dir,
            command=config.command,
            privileged=config.privileged,
            read_only=config.read_only,
            private_users=config.private_users,
            users=[
                SpecUser(
                    name=u.name,
                    uid=u.uid,
                    gid=u.gid,
                    groups=u.groups,
                    home=u.home,
                    shell=u.shell,
                    create_home=u.create_home,
                )
                for u in config.users
            ],
            services=[
                SpecService(
                    name=s.name,
                    command=s.command,
                    user=s.user,
                    workdir=s.workdir,
                    description=s.description,
                    restart=s.restart,
                    environment=s.environment,
                    enabled=s.enabled,
                )
                for s in config.services
            ],
            packages=config.packages,
        )
