"""Configuration for the local OMNI Windows desktop companion.

The companion deliberately accepts literal loopback addresses only.  It is a
privileged boundary even when the active driver is the built-in no-op driver,
so accidentally exposing it on a LAN interface is never a valid configuration.
"""

from __future__ import annotations

from dataclasses import dataclass, field
import ipaddress
import os
from pathlib import Path
import string
from urllib.parse import urlparse


TOKEN_ENV = "OMNI_DESKTOP_TOKEN"


@dataclass(frozen=True, slots=True)
class CompanionConfig:
    """Runtime settings with secure local-only defaults."""

    host: str = "127.0.0.1"
    port: int = 8765
    token: str = field(default="", repr=False)
    driver: str = "noop"
    overlay_enabled: bool = True
    cursor_scale: float = 3.0
    activity_ttl_seconds: float = 1.4
    error_ttl_seconds: float = 2.0
    max_body_bytes: int = 64 * 1024
    node_id: str = "windows-local"
    node_name: str = "Windows computer"
    remote_listener_enabled: bool = False
    mtls_ca_path: Path | None = None
    mtls_cert_path: Path | None = None
    mtls_key_path: Path | None = None
    trust_store_path: Path | None = None
    grant_hmac_key: str = field(default="", repr=False)
    audit_path: Path | None = None
    enable_real_control: bool = False
    artifact_dir: Path | None = None
    max_file_read_bytes: int = 256 * 1024
    mdns_enabled: bool = False
    pairing_enabled: bool = False
    pairing_code: str = field(default="", repr=False)
    pairing_sas_confirmed: bool = False
    pairing_expected_client_fingerprint_sha256: str = ""
    pairing_window_seconds: int = 120
    pairing_max_attempts: int = 5
    hermes_proxy_enabled: bool = False
    hermes_url: str = "http://127.0.0.1:8642"
    hermes_api_key: str = field(default="", repr=False)
    hermes_bridge_token: str = field(default="", repr=False)
    hermes_bridge_port: int = 8766
    hermes_bridge_enabled: bool = False
    pairing_allowed_permissions: tuple[str, ...] = (
        "desktop.observe",
        "desktop.control",
        "files.read",
        "files.write",
        "tool.execute",
        "webcam.read",
        "network.search",
    )

    def __post_init__(self) -> None:
        try:
            address = ipaddress.ip_address(self.host)
        except ValueError as exc:
            raise ValueError("host must be a literal loopback IP address") from exc
        if address.is_unspecified:
            raise ValueError("unspecified remote binds such as 0.0.0.0 and :: are forbidden")
        if not address.is_loopback:
            if not self.remote_listener_enabled:
                raise ValueError("non-loopback bind requires explicit remote_listener_enabled")
            if not address.is_private:
                raise ValueError("remote desktop companion requires a concrete private interface IP")
        if not 0 <= self.port <= 65535:
            raise ValueError("port must be between 0 and 65535")
        if len(self.token) < 16 or "\n" in self.token or "\r" in self.token:
            raise ValueError("Bearer token must contain at least 16 safe characters")
        if self.driver not in {"noop", "mock", "windows"}:
            raise ValueError("driver must be noop, mock or explicitly enabled windows")
        if not 1.0 <= self.cursor_scale <= 4.0:
            raise ValueError("cursor_scale must be between 1.0 and 4.0")
        if self.activity_ttl_seconds <= 0 or self.error_ttl_seconds <= 0:
            raise ValueError("overlay TTL values must be positive")
        if self.max_body_bytes < 1024:
            raise ValueError("max_body_bytes must be at least 1024")
        if self.grant_hmac_key and len(self.grant_hmac_key.encode("utf-8")) < 32:
            raise ValueError("grant HMAC key must contain at least 32 bytes")
        id_characters = set(string.ascii_letters + string.digits + "_.:-")
        if (
            not self.node_id
            or len(self.node_id) > 160
            or self.node_id[0] not in string.ascii_letters + string.digits
            or any(character not in id_characters for character in self.node_id)
        ):
            raise ValueError("node_id must contain between 1 and 160 characters")
        if not self.node_name or len(self.node_name) > 240:
            raise ValueError("node_name must contain between 1 and 240 characters")

        path_fields = (
            "mtls_ca_path",
            "mtls_cert_path",
            "mtls_key_path",
            "trust_store_path",
            "audit_path",
            "artifact_dir",
        )
        for field_name in path_fields:
            value = getattr(self, field_name)
            if value is not None and not isinstance(value, Path):
                object.__setattr__(self, field_name, Path(value))
        if self.remote_listener_enabled:
            required_paths = {
                "mtls_ca_path": self.mtls_ca_path,
                "mtls_cert_path": self.mtls_cert_path,
                "mtls_key_path": self.mtls_key_path,
                "trust_store_path": self.trust_store_path,
            }
            for label, path in required_paths.items():
                if path is None or not path.is_file():
                    raise ValueError(f"remote listener requires an existing {label}")
            if len(self.grant_hmac_key.encode("utf-8")) < 32:
                raise ValueError("remote listener requires a separate 32-byte grant HMAC key")
            if self.audit_path is None:
                raise ValueError("remote listener requires an append-only audit_path")
        if self.driver == "windows":
            if not self.enable_real_control:
                raise ValueError("windows driver requires explicit enable_real_control")
            if self.artifact_dir is None:
                raise ValueError("windows driver requires an explicit artifact_dir")
            if len(self.grant_hmac_key.encode("utf-8")) < 32:
                raise ValueError("windows driver requires signed scoped grants")
        if not 4096 <= self.max_file_read_bytes <= 4 * 1024 * 1024:
            raise ValueError("max_file_read_bytes must be between 4096 and 4194304")
        if self.mdns_enabled and not self.remote_listener_enabled:
            raise ValueError("mDNS advertisement requires the explicit remote listener")
        if self.mdns_enabled and address.is_loopback:
            raise ValueError("mDNS advertisement requires a concrete private LAN interface")
        if self.pairing_enabled:
            if not self.remote_listener_enabled:
                raise ValueError("pairing requires the explicit mTLS remote listener")
            if len(self.pairing_code) < 8:
                raise ValueError("pairing requires a one-time code with at least 8 characters")
            if not self.pairing_sas_confirmed:
                raise ValueError("pairing requires explicit local SAS confirmation")
            expected_fingerprint = self.pairing_expected_client_fingerprint_sha256.lower()
            if len(expected_fingerprint) != 64 or any(
                character not in "0123456789abcdef" for character in expected_fingerprint
            ):
                raise ValueError("pairing requires the confirmed client certificate fingerprint")
            if not 10 <= self.pairing_window_seconds <= 120:
                raise ValueError("pairing window must be between 10 and 120 seconds")
            if not 1 <= self.pairing_max_attempts <= 10:
                raise ValueError("pairing max attempts must be between 1 and 10")
        if self.hermes_proxy_enabled:
            if not self.remote_listener_enabled:
                raise ValueError("Hermes node proxy requires the explicit mTLS remote listener")
            if not self.hermes_bridge_enabled:
                raise ValueError("Hermes node proxy requires the local Hermes tool bridge")
        if self.hermes_bridge_enabled:
            if len(self.hermes_bridge_token.encode("utf-8")) < 32:
                raise ValueError("Hermes tool bridge requires a separate 32-byte bridge token")
            if len(self.grant_hmac_key.encode("utf-8")) < 32:
                raise ValueError("Hermes tool bridge requires signed scoped grants")
        if not 0 <= self.hermes_bridge_port <= 65535:
            raise ValueError("Hermes bridge port must be between 0 and 65535")
        parsed_hermes = urlparse(self.hermes_url)
        try:
            hermes_address = ipaddress.ip_address(parsed_hermes.hostname or "")
        except ValueError as exc:
            raise ValueError("Hermes proxy URL must use a literal loopback address") from exc
        if (
            parsed_hermes.scheme not in {"http", "https"}
            or not hermes_address.is_loopback
            or parsed_hermes.username
            or parsed_hermes.password
            or parsed_hermes.path not in {"", "/"}
        ):
            raise ValueError("Hermes proxy URL must be a credential-free loopback origin")

    @classmethod
    def from_env(cls, **overrides: object) -> "CompanionConfig":
        """Build a configuration without ever inventing an authentication token."""

        values: dict[str, object] = {
            "host": os.environ.get("OMNI_DESKTOP_HOST", "127.0.0.1"),
            "port": int(os.environ.get("OMNI_DESKTOP_PORT", "8765")),
            "token": os.environ.get(TOKEN_ENV, ""),
            "driver": os.environ.get("OMNI_DESKTOP_DRIVER", "noop"),
            "node_id": os.environ.get("OMNI_DESKTOP_NODE_ID", "windows-local"),
            "node_name": os.environ.get("OMNI_DESKTOP_NODE_NAME", "Windows computer"),
            "remote_listener_enabled": _env_bool("OMNI_REMOTE_LISTENER_ENABLED", False),
            "mtls_ca_path": _env_path("OMNI_MTLS_CA_PATH"),
            "mtls_cert_path": _env_path("OMNI_MTLS_CERT_PATH"),
            "mtls_key_path": _env_path("OMNI_MTLS_KEY_PATH"),
            "trust_store_path": _env_path("OMNI_NODE_ALLOWLIST_PATH"),
            "grant_hmac_key": os.environ.get("OMNI_DESKTOP_GRANT_KEY", ""),
            "audit_path": _env_path("OMNI_REMOTE_AUDIT_PATH"),
            "enable_real_control": _env_bool("OMNI_DESKTOP_ENABLE_REAL_CONTROL", False),
            "artifact_dir": _env_path("OMNI_DESKTOP_ARTIFACT_DIR"),
            "max_file_read_bytes": int(os.environ.get("OMNI_DESKTOP_MAX_FILE_BYTES", str(256 * 1024))),
            "mdns_enabled": _env_bool("OMNI_MDNS_DISCOVERY_ENABLED", False),
            "pairing_enabled": _env_bool("OMNI_PAIRING_ENABLED", False),
            "pairing_code": os.environ.get("OMNI_PAIRING_CODE", ""),
            "pairing_sas_confirmed": _env_bool("OMNI_PAIRING_SAS_CONFIRMED", False),
            "pairing_expected_client_fingerprint_sha256": os.environ.get(
                "OMNI_PAIRING_EXPECTED_CLIENT_FINGERPRINT_SHA256",
                "",
            ),
            "pairing_window_seconds": int(os.environ.get("OMNI_PAIRING_WINDOW_SECONDS", "120")),
            "pairing_max_attempts": int(os.environ.get("OMNI_PAIRING_MAX_ATTEMPTS", "5")),
            "hermes_proxy_enabled": _env_bool("OMNI_NODE_HERMES_PROXY_ENABLED", False),
            "hermes_url": os.environ.get("OMNI_NODE_HERMES_URL", "http://127.0.0.1:8642"),
            "hermes_api_key": os.environ.get("HERMES_API_KEY", ""),
            "hermes_bridge_token": os.environ.get("OMNI_HERMES_BRIDGE_TOKEN", ""),
            "hermes_bridge_port": int(os.environ.get("OMNI_HERMES_BRIDGE_PORT", "8766")),
            "hermes_bridge_enabled": _env_bool("OMNI_HERMES_BRIDGE_ENABLED", False),
        }
        values.update(overrides)
        return cls(**values)

    def public_dict(self) -> dict[str, object]:
        """Return diagnostics that intentionally omit the bearer credential."""

        return {
            "host": self.host,
            "port": self.port,
            "driver": self.driver,
            "overlay_enabled": self.overlay_enabled,
            "cursor_scale": self.cursor_scale,
            "activity_ttl_seconds": self.activity_ttl_seconds,
            "node_id": self.node_id,
            "node_name": self.node_name,
            "remote_listener_enabled": self.remote_listener_enabled,
            "mtls_required": self.remote_listener_enabled,
            "real_control_enabled": self.enable_real_control,
            "mdns_enabled": self.mdns_enabled,
            "pairing_enabled": self.pairing_enabled,
            "hermes_proxy_enabled": self.hermes_proxy_enabled,
            "hermes_bridge_port": self.hermes_bridge_port if self.hermes_bridge_enabled else None,
            "hermes_bridge_enabled": self.hermes_bridge_enabled,
        }

    @property
    def is_loopback(self) -> bool:
        return ipaddress.ip_address(self.host).is_loopback


def _env_bool(name: str, default: bool) -> bool:
    raw = os.environ.get(name)
    if raw is None:
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}


def _env_path(name: str) -> Path | None:
    raw = os.environ.get(name, "").strip()
    return Path(raw) if raw else None
