"""Mesh configuration — YAML-backed dataclass."""
from __future__ import annotations

import socket
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional

import yaml


@dataclass
class NodeConfig:
    """Configuration for a single mesh node."""
    name: str = ""
    host: str = "0.0.0.0"
    agent_port: int = 8400
    ssh_user: str = "craighass"
    ssh_key: str = "~/.ssh/id_ed25519"
    tags: List[str] = field(default_factory=list)
    scratch_paths: List[str] = field(default_factory=list)
    slento_cuda_path: str = ""

    # WAN support: the URL the controller should use to reach this agent.
    # Can be a public IP, domain, or reverse proxy URL (e.g. "https://agent1.example.com:8400").
    # If empty, falls back to auto-detected local IP + agent_port (current LAN behavior).
    public_url: str = ""

    # NAT-friendly mode: when True, the agent does NOT start its own API server.
    # It only pushes heartbeats/atlas data to the controller and polls for job assignments.
    # This allows agents behind NAT/firewalls to participate without port forwarding.
    nat_mode: bool = False

    # How often (seconds) to poll the controller for pending jobs in NAT mode.
    # Only used when nat_mode=True. Lower values = more responsive but more traffic.
    nat_poll_interval_s: float = 15.0


@dataclass
class ControllerConfig:
    """Controller-specific settings."""
    host: str = "0.0.0.0"
    api_port: int = 8401
    dashboard_port: int = 8402
    heartbeat_interval_s: float = 10.0
    controller_heartbeat_s: float = 15.0
    node_timeout_s: float = 120.0
    probe_interval_s: float = 21600.0  # 6 hours
    retrain_interval_s: float = 21600.0
    atlas_dir: str = "data"
    jepa_model_path: str = "data/jepa_policy_model.pt"


@dataclass
class SecurityConfig:
    """Authentication and security settings."""
    auth_secret: str = ""  # HMAC signing secret; if empty, a random one is generated at startup
    require_auth: bool = False  # Opt-in: set True to enforce token auth
    token_ttl_hours: int = 24
    token_db_path: str = "data/mesh_tokens.db"
    rate_limit: int = 100  # Max requests per minute per IP
    # TLS settings (opt-in)
    tls_cert_path: str = ""  # Path to TLS certificate PEM
    tls_key_path: str = ""   # Path to TLS private key PEM
    tls_ca_path: str = ""    # Path to CA bundle for client verification
    verify_tls: bool = True  # Verify TLS certificates in client connections
    # SSRF prevention
    allowed_networks: Optional[List[str]] = None  # CIDR allowlist for agent URLs (None = allow all)
    allow_localhost: bool = False  # Allow loopback addresses in agent URLs


@dataclass
class UpdateConfig:
    """Auto-update settings."""
    auto_update: bool = False  # Enable automatic update checks
    update_channel: str = "stable"  # stable/beta/nightly
    update_server_url: str = ""  # URL of update server (empty = manual only)
    check_interval_hours: int = 24  # How often to check for updates
    auto_restart: bool = True  # Restart service after applying update
    backup_count: int = 3  # Number of backups to keep


@dataclass
class LicensingConfig:
    """License enforcement settings."""
    license_key: str = ""  # Empty = community mode (5 nodes, basic features)
    license_server_url: str = "https://license.mesh-optimizer.io"
    telemetry_enabled: bool = True
    offline_grace_days: int = 7


@dataclass
class VaultConfig:
    """Credential vault settings."""
    enabled: bool = False
    db_path: str = "data/mesh_vault.db"
    mcp_port: int = 8403
    master_passphrase_env: str = "MESH_VAULT_PASSPHRASE"  # Read from env var
    auto_unseal: bool = False  # If True, auto-unseal from env var on startup
    grant_ttl_minutes: int = 15
    max_access_rate: int = 10  # Per minute per credential


@dataclass
class MeshConfig:
    """Top-level mesh configuration."""
    cluster_name: str = "rdna3-mesh"
    node: NodeConfig = field(default_factory=NodeConfig)
    controller: ControllerConfig = field(default_factory=ControllerConfig)
    security: SecurityConfig = field(default_factory=SecurityConfig)
    licensing: LicensingConfig = field(default_factory=LicensingConfig)
    update: UpdateConfig = field(default_factory=UpdateConfig)
    vault: VaultConfig = field(default_factory=VaultConfig)
    controller_url: str = "http://localhost:8401"
    known_nodes: List[Dict[str, str]] = field(default_factory=list)
    log_level: str = "INFO"
    db_path: str = "data/mesh.db"

    @classmethod
    def from_yaml(cls, path: str | Path) -> MeshConfig:
        path = Path(path)
        if not path.exists():
            return cls()
        with open(path) as f:
            raw = yaml.safe_load(f) or {}
        cfg = cls()
        if "cluster_name" in raw:
            cfg.cluster_name = raw["cluster_name"]
        if "controller_url" in raw:
            cfg.controller_url = raw["controller_url"]
        if "log_level" in raw:
            cfg.log_level = raw["log_level"]
        if "db_path" in raw:
            cfg.db_path = raw["db_path"]
        if "node" in raw:
            for k, v in raw["node"].items():
                if hasattr(cfg.node, k):
                    setattr(cfg.node, k, v)
        if "controller" in raw:
            for k, v in raw["controller"].items():
                if hasattr(cfg.controller, k):
                    setattr(cfg.controller, k, v)
        if "security" in raw:
            for k, v in raw["security"].items():
                if hasattr(cfg.security, k):
                    setattr(cfg.security, k, v)
        if "licensing" in raw:
            for k, v in raw["licensing"].items():
                if hasattr(cfg.licensing, k):
                    setattr(cfg.licensing, k, v)
        if "update" in raw:
            for k, v in raw["update"].items():
                if hasattr(cfg.update, k):
                    setattr(cfg.update, k, v)
        if "vault" in raw:
            for k, v in raw["vault"].items():
                if hasattr(cfg.vault, k):
                    setattr(cfg.vault, k, v)
        if "known_nodes" in raw:
            cfg.known_nodes = raw["known_nodes"]
        # Generate a random secret if none provided
        if not cfg.security.auth_secret:
            import secrets as _secrets
            cfg.security.auth_secret = _secrets.token_hex(32)
        return cfg

    def to_yaml(self, path: str | Path) -> None:
        import dataclasses
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        data = dataclasses.asdict(self)
        with open(path, "w") as f:
            yaml.dump(data, f, default_flow_style=False, sort_keys=False)

    def get_node_name(self) -> str:
        if self.node.name:
            return self.node.name
        return socket.gethostname()
