from __future__ import annotations

import ipaddress
import os
import re
from dataclasses import dataclass
from urllib.parse import urlparse


class ConfigurationError(ValueError):
    pass


_CONTAINER_HOST = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")


def _env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name)
    if raw is None:
        return default
    value = raw.strip().casefold()
    if value in {"1", "true", "yes", "on"}:
        return True
    if value in {"0", "false", "no", "off"}:
        return False
    raise ConfigurationError(f"{name} must be a boolean")


def _env_int(name: str, default: int) -> int:
    try:
        return int(os.getenv(name, str(default)))
    except ValueError as exc:
        raise ConfigurationError(f"{name} must be an integer") from exc


def _env_float(name: str, default: float) -> float:
    try:
        return float(os.getenv(name, str(default)))
    except ValueError as exc:
        raise ConfigurationError(f"{name} must be a number") from exc


def validate_local_url(value: str, *, allow_container: bool, name: str) -> str:
    parsed = urlparse(value)
    if parsed.scheme not in {"http", "https"} or not parsed.hostname:
        raise ConfigurationError(f"{name} must be an explicit HTTP(S) URL")
    if parsed.username or parsed.password or parsed.fragment:
        raise ConfigurationError(f"{name} must not contain credentials or a fragment")
    host = parsed.hostname.casefold()
    if host not in {"127.0.0.1", "localhost", "::1"}:
        safe = bool(_CONTAINER_HOST.fullmatch(host))
        try:
            address = ipaddress.ip_address(host)
        except ValueError:
            pass
        else:
            safe = address.is_private or address.is_loopback or address.is_link_local
        if not allow_container or not safe:
            raise ConfigurationError(
                f"{name} must be loopback or an explicitly allowed local container endpoint"
            )
    return value.rstrip("/")


@dataclass(slots=True)
class ManagerSettings:
    host: str = "127.0.0.1"
    port: int = 8110
    allow_container_bind: bool = False
    allow_container_endpoints: bool = False
    media_url: str = "http://127.0.0.1:8111"
    launch_enabled: bool = False
    video_launch_url: str | None = None
    image_launch_url: str | None = None
    video_release_url: str | None = None
    image_release_url: str | None = None
    max_active_leases: int = 64
    max_records: int = 256
    lease_ttl_s: float = 1800.0
    connect_timeout_s: float = 0.75
    request_timeout_s: float = 20.0
    max_response_bytes: int = 64 * 1024
    max_request_bytes: int = 16 * 1024

    @classmethod
    def from_env(cls) -> "ManagerSettings":
        return cls(
            host=os.getenv("OMNI_MANAGER_HOST", "127.0.0.1"),
            port=_env_int("OMNI_MANAGER_PORT", 8110),
            allow_container_bind=_env_bool("OMNI_MANAGER_ALLOW_CONTAINER_BIND"),
            allow_container_endpoints=_env_bool("OMNI_MANAGER_ALLOW_CONTAINER_ENDPOINTS"),
            media_url=os.getenv("OMNI_MANAGER_MEDIA_URL", "http://127.0.0.1:8111"),
            launch_enabled=_env_bool("OMNI_MANAGER_LAUNCH_ENABLED"),
            video_launch_url=os.getenv("OMNI_MANAGER_VIDEO_LAUNCH_URL") or None,
            image_launch_url=os.getenv("OMNI_MANAGER_IMAGE_LAUNCH_URL") or None,
            video_release_url=os.getenv("OMNI_MANAGER_VIDEO_RELEASE_URL") or None,
            image_release_url=os.getenv("OMNI_MANAGER_IMAGE_RELEASE_URL") or None,
            max_active_leases=_env_int("OMNI_MANAGER_MAX_ACTIVE_LEASES", 64),
            max_records=_env_int("OMNI_MANAGER_MAX_RECORDS", 256),
            lease_ttl_s=_env_float("OMNI_MANAGER_LEASE_TTL_S", 1800),
            connect_timeout_s=_env_float("OMNI_MANAGER_CONNECT_TIMEOUT_S", 0.75),
            request_timeout_s=_env_float("OMNI_MANAGER_REQUEST_TIMEOUT_S", 20),
            max_response_bytes=_env_int("OMNI_MANAGER_MAX_RESPONSE_BYTES", 64 * 1024),
            max_request_bytes=_env_int("OMNI_MANAGER_MAX_REQUEST_BYTES", 16 * 1024),
        ).validated()

    def validated(self) -> "ManagerSettings":
        if self.host not in {"127.0.0.1", "localhost", "::1"}:
            if self.host not in {"0.0.0.0", "::"} or not self.allow_container_bind:
                raise ConfigurationError(
                    "OMNI_MANAGER_HOST must be loopback; a wildcard container bind "
                    "requires OMNI_MANAGER_ALLOW_CONTAINER_BIND=1"
                )
        if not 1 <= self.port <= 65_535:
            raise ConfigurationError("OMNI_MANAGER_PORT must be a valid port")
        for name in (
            "max_active_leases", "max_records", "max_response_bytes", "max_request_bytes"
        ):
            if getattr(self, name) <= 0:
                raise ConfigurationError(f"{name} must be positive")
        if self.max_records < self.max_active_leases:
            raise ConfigurationError("max_records cannot be smaller than max_active_leases")
        if min(self.lease_ttl_s, self.connect_timeout_s, self.request_timeout_s) <= 0:
            raise ConfigurationError("manager timeouts and lease TTL must be positive")
        self.media_url = validate_local_url(
            self.media_url,
            allow_container=self.allow_container_endpoints,
            name="OMNI_MANAGER_MEDIA_URL",
        )
        for field_name in (
            "video_launch_url", "image_launch_url", "video_release_url", "image_release_url"
        ):
            value = getattr(self, field_name)
            if value:
                setattr(
                    self,
                    field_name,
                    validate_local_url(
                        value,
                        allow_container=self.allow_container_endpoints,
                        name=field_name,
                    ),
                )
        return self

    def launch_url(self, capability: str) -> str | None:
        return self.video_launch_url if capability == "video" else self.image_launch_url

    def release_url(self, capability: str) -> str | None:
        return self.video_release_url if capability == "video" else self.image_release_url
