import json
import logging
import os
import re
from collections import defaultdict
from typing import Any

from peewee import SqliteDatabase

RULES_CONF_PATTERN = "<IfModule security2_module>\n{}\n</IfModule>"
GENERAL_RULES = "/var/imunify360/files/modsec/v2/general_rules.json"

logger = logging.getLogger(__name__)


class DatabaseNotFoundError(Exception):
    pass


def map_components_versions_to_tags(
    components_sqlite_file: str, tags_mapping: dict[str, str], is_coraza: bool
) -> str:
    if not os.path.isfile(components_sqlite_file):
        raise DatabaseNotFoundError(
            "App detector database '{}' couldn't be found.".format(
                components_sqlite_file
            )
        )

    db = SqliteDatabase(components_sqlite_file)
    tags_regex = []
    for tag, reg in tags_mapping.items():
        tags_regex.append((tag, re.compile(reg)))
    cursor = db.execute_sql("select path, title from apps")
    path_tags = defaultdict(set)
    cache = dict()

    for path, title in cursor:
        tag = cache.get(title, None)
        if tag is not None:
            path_tags[path].add(tag)
        else:
            for tag, reg in tags_regex:
                if reg.match(title):
                    path_tags[path].add(tag)
                    cache[title] = tag
                    break

    return generate_conf(path_tags, is_coraza)


def generate_conf(path_tags: defaultdict[Any, set], is_coraza: bool) -> str:
    """
    Generate conf file with rules
    Use json.dumps for converting special symbols like \n and
    escape quoters inside quoters
    :param is_coraza:
    :param path_tags:
    :return:
    """
    config = []
    if is_coraza:
        if os.path.exists(GENERAL_RULES):
            with open(GENERAL_RULES, "r") as f:
                general_rules = json.load(f)
        else:
            general_rules = ["noshow", "service_im360"]
            logger.warning(
                "File %s does not exist. Using fallback", GENERAL_RULES
            )

        for path, tags in path_tags.items():
            config.append(
                {
                    "path": path,
                    "enabled_tags": sorted(list(tags) + general_rules),
                }
            )
        return json.dumps(
            sorted(config, key=lambda v: v["path"]), separators=(",", ":")
        )
    else:
        for path, tags in path_tags.items():
            config.append(
                """<Directory {}>
SecRuleRemoveByTag ^(?!(?:service.*|noshow|{})$)
</Directory>""".format(
                    json.dumps(path), "|".join(sorted(tags))
                )
            )
        return RULES_CONF_PATTERN.format("\n".join(sorted(config)))
