"""Server config generation — produces YAML for the agentsh server.

See spec Section 17 (17.1) for the ``generate_server_config()`` function.
"""

from __future__ import annotations

from typing import Any

import yaml

from agentsh_secure_sandbox.core.types import SecureConfig

# Default threat feeds when threat_feeds is None (not explicitly False)
_DEFAULT_FEEDS = [
    {
        "name": "urlhaus",
        "url": "https://urlhaus.abuse.ch/downloads/hostfile/",
        "format": "hostfile",
        "refresh_interval": "6h",
    },
    {
        "name": "phishing",
        "url": "https://raw.githubusercontent.com/mitchellkrogza/Phishing.Database/master/phishing-domains-ACTIVE.txt",
        "format": "domain-list",
        "refresh_interval": "12h",
    },
]

_DEFAULT_ALLOWLIST = [
    "github.com",
    "*.github.com",
    "registry.npmjs.org",
    "registry.yarnpkg.com",
    "pypi.org",
    "files.pythonhosted.org",
    "crates.io",
    "static.crates.io",
    "index.crates.io",
    "proxy.golang.org",
    "sum.golang.org",
]

_DEFAULT_PACKAGE_CHECK_PROVIDERS = {
    "local": {"enabled": True, "priority": 0},
    "osv": {"enabled": True, "priority": 1},
    "depsdev": {"enabled": True, "priority": 2},
}


def _provider_config_to_dict(config: Any) -> dict[str, Any]:
    """Convert a ProviderConfig to a snake_case dict, omitting None fields."""
    result: dict[str, Any] = {"enabled": config.enabled if config.enabled is not None else True}
    if config.priority is not None:
        result["priority"] = config.priority
    if config.timeout is not None:
        result["timeout"] = config.timeout
    if config.on_failure is not None:
        result["on_failure"] = config.on_failure
    if config.api_key_env is not None:
        result["api_key_env"] = config.api_key_env
    if config.type is not None:
        result["type"] = config.type
    if config.command is not None:
        result["command"] = config.command
    if config.options is not None:
        result["options"] = config.options
    return result


def generate_server_config(config: SecureConfig) -> str:
    """Generate agentsh server config YAML from SecureConfig."""
    sc = config.server_config

    # Determine allow_degraded: from server_config or default True
    allow_degraded = True
    if sc is not None and sc.allow_degraded is not None:
        allow_degraded = sc.allow_degraded

    doc: dict[str, Any] = {
        "server": {"http": {"addr": "127.0.0.1:18080"}},
        "auth": {"type": "none"},
        "policies": {
            "system_dir": "/etc/agentsh/system",
            "dir": "/etc/agentsh",
            "default": config.policy_name,
        },
        "workspace": config.workspace,
        "sandbox": {
            "enabled": True,
            "allow_degraded": allow_degraded,
            "fuse": {"enabled": False},
            "network": {"enabled": True},
            # Seccomp NOTIFY disabled by default: many container environments
            # restrict the seccomp() syscall, causing failures on every exec.
            "seccomp": {"enabled": False},
            # Unix sockets wrapper disabled: the wrapper (agentsh-unixwrap)
            # installs seccomp-bpf filters that fail in container environments.
            # In v0.16.9+, leaving this enabled triggers file_monitor enforcement
            # which intercepts ALL file operations.
            "unix_sockets": {"enabled": False},
        },
    }

    # ─── Sessions (always present, with default base_dir) ─────────
    sessions_obj: dict[str, Any] = {
        "base_dir": "/var/lib/agentsh/sessions",
    }
    if config.real_paths is not None and config.real_paths:
        sessions_obj["real_paths"] = True
    if sc is not None and sc.sessions is not None:
        sess = sc.sessions
        if sess.base_dir is not None:
            sessions_obj["base_dir"] = sess.base_dir
        if sess.max_sessions is not None:
            sessions_obj["max_sessions"] = sess.max_sessions
        if sess.default_timeout is not None:
            sessions_obj["default_timeout"] = sess.default_timeout
        if sess.idle_timeout is not None:
            sessions_obj["idle_timeout"] = sess.idle_timeout
        if sess.cleanup_interval is not None:
            sessions_obj["cleanup_interval"] = sess.cleanup_interval
    doc["sessions"] = sessions_obj

    # ─── real_paths on sandbox (legacy position) ──────────────────
    if config.real_paths is not None:
        doc["sandbox"]["real_paths"] = config.real_paths

    # ─── Extended config sections from server_config ──────────────
    if sc is not None:
        # gRPC
        if sc.grpc is not None:
            doc["server"]["grpc"] = {"enabled": True, "addr": sc.grpc.addr}

        # Server timeouts → merge into server.http
        if sc.server_timeouts is not None:
            http = doc["server"]["http"]
            if sc.server_timeouts.read_timeout is not None:
                http["read_timeout"] = sc.server_timeouts.read_timeout
            if sc.server_timeouts.write_timeout is not None:
                http["write_timeout"] = sc.server_timeouts.write_timeout
            if sc.server_timeouts.max_request_size is not None:
                http["max_request_size"] = sc.server_timeouts.max_request_size

        # Logging
        if sc.logging is not None:
            log_obj: dict[str, Any] = {}
            if sc.logging.level is not None:
                log_obj["level"] = sc.logging.level
            if sc.logging.format is not None:
                log_obj["format"] = sc.logging.format
            if sc.logging.output is not None:
                log_obj["output"] = sc.logging.output
            doc["logging"] = log_obj

        # Audit — v0.16.9: sqlite_path nests under storage
        if sc.audit is not None:
            audit_obj: dict[str, Any] = {}
            if sc.audit.enabled is not None:
                audit_obj["enabled"] = sc.audit.enabled
            if sc.audit.sqlite_path is not None:
                storage_obj: dict[str, Any] = {"sqlite_path": sc.audit.sqlite_path}
                if sc.audit.batch_size is not None:
                    storage_obj["batch_size"] = sc.audit.batch_size
                if sc.audit.flush_interval is not None:
                    storage_obj["flush_interval"] = sc.audit.flush_interval
                if sc.audit.channel_size is not None:
                    storage_obj["channel_size"] = sc.audit.channel_size
                audit_obj["storage"] = storage_obj
            doc["audit"] = audit_obj

        # Sandbox limits
        if sc.sandbox_limits is not None:
            limits_obj: dict[str, Any] = {}
            if sc.sandbox_limits.max_memory_mb is not None:
                limits_obj["max_memory_mb"] = sc.sandbox_limits.max_memory_mb
            if sc.sandbox_limits.max_cpu_percent is not None:
                limits_obj["max_cpu_percent"] = sc.sandbox_limits.max_cpu_percent
            if sc.sandbox_limits.max_processes is not None:
                limits_obj["max_processes"] = sc.sandbox_limits.max_processes
            doc["sandbox"]["limits"] = limits_obj

        # FUSE deferred
        if sc.fuse is not None:
            fuse_obj = doc["sandbox"]["fuse"]
            if sc.fuse.deferred is not None:
                fuse_obj["deferred"] = sc.fuse.deferred
            if sc.fuse.deferred_marker_file is not None:
                fuse_obj["deferred_marker_file"] = sc.fuse.deferred_marker_file
            if sc.fuse.deferred_enable_command is not None:
                fuse_obj["deferred_enable_command"] = sc.fuse.deferred_enable_command

        # Network intercept
        if sc.network_intercept is not None:
            net = doc["sandbox"]["network"]
            if sc.network_intercept.intercept_mode is not None:
                net["intercept_mode"] = sc.network_intercept.intercept_mode
            if sc.network_intercept.proxy_listen_addr is not None:
                net["proxy_listen_addr"] = sc.network_intercept.proxy_listen_addr

        # Seccomp details — providing seccomp_details implicitly enables seccomp
        # (unless ptrace is also enabled, since they're mutually exclusive)
        if sc.seccomp_details is not None:
            sec = doc["sandbox"]["seccomp"]
            if not (sc.ptrace is not None and sc.ptrace.enabled):
                sec["enabled"] = True
            if sc.seccomp_details.execve is not None:
                sec["execve"] = sc.seccomp_details.execve
            if sc.seccomp_details.file_monitor is not None:
                fm_obj: dict[str, Any] = {}
                if sc.seccomp_details.file_monitor.enabled is not None:
                    fm_obj["enabled"] = sc.seccomp_details.file_monitor.enabled
                if sc.seccomp_details.file_monitor.enforce_without_fuse is not None:
                    fm_obj["enforce_without_fuse"] = sc.seccomp_details.file_monitor.enforce_without_fuse
                if sc.seccomp_details.file_monitor.intercept_metadata is not None:
                    fm_obj["intercept_metadata"] = sc.seccomp_details.file_monitor.intercept_metadata
                if sc.seccomp_details.file_monitor.openat_emulation is not None:
                    fm_obj["openat_emulation"] = sc.seccomp_details.file_monitor.openat_emulation
                if sc.seccomp_details.file_monitor.block_io_uring is not None:
                    fm_obj["block_io_uring"] = sc.seccomp_details.file_monitor.block_io_uring
                sec["file_monitor"] = fm_obj

        # Cgroups
        if sc.cgroups is not None:
            doc["sandbox"]["cgroups"] = {"enabled": sc.cgroups.enabled}

        # Unix sockets
        if sc.unix_sockets is not None:
            doc["sandbox"]["unix_sockets"] = {"enabled": sc.unix_sockets.enabled}

        # Ptrace
        if sc.ptrace is not None:
            ptrace_obj: dict[str, Any] = {}
            if sc.ptrace.enabled is not None:
                ptrace_obj["enabled"] = sc.ptrace.enabled
            if sc.ptrace.attach_mode is not None:
                ptrace_obj["attach_mode"] = sc.ptrace.attach_mode
            if sc.ptrace.mask_tracer_pid is not None:
                ptrace_obj["mask_tracer_pid"] = sc.ptrace.mask_tracer_pid
            if sc.ptrace.trace is not None:
                trace_obj: dict[str, Any] = {}
                if sc.ptrace.trace.execve is not None:
                    trace_obj["execve"] = sc.ptrace.trace.execve
                if sc.ptrace.trace.file is not None:
                    trace_obj["file"] = sc.ptrace.trace.file
                if sc.ptrace.trace.network is not None:
                    trace_obj["network"] = sc.ptrace.trace.network
                if sc.ptrace.trace.signal is not None:
                    trace_obj["signal"] = sc.ptrace.trace.signal
                ptrace_obj["trace"] = trace_obj
            if sc.ptrace.performance is not None:
                perf_obj: dict[str, Any] = {}
                if sc.ptrace.performance.seccomp_prefilter is not None:
                    perf_obj["seccomp_prefilter"] = sc.ptrace.performance.seccomp_prefilter
                if sc.ptrace.performance.max_tracees is not None:
                    perf_obj["max_tracees"] = sc.ptrace.performance.max_tracees
                if sc.ptrace.performance.max_hold_ms is not None:
                    perf_obj["max_hold_ms"] = sc.ptrace.performance.max_hold_ms
                if sc.ptrace.performance.static_allow_file is not None:
                    perf_obj["static_allow_file"] = sc.ptrace.performance.static_allow_file
                if sc.ptrace.performance.static_allow_network is not None:
                    perf_obj["static_allow_network"] = sc.ptrace.performance.static_allow_network
                if sc.ptrace.performance.arg_level_filter is not None:
                    perf_obj["arg_level_filter"] = sc.ptrace.performance.arg_level_filter
                ptrace_obj["performance"] = perf_obj
            if sc.ptrace.on_attach_failure is not None:
                ptrace_obj["on_attach_failure"] = sc.ptrace.on_attach_failure
            doc["sandbox"]["ptrace"] = ptrace_obj

        # Environment injection
        if sc.env_inject is not None:
            doc["sandbox"]["env_inject"] = {**sc.env_inject}

        # Proxy
        if sc.proxy is not None:
            proxy_obj: dict[str, Any] = {}
            if sc.proxy.mode is not None:
                proxy_obj["mode"] = sc.proxy.mode
            if sc.proxy.port is not None:
                proxy_obj["port"] = sc.proxy.port
            if sc.proxy.providers is not None:
                proxy_obj["providers"] = sc.proxy.providers
            doc["proxy"] = proxy_obj

        # DLP
        if sc.dlp is not None:
            dlp_obj: dict[str, Any] = {}
            if sc.dlp.mode is not None:
                dlp_obj["mode"] = sc.dlp.mode
            if sc.dlp.patterns is not None:
                dlp_obj["patterns"] = sc.dlp.patterns
            if sc.dlp.custom_patterns is not None:
                dlp_obj["custom_patterns"] = [
                    {"name": p.name, "display": p.display, "regex": p.regex}
                    for p in sc.dlp.custom_patterns
                ]
            doc["dlp"] = dlp_obj

        # Policies override — replaces entire policies key
        if sc.policies_override is not None:
            policies_obj: dict[str, Any] = {}
            if sc.policies_override.dir is not None:
                policies_obj["dir"] = sc.policies_override.dir
            if sc.policies_override.default_policy is not None:
                policies_obj["default"] = sc.policies_override.default_policy
            doc["policies"] = policies_obj

        # Policy signing (v0.16.9+)
        if sc.policy_signing is not None:
            policies = doc.get("policies", {})
            signing_obj: dict[str, Any] = {}
            if sc.policy_signing.trust_store is not None:
                signing_obj["trust_store"] = sc.policy_signing.trust_store
            if sc.policy_signing.mode is not None:
                signing_obj["mode"] = sc.policy_signing.mode
            policies["signing"] = signing_obj
            doc["policies"] = policies

        # Approvals
        if sc.approvals is not None:
            approvals_obj: dict[str, Any] = {}
            if sc.approvals.enabled is not None:
                approvals_obj["enabled"] = sc.approvals.enabled
            if sc.approvals.mode is not None:
                approvals_obj["mode"] = sc.approvals.mode
            if sc.approvals.timeout is not None:
                approvals_obj["timeout"] = sc.approvals.timeout
            doc["approvals"] = approvals_obj

        # Metrics
        if sc.metrics is not None:
            metrics_obj: dict[str, Any] = {}
            if sc.metrics.enabled is not None:
                metrics_obj["enabled"] = sc.metrics.enabled
            if sc.metrics.path is not None:
                metrics_obj["path"] = sc.metrics.path
            doc["metrics"] = metrics_obj

        # Health
        if sc.health is not None:
            health_obj: dict[str, Any] = {}
            if sc.health.path is not None:
                health_obj["path"] = sc.health.path
            if sc.health.readiness_path is not None:
                health_obj["readiness_path"] = sc.health.readiness_path
            doc["health"] = health_obj

        # Development
        if sc.development is not None:
            dev_obj: dict[str, Any] = {}
            if sc.development.disable_auth is not None:
                dev_obj["disable_auth"] = sc.development.disable_auth
            if sc.development.verbose_errors is not None:
                dev_obj["verbose_errors"] = sc.development.verbose_errors
            doc["development"] = dev_obj

    # ─── Threat feeds ─────────────────────────────────────────────
    if config.threat_feeds is not False:
        tf = config.threat_feeds
        if tf is None:
            doc["threat_feeds"] = {
                "enabled": True,
                "action": "deny",
                "feeds": _DEFAULT_FEEDS,
                "allowlist": _DEFAULT_ALLOWLIST,
            }
        else:
            doc["threat_feeds"] = {
                "enabled": True,
                "action": tf.action,
                "feeds": [
                    {
                        "name": f.name,
                        "url": f.url,
                        "format": f.format,
                        "refresh_interval": f.refresh_interval,
                    }
                    for f in tf.feeds
                ],
            }
            if tf.allowlist is not None:
                doc["threat_feeds"]["allowlist"] = tf.allowlist

    # ─── Watchtower ───────────────────────────────────────────────
    if config.watchtower:
        doc["watchtower"] = {"url": config.watchtower}

    # ─── Package checks ──────────────────────────────────────────
    if config.package_checks is not False and config.package_checks is not None:
        pc = config.package_checks
        providers: dict[str, dict[str, Any]] = {}

        # Start with defaults
        for name, defaults in _DEFAULT_PACKAGE_CHECK_PROVIDERS.items():
            providers[name] = {**defaults}

        # Merge user-provided providers
        if pc.providers is not None:
            for name, value in pc.providers.items():
                if value is False:
                    providers[name] = {"enabled": False}
                elif value is True:
                    providers[name] = {**providers.get(name, {}), "enabled": True}
                else:
                    # ProviderConfig object — merge with existing default if present
                    base = providers.get(name, {})
                    providers[name] = {**base, **_provider_config_to_dict(value)}

        doc["package_checks"] = {
            "enabled": True,
            "scope": pc.scope if pc.scope is not None else "new_packages_only",
            "providers": providers,
        }

    return yaml.dump(doc, default_flow_style=False, width=1000)
