import json
import logging
import os
from asyncio import CancelledError
from packaging.version import Version
from pathlib import Path

from defence360agent.utils import (
    BACKUP_EXTENSION,
    CheckRunError,
    atomic_rewrite,
)
from im360.files import MODSEC, Index
from defence360agent.subsys import web_server
from im360.subsys.panels.base import APACHE
from im360.subsys.panels.generic.panel import GenericPanel

from .modsec_app_version_detector import map_components_versions_to_tags
from .panels.hosting_panel import HostingPanel
from .panels.coraza_modsecurity import CorazaModSecurity
from defence360agent.subsys.web_server import (
    safe_update_config,
    graceful_restart,
)
from im360.utils import is_apache2nginx_enabled
from .int_config import is_force_use_coraza

logger = logging.getLogger(__name__)

COMPONENTS_VERSION_DB = (
    "/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3"
)
MAPPING_FILE = "tags_matching.json"


class NotSupportedWebserverError(Exception):
    pass


async def is_webserver_supported() -> bool:
    """Apache >= 2.4 is supported and security2_module installed.

    - litespeed is not supported
    - nginx is not supported

    Apache is expected to be running, otherwise False is returned
    In case of any error, False is returned also
    """
    hp = HostingPanel()
    # Not supported with Coraza
    if is_force_use_coraza() or is_apache2nginx_enabled():
        return True
    webserver = await hp.get_web_server()
    if isinstance(hp, GenericPanel):
        # on Generic panel we expect that apache version >= 2.4 will be used
        # no check apache running and version explicitly
        return webserver == APACHE

    if webserver != APACHE:
        return False
    try:
        version = await web_server.apache_version()
        modules = await web_server.apache_modules()
    except CancelledError:
        raise
    except Exception as exc:
        logger.error("Error occurs while getting Apache version: %s", exc)
        return False
    return version >= Version("2.4") and b"security2_module" in modules


async def update_waf_rules_config():
    """
    Update modsec config file with enabled tags for specific directories.
    """
    if not await is_webserver_supported():
        raise NotSupportedWebserverError(
            "WAF rules configurator supports only apache webserver with "
            "version >= 2.4 or apache2nginx and ModSecurity 2"
        )

    panel = HostingPanel()
    config_path = Path(panel.get_app_specific_waf_config())

    # to make sure there is no change in apache2nginx state file
    # during the processing of waf rules config we need to ensure to
    # trigger it only once and then proceed further based on instance of panel
    is_coraza = isinstance(panel, CorazaModSecurity)
    new_config = _rules_config(is_coraza)

    is_uptodate = (
        config_path.exists() and config_path.read_text() == new_config
    )

    if not is_uptodate:
        if is_coraza:
            # we do not need to restart wafd since wafd has config watch
            # implemented, and it reloads if file changes
            if atomic_rewrite(config_path, new_config, backup=False):
                logger.info(
                    "App specific rules config was successfully updated"
                )
        else:
            if await safe_update_config(config_path, new_config):
                logger.info("WAF Rules Set Config was successfully updated")
    else:
        logger.info("WAF Rules Set Config is already up to date")


def _rules_config(is_coraza: bool):
    mapping_path = os.path.join(Index.files_path(MODSEC), MAPPING_FILE)
    with open(mapping_path, encoding="utf-8") as f:
        tags = json.load(f)
    return map_components_versions_to_tags(
        COMPONENTS_VERSION_DB, tags, is_coraza
    )


async def try_restore_config_from_backup():
    """
        In case if Agent starts and config backup is present — then
    it is required to restore it: original .conf files must be replaced by
    the backup file and then WS restart command must be applied.
    In this case we assume that backed up configuration file is correct
    and none config checks are performed.
    """

    try:
        config_path = HostingPanel().get_app_specific_waf_config()
    except NotImplementedError:
        return

    if os.path.isfile(config_path + BACKUP_EXTENSION):
        os.rename(config_path + BACKUP_EXTENSION, config_path)

        try:
            await graceful_restart()
        except CheckRunError:
            logger.exception(
                "Web server failed to start with a backed up config"
            )
