import json
import logging
import os

from defence360agent.subsys.persistent_state import load_state

logger = logging.getLogger(__name__)

ABSENT = "absent"
PRESENT = "present"
ENABLED = "enabled"

_STATES = (ABSENT, PRESENT, ENABLED)

WAF_SITES_STATE = "wp_waf_sites"
WP_PLUGIN_STATE = "ImunifySecurityPlugin"


def _kernelcare() -> dict:
    # Circular at module scope: kernel_care -> rpc_tools -> validate ->
    # backup_systems -> cln.
    from defence360agent.subsys.features.kernel_care import KernelCare

    try:
        os.stat(KernelCare.BIN_PATH)
    except FileNotFoundError:
        return {"state": ABSENT}
    # Any other OSError is a failed observation, not an absent binary: let it
    # reach collect() so the component is omitted instead of denied.
    return {"state": PRESENT}


def _state_value(name, key):
    # load_state returns whatever the file decoded to, so a state file holding
    # a JSON scalar or list would otherwise take the whole component down.
    state = load_state(name)
    return state.get(key) if isinstance(state, dict) else None


def _install_receipt() -> dict:
    # Advisory, so an unusable receipt omits the key rather than reporting a
    # confident false.
    installed = _state_value(WP_PLUGIN_STATE, "installed")
    return {} if installed is None else {"installed": bool(installed)}


def _normalize_waf_state(plugin_enabled, config_flag):
    if not plugin_enabled:
        return ABSENT
    return ENABLED if config_flag else PRESENT


def _waf_wordpress() -> dict:
    # Circular at module scope: wordpress -> incident_collector ->
    # rpc_tools -> validate -> backup_systems -> cln.
    from defence360agent.wordpress.plugin import (
        _get_global_waf_enabled,
        _get_security_plugin_enabled,
    )

    config_flag = bool(_get_global_waf_enabled())
    plugin = bool(_get_security_plugin_enabled())
    # Either toggle going off schedules removal of every rules.php, and the
    # count only refreshes while the plugin is on, so a frozen non-zero value
    # would read as coverage we no longer have.
    deployed = plugin and config_flag
    sites = int(_state_value(WAF_SITES_STATE, "sites") or 0) if deployed else 0
    return {
        "state": _normalize_waf_state(plugin, config_flag),
        "detail": {
            "config_flag": config_flag,
            "plugin": plugin,
            # Beside plugin, not folded in: the receipt reads false on
            # upgrades and mid-install where WAF is genuinely deployed.
            **_install_receipt(),
            "sites": sites,
        },
    }


PROBES = {
    "waf_wordpress": _waf_wordpress,
    "kernelcare": _kernelcare,
}


def _checked(value):
    if not isinstance(value, dict) or value.get("state") not in _STATES:
        raise ValueError(f"bad component state object: {value!r}")
    if not isinstance(value.get("detail", {}), dict):
        raise ValueError(f"bad component detail: {value!r}")
    # Round-tripping rejects the non-finite floats json.dumps would otherwise
    # encode as invalid JSON, and detaches the result so a later probe cannot
    # mutate it into something checkin can no longer serialise.
    return json.loads(json.dumps(value, allow_nan=False))


def collect() -> dict:
    components = {}
    # Snapshot: a probe that registered another one would otherwise break the
    # iteration, and that raises outside the per-probe guard below.
    for name, probe in list(PROBES.items()):
        try:
            components[name] = _checked(probe())
        except Exception:
            logger.exception("Component probe %r failed", name)
    return components
