from collections import defaultdict
import json
from typing import List, Optional, Dict, Tuple, DefaultDict, Set

from helpers.md_and_html_convertor import MdAndHTMLConvertor
from helpers.file_root import relative_path
import pwndoc_api
from helpers.db_models import DbTemplate, DbPwndocMapping
from pwndoc_api import pwndoc_download_json_of_vulnerabilities_and_custom_fields
from pwndoc_custom_fields import PwndocCustomFields
from template_pwndoc import MultilocaleTemplateWrapperPwndoc, TNSTemplatePwndoc

from loguru import logger


class PwndocTemplateManager:

    @classmethod
    def update_template_db_from_pwndoc(cls):
        # todo: Ideally this should be called only when PwnDoc Template changes, not when any importer action happens
        pwndoc_download_json_of_vulnerabilities_and_custom_fields()
        current_multilocale_templates_in_pwndoc: List[MultilocaleTemplateWrapperPwndoc] = cls._parse_pwndoc_multilang_templates()

        pwndoc_fids_and_locales: DefaultDict[str, Set[str]] = defaultdict(set)
        for single_fid_templates in current_multilocale_templates_in_pwndoc:
            for single_template in single_fid_templates.details:
                single_template: TNSTemplatePwndoc = single_template
                DbTemplate.add_template(single_template.fid, single_template.locale, single_template)

                pwndoc_fids_and_locales[single_template.fid].add(single_template.locale)

        all_db_templates: List[DbTemplate] = DbTemplate.get_all_current_templates()
        for single_template in all_db_templates:
            fid, locale = single_template.fid, single_template.locale
            if locale not in pwndoc_fids_and_locales.get(fid, {}):
                DbTemplate.delete_template(fid, locale)

    @staticmethod
    def _parse_pwndoc_multilang_templates() -> List[MultilocaleTemplateWrapperPwndoc]:
        with open(relative_path("api_examples/_api_vulnerabilities.json"), encoding="utf8") as f:
            # todo: The next section has a race condition (the easiest reproducer is running two requests simultaneously:
            #  /import_automator/templates/version/WEBAPP_Clickjacking/cs/0 )
            #  In future it would be better to save it to DB, that has transaction consistency.
            try:
                text = f.read()
                data = json.loads(text)
            except json.decoder.JSONDecodeError:
                # logger.exception("JSON decoding error")
                # logger.debug(text)
                raise

        pwndoc_templates: List[dict] = data["datas"]

        answer = []
        for x in pwndoc_templates:
            answer.append(PwndocTemplateManager.__parse_single_pwndoc_template(x))

        return answer

    @staticmethod
    def get_all_templates(fid: str = None) -> List[TNSTemplatePwndoc]:
        return [x.decode_content() for x in DbTemplate.get_all_current_templates(fid)]

    @classmethod
    def get_all_templates_as_dict_of_locales(cls, fid: str = None) -> Dict[str, Dict[str, TNSTemplatePwndoc]]:
        all_templates = cls.get_all_templates(fid)
        dict_of_fids_and_locales: Dict[str, Dict[str, TNSTemplatePwndoc]] = defaultdict(lambda: defaultdict(TNSTemplatePwndoc))
        for single_template in all_templates:
            dict_of_fids_and_locales[single_template.fid][single_template.locale] = single_template
        return dict_of_fids_and_locales

    @staticmethod
    def find_pwndoc_id_by_fid(fid: str) -> Optional[str]:
        return DbPwndocMapping.get_pwndoc_id_from_fid(fid)

    @staticmethod
    def __parse_single_pwndoc_template(template: dict) -> MultilocaleTemplateWrapperPwndoc:  # returns all lang mutations
        pwndoc_id = template.get("_id", None)
        multilang_template = MultilocaleTemplateWrapperPwndoc(_id=pwndoc_id)

        for lang_dict in template["details"]:
            tmp = PwndocTemplateManager.parse_single_pwndoc_locale(lang_dict)
            multilang_template.details.append(tmp)

        if multilang_template.get_fid():
            DbPwndocMapping.save_fid_and_pwndoc_id(multilang_template.get_fid(), pwndoc_id)

        return multilang_template

    @staticmethod
    def parse_single_pwndoc_locale(lang_dict: dict) -> TNSTemplatePwndoc:  # returns single lang mutation
        answer = {}

        for field in lang_dict:
            if field == "customFields":
                continue
            answer[field] = lang_dict[field]

        for custom_field in lang_dict["customFields"]:
            custom_field_id = custom_field["customField"]
            custom_field_label = PwndocCustomFields().get_custom_field_by_id(custom_field_id)["label"]

            answer[custom_field_label] = custom_field["text"]
            if isinstance(answer[custom_field_label], dict):
                assert len(answer[custom_field_label]) == 2, "Dict doesn't contain value and locale?"
                answer[custom_field_label] = answer[custom_field_label]['value']

        tmp = TNSTemplatePwndoc.from_dict(answer)
        tmp.set_is_template(True)

        return tmp

    @classmethod
    def add_locales_for_fid(cls, templates: List[TNSTemplatePwndoc]) -> bool:
        some_new = False
        fid = templates[0].fid

        for template in templates:
            template.set_is_template(True)
            template.remove_finding_specific_data()
            template.to_pwndoc_html()
            _, is_new = DbTemplate.add_template(template.fid, template.locale, template)
            some_new |= is_new

        if some_new:
            dict_of_current_templates = cls.get_all_templates_as_dict_of_locales(fid)
            multiloc_pwndoc = cls.prepare_multilocale_template_pwndoc(dict_of_current_templates[fid])
            pwndoc_api.add_templates_for_vulnerability(multiloc_pwndoc)

        return some_new

    @classmethod
    def prepare_multilocale_template_pwndoc(cls, lang_mutations_of_template: Dict[str, 'TNSTemplatePwndoc']) -> Optional[MultilocaleTemplateWrapperPwndoc]:
        NO_FID = "NO_FID"
        data = MultilocaleTemplateWrapperPwndoc()

        if len(lang_mutations_of_template) == 0:
            logger.warning(f"Skipping empty dictionary of templates.")
            return

        fid = NO_FID
        for locale, single_template in lang_mutations_of_template.items():
            fid = getattr(single_template, "fid", fid)
            single_template.set_is_template(True)

            og_template: Optional[TNSTemplatePwndoc] = lang_mutations_of_template.get("og")
            if og_template and len(single_template.description):
                single_template.set_custom_field("original_description_from_template", og_template.description)

            data.details.append(single_template)

        if fid != NO_FID:
            pwndoc_template_fid = DbPwndocMapping.get_pwndoc_id_from_fid(fid)
            if pwndoc_template_fid:
                data._id = pwndoc_template_fid

        return data

    @classmethod
    def add_single_locale_template(cls, template: TNSTemplatePwndoc) -> bool:
        return cls.add_locales_for_fid([template])

    @classmethod
    def reinit_importer_pwndoc_mapping(cls):
        DbPwndocMapping.delete_all()
        cls.update_template_db_from_pwndoc()
