"""
Config settings graph: which keys are required or optional depending on context.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com

Used to verify generator output and validator behaviour. Same setting can be
required in one context (e.g. server.ssl when protocol=https) and optional in another.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Set, Tuple

# Graph: section -> key -> requirement
# requirement: True (always required), False (optional), or "when" dict: {"section.key": value}
# For "when", the key is required when the condition holds.
CONFIG_GRAPH: Dict[str, Dict[str, Any]] = {
    "server": {
        "host": True,
        "port": True,
        "protocol": True,
        "servername": True,
        "ssl": "when",  # required when server.protocol in ("https", "mtls")
        "rules": False,
        "log_dir": True,
        "debug": True,
        "log_level": True,
        "advertised_host": False,
    },
    "server.ssl": {
        "cert": "when",  # required when protocol in (https, mtls); mtls requires cert+key
        "key": "when",
        "ca": "when",  # required when protocol=mtls
        "crl": False,
        "dnscheck": False,
        "check_hostname": False,
    },
    "client": {
        "enabled": True,
        "protocol": True,
        "ssl": "when",  # when client.enabled and client.protocol in (https, mtls)
    },
    "registration": {
        "enabled": True,
        "protocol": True,
        "register_url": "when",  # when enabled
        "unregister_url": "when",
        "heartbeat_interval": "when",
        "ssl": "when",  # when enabled and protocol in (https, mtls)
        "server_id": False,
        "server_name": False,
        "instance_uuid": "when",  # when enabled, must be UUID4
        "auto_on_startup": True,
        "auto_on_shutdown": True,
        "heartbeat": True,
    },
    "registration.heartbeat": {
        "url": False,
        "interval": True,
    },
    "server_validation": {
        "enabled": True,
        "protocol": True,
        "ssl": False,
        "timeout": True,
        "use_token": True,
        "use_roles": True,
        "tokens": True,
        "roles": True,
        "auth_header": True,
        "roles_header": True,
        "health_path": True,
        "check_hostname": True,
    },
    "auth": {
        "use_token": True,
        "use_roles": True,
        "tokens": "when",  # when use_token
        "roles": True,
    },
    "queue_manager": {
        "enabled": True,
        "in_memory": True,
        "registry_path": False,
        "shutdown_timeout": True,
        "max_concurrent_jobs": True,
        "max_queue_size": False,
        "per_job_type_limits": False,
        "completed_job_retention_seconds": True,
        "default_poll_interval": True,
        "default_max_wait_time": False,
    },
}


def _get_nested(data: Dict[str, Any], path: str) -> Any:
    """Get value at dot path (e.g. server.protocol)."""
    parts = path.split(".", 1)
    if len(parts) == 1:
        return data.get(path)
    key, rest = parts
    if key not in data:
        return None
    return _get_nested(data[key] if isinstance(data[key], dict) else {}, rest)


def _context_required(
    section: str,
    key: str,
    config: Dict[str, Any],
    graph: Dict[str, Dict[str, Any]],
) -> bool:
    """Return True if key is required in config under current context."""
    req = graph.get(section, {}).get(key)
    if req is True:
        return True
    if req is False or req is None:
        return False
    if req != "when":
        return False
    # Context-dependent
    if section == "server" and key == "ssl":
        return _get_nested(config, "server.protocol") in ("https", "mtls")
    if section == "server.ssl":
        protocol = _get_nested(config, "server.protocol")
        if key == "ca":
            return protocol == "mtls"
        if key in ("cert", "key"):
            return protocol in ("https", "mtls")
        return False
    if section == "client" and key == "ssl":
        if not _get_nested(config, "client.enabled"):
            return False
        return _get_nested(config, "client.protocol") in ("https", "mtls")
    if section == "registration":
        if not _get_nested(config, "registration.enabled"):
            return False
        if key in (
            "register_url",
            "unregister_url",
            "heartbeat_interval",
            "instance_uuid",
        ):
            return True
        if key == "ssl":
            return _get_nested(config, "registration.protocol") in ("https", "mtls")
        return False
    if section == "auth" and key == "tokens":
        return _get_nested(config, "auth.use_token") is True
    return False


def check_config_against_graph(
    config: Dict[str, Any],
    graph: Optional[Dict[str, Dict[str, Any]]] = None,
) -> List[Tuple[str, str]]:
    """
    Check that config has all keys required by the graph for its current context.
    Returns list of (path, message) for missing required keys.
    """
    graph = graph or CONFIG_GRAPH
    errors: List[Tuple[str, str]] = []

    def check_section(section: str, data: Any, prefix: str) -> None:
        if not isinstance(data, dict):
            return
        for key, req in graph.get(section, {}).items():
            path = f"{prefix}.{key}" if prefix else key
            if key not in data or data[key] is None:
                if _context_required(section, key, config, graph):
                    errors.append((path, f"Missing required key: {path}"))
            elif isinstance(req, dict) and isinstance(data.get(key), dict):
                sub_section = f"{section}.{key}"
                if sub_section in graph:
                    check_section(sub_section, data[key], path)

    for section in (
        "server",
        "client",
        "registration",
        "server_validation",
        "auth",
        "queue_manager",
    ):
        if section not in config:
            if any(
                graph.get(section, {}).get(k) is True for k in graph.get(section, {})
            ):
                errors.append((section, f"Missing required section: {section}"))
            continue
        check_section(section, config[section], section)
        if section == "server" and isinstance(config["server"].get("ssl"), dict):
            check_section("server.ssl", config["server"]["ssl"], "server.ssl")
        if section == "registration" and isinstance(
            config["registration"].get("heartbeat"), dict
        ):
            check_section(
                "registration.heartbeat",
                config["registration"]["heartbeat"],
                "registration.heartbeat",
            )
    return errors


def required_keys_for_context(config: Dict[str, Any]) -> Set[str]:
    """Return set of key paths that are required for this config's context."""
    # Build required set from graph for current context.
    required: Set[str] = set()
    protocol = _get_nested(config, "server.protocol")
    reg_enabled = _get_nested(config, "registration.enabled")
    auth_use_token = _get_nested(config, "auth.use_token")

    for section, keys in CONFIG_GRAPH.items():
        if section in ("server.ssl", "registration.heartbeat"):
            continue
        for key, req in keys.items():
            if req is True:
                required.add(f"{section}.{key}")
            elif req == "when":
                if (
                    section == "server"
                    and key == "ssl"
                    and protocol in ("https", "mtls")
                ):
                    required.add("server.ssl")
                if (
                    section == "registration"
                    and reg_enabled
                    and key
                    in (
                        "register_url",
                        "unregister_url",
                        "heartbeat_interval",
                        "instance_uuid",
                    )
                ):
                    required.add(f"registration.{key}")
                if section == "auth" and key == "tokens" and auth_use_token:
                    required.add("auth.tokens")
    return required
