import html
import io
from typing import Optional, Set, Dict, List, Tuple
from dataclasses import dataclass, field

from tqdm import tqdm
from dataclasses_json import dataclass_json
import json
import os
import pathlib
import shlex
from loguru import logger
from enum import Enum

from flask import Blueprint, render_template, request, redirect, url_for, current_app, send_file
from werkzeug.utils import secure_filename

import config
from helpers.time_helper import current_timestamp
from helpers.md_and_html_convertor import MdAndHTMLConvertorLowLevel, MdAndHTMLConvertor
import helpers.table_of_services
from helpers.file_utils import relative_path, extract_fid_from_filename, json_safe_load, generate_random_folder_name, \
    persistent_audit_scopes_file_path, folder_in_upload_path
from template_manager import PwndocTemplateManager
from helpers.flask_multifile_upload import save_multiple_uploaded_files
import template_pwndoc
from scan2report import scan2report
from api_templates import download_templates_from_pwndoc_to_scan2report
from pwndoc_api import login, get_findings_from_audit, upsert_raw_finding, get_audits
from template_scan2report import TemplateScan2Report, str_bool_to_bool
from template_pwndoc import get_fid_from_raw_finding
from template_converters import Scan2ReportConvert
import template_converters
from helpers.custom_logging import FlashLog
from helpers.rq_helper import REDIS_QUEUE, rq_job_finalized, rq_job_failed
import redis  # dependency via rq


INFO_FILE = "_pwndoc_importer_processing.json"


class OutputFormat(Enum):
    PWNDOC = "pwndoc"
    JSON = "json"
    CSV = "csv"
    DOCX = "docx"


class ProcessingSteps(Enum):
    UPLOAD_VULN_SCANS = "Upload scan results from browser to server"
    PREPARATIONS_FOR_SCAN2REPORT = "Download templates from PwnDoc"
    SCAN2REPORT_FIRST_PASS = "Process scan findings using Scan2Report"
    UPLOAD_OF_NEW_TEMPLATES = "Upload newly found templates to PwnDoc"
    SCAN2REPORT_SECOND_PASS = "Rerun Scan2Report with new templates (if any)"
    PWNDOC_FINDINGS_UPLOAD = "Upload findings to PwnDoc audit"


class ProcessingStatus(Enum):
    WAITING = '⏳',
    RUNNING = '🚀',
    DONE = '✔',
    FAILED = '❌',
    SKIPPED = '⏩'


SEVERITIES = {  # todo: synchronize this with scan2report
    '0': 'Informative',
    '1': 'Low',
    '2': 'Medium',
    '3': 'High',
    '4': 'Critical'
}


# TODO: There is race condition. When two people are adding findings - the scan2report template synchronization might break the other scan2report run.

bp = Blueprint('api_process_findings', __name__, template_folder='templates')


def _get_audits() -> Dict[str, str]:
    audits_dict = {
        "Export to JSON": OutputFormat.JSON.value,
        "Export to CSV": OutputFormat.CSV.value,
        "Export to DOCX": OutputFormat.DOCX.value,
    }
    for audit_id, audit_name in get_audits():
        nice_name = f'{audit_name}  (id: {audit_id})'
        audits_dict[nice_name] = audit_id

    return audits_dict


def _list_profiles() -> List[str]:
    with open(relative_path("scan2report/plugins/profiles.json"), encoding="utf8") as f:
        data: dict = json.load(f)
    return list(data.keys())


@bp.route("/upload_scanner_result/<audit_id>", methods=["GET"])
@bp.route("/upload_scanner_result", methods=["GET"])
def upload_scanner_result_get(audit_id: str = ""):  # todo: maybe add additional arguments for setting persistence across error
    audits_dict = _get_audits()
    if len(audit_id) and audit_id not in list(audits_dict.values()):
        FlashLog.warning("Audit ID from URL doesn't exist.")
        audit_id = OutputFormat.DOCX.value

    languages = list(filter(lambda x: x != "og", config.LOCALES))
    profiles = _list_profiles()

    return render_template('upload_scanner_result_get.html', audit_dict=audits_dict, audit_id=audit_id, lang_list=languages, severities=SEVERITIES, profiles=profiles, allowed_scanner_extensions=config.ALLOWED_SCANNER_EXTENSIONS)


def _go_to_start(audit_id: str = ""):
    return redirect(url_for('api_process_findings.upload_scanner_result_get', audit_id=audit_id))


def _go_to_meta(folder_name: str):
    return redirect(url_for("api_process_findings.show_meta_output", folder_name=folder_name))


def _flash_msg_and_go_to_start(msg: str, level: str = "warning", audit_id: str = ""):
    FlashLog.log_and_flash_with_level(msg, level, depth=2)
    # TODO: ps.user_msgs
    return _go_to_start(audit_id)


@dataclass_json
@dataclass
class ProcessingSettings:
    audit_id: str = OutputFormat.DOCX.value
    upload_to_pwndoc: bool = False
    scan2report_args: str = ''
    __folder_name: str = field(default_factory=lambda: generate_random_folder_name("findings_import"))
    stored_files: Set[str] = field(default_factory=set)
    locale: str = 'cs'

    steps_progress: Dict[str, Tuple[str, int, int]] = field(default_factory=lambda:
        {e.name: (ProcessingStatus.WAITING.name, 0, 0) for e in ProcessingSteps}
    )

    user_msgs: List[Tuple[str, str]] = field(default_factory=list)  # level, msg
    finished: bool = False

    missing_templates: List[str] = field(default_factory=list)  # todo: missing templates might not include grouped findings which don't have their own fid in the resulting findings.json
    output_unspecified: dict = field(default_factory=dict)

    @staticmethod
    def __get_folder_path(folder_name: str) -> str:
        assert folder_name == secure_filename(folder_name)
        return folder_in_upload_path(folder_name)

    def get_folder_path(self) -> str:
        return self.__get_folder_path(self.__folder_name)

    def get_folder_name(self) -> str:
        return self.__folder_name

    @classmethod
    def __get_info_file_path(cls, folder_name: str) -> str:
        return os.path.join(cls.__get_folder_path(folder_name), INFO_FILE)

    @classmethod
    def load_from_folder_name(cls, folder_name: str) -> Optional['ProcessingSettings']:
        filepath = cls.__get_info_file_path(folder_name)
        if not pathlib.Path(filepath).is_file():
            return None

        with open(filepath, encoding="utf-8") as f:
            file_data = json.load(f)
            return ProcessingSettings.from_dict(file_data)

    def save(self):
        pathlib.Path(self.get_folder_path()).mkdir(exist_ok=True)
        with open(self.__get_info_file_path(self.__folder_name), "w", encoding="utf-8") as f:
            json.dump(self.to_dict(), f, indent=4)

    def cleanup_uploaded_files(self):
        files = [os.path.join(self.get_folder_path(), f) for f in self.stored_files]

        for file in files:
            if pathlib.Path(file).is_file():
                os.remove(file)

    def get_result_path(self) -> str:
        if self.audit_id == OutputFormat.DOCX.value:
            return self._get_result_docx_path()
        if self.audit_id == OutputFormat.CSV.value:
            return self._get_result_csv_path()
        return self._get_result_json_path()

    def _get_result_docx_path(self):
        return os.path.join(self.get_folder_path(), 'scan2report_output.docx')

    def _get_result_json_path(self):
        return os.path.join(self.get_folder_path(), 'scan2report_output.json')

    def _get_result_csv_path(self):
        return os.path.join(self.get_folder_path(), 'scan2report_output.csv')

    def _meta_file_path(self) -> str:
        return f"{self.get_result_path()}.meta.json"

    def load_meta_file(self) -> dict:
        meta_data = json_safe_load(self._meta_file_path())
        return meta_data if meta_data else {}

    def set_step_status(self, step: ProcessingSteps, new_status: ProcessingStatus):
        # todo: make sure all calls to this function happen even when exceptions occur
        step_name = step.name
        old_status_name, start_timestamp, end_timestamp = self.steps_progress[step_name]

        if old_status_name == ProcessingStatus.WAITING.name:
            start_timestamp = current_timestamp()
        if new_status in [ProcessingStatus.DONE, ProcessingStatus.FAILED, ProcessingStatus.SKIPPED]:
            end_timestamp = current_timestamp()
        if new_status == ProcessingStatus.FAILED:
            self.finished = True

        self.steps_progress[step_name] = (new_status.name, start_timestamp, end_timestamp)
        self.save()

    def get_statuses_for_display(self) -> List[Tuple[str, str, str]]:
        """Step display text | Step status text | Step duration in minutes and seconds"""

        answer: List[Tuple[str, str, str]] = []

        for step_enum in ProcessingSteps:
            step_info = self.steps_progress.get(step_enum.name, None)
            if step_info is None:
                continue
            old_status_name, start_timestamp, end_timestamp = step_info

            if end_timestamp >= start_timestamp:
                duration = end_timestamp - start_timestamp
            elif start_timestamp != 0:
                duration = current_timestamp() - start_timestamp
            else:
                duration = 0

            step_text = step_enum.value

            step_status = ProcessingStatus[step_info[0]]
            step_status_text = step_status.value[0] if isinstance(step_status.value, tuple) else step_status.value  # todo: inspect why python sees some emojis as tuples

            step_duration_text = ""
            if step_status not in [ProcessingStatus.WAITING, ProcessingStatus.SKIPPED]:
                step_duration_text = f"{str(duration//60).zfill(2)}:{str(duration%60).zfill(2)}"

            answer.append((step_text, step_status_text, step_duration_text))

        return answer

    def rq_job_name(self):
        return self.get_folder_name()

    def rq_is_still_in_enqueued(self) -> bool:
        return rq_job_finalized(self.rq_job_name()) is False

    def rq_job_failed(self) -> bool:
        return rq_job_failed(self.rq_job_name())


@bp.route("/upload_scanner_result", methods=["POST"])
def upload_scanner_result_post():
    audit_id = request.form.get("audit")
    locale = request.form.get("locale")
    profile = request.form.get("profile")
    min_severity = request.form.get("min-severity")

    def _flash_error_msg(msg: str, level: str = "warning"):
        return _flash_msg_and_go_to_start(msg, level, audit_id)  # note: audit_id is taken from outer scope

    if len(audit_id) and audit_id not in list(_get_audits().values()):
        return _flash_error_msg("This audit ID doesn't exist.")

    if locale not in config.LOCALES:
        return _flash_error_msg("This locale does not exist.")

    if profile not in _list_profiles():
        return _flash_error_msg("This profile does not exist.")

    if min_severity not in SEVERITIES.keys():
        return _flash_error_msg("This severity doesn't exist")

    scan2report_args = request.form.get("scan2report_args")
    scan2report_args += f' --min-severity {min_severity} --profile {profile}'  # todo: make sure that I'm not overwriting explicitly set parameters

    audit_info = list(filter(lambda x: x[0] == audit_id, get_audits()))
    upload_to_pwndoc = len(audit_info) > 0

    ps = ProcessingSettings(
        audit_id=audit_id,
        scan2report_args=scan2report_args,
        locale=locale,
        upload_to_pwndoc=upload_to_pwndoc,
    )
    ps.set_step_status(ProcessingSteps.UPLOAD_VULN_SCANS, ProcessingStatus.RUNNING)
    if not upload_to_pwndoc:
        ps.set_step_status(ProcessingSteps.PWNDOC_FINDINGS_UPLOAD, ProcessingStatus.SKIPPED)

    error_msg, saved_filenames = save_multiple_uploaded_files(output_folder=ps.get_folder_path(), reserved_filenames=set(INFO_FILE), allowed_extensions=config.ALLOWED_SCANNER_EXTENSIONS)
    ps.stored_files = ps.stored_files.union(saved_filenames)
    ps.save()

    if len(error_msg) > 0:
        ps.set_step_status(ProcessingSteps.UPLOAD_VULN_SCANS, ProcessingStatus.FAILED)
        ps.cleanup_uploaded_files()
        return _flash_msg_and_go_to_start(error_msg, "error", audit_id)

    ps.set_step_status(ProcessingSteps.UPLOAD_VULN_SCANS, ProcessingStatus.DONE)

    if config.RQ_DISABLED:
        full_processing_native(ps.get_folder_name())
    else:
        try:
            REDIS_QUEUE.enqueue(
                full_processing_background_wrapper, args=(ps.get_folder_name(),),
                job_timeout=config.RQ_JOB_TIMEOUT, ttl='1d', result_ttl=7*24*60*60, job_id=ps.rq_job_name()
            )
        except redis.exceptions.ConnectionError as e:
            FlashLog.log_and_flash_with_level(f"Failed to connect to Redis - is the container running? ({e})", "error")
    return _go_to_meta(ps.get_folder_name())


def _upload_missing_templates(ps: ProcessingSettings):
    PwndocTemplateManager.update_template_db_from_pwndoc()  # Update DB so that we can do automatic template merging.

    for single_missing_string in tqdm(ps.missing_templates, desc="Uploading missing templates"):
        filename, severity, title = single_missing_string.strip().split("\t")
        filename = secure_filename(filename)
        fid = extract_fid_from_filename(filename)
        path = os.path.join(TemplateScan2Report.native_folder_name_for_locale(ps.locale, original=True), filename)
        with open(path, encoding="utf8") as f:
            s2r_template_dict = json.load(f)

        og_template: TemplateScan2Report = TemplateScan2Report.parse_dict_custom(fid=fid, locale="og", template_dict=s2r_template_dict, original=True)
        if og_template is None:
            logger.info(f"Empty og template for fid {fid}?")
            continue
        og_template_tns = Scan2ReportConvert.convert_single_finding_to_tns_pwndoc(og_template, "og")

        # todo: fix this - I shouldn't need to hack it here
        og_template_tns.set_is_template(True)
        if og_template_tns.hosts == "":
            og_template_tns.hosts = []

        try:
            PwndocTemplateManager.add_single_locale_template(og_template_tns)
        except AssertionError as e:
            # todo: find the cause of this. The full error is: AssertionError: Creation of vulnerability failed: 404 {"status":"error","datas":"Vulnerability not found"}
            logger.error(f'Upload of template failed.', stack_info=True, exc_info=True)
            ps.user_msgs.append(('error', f"Upload of template with FID {fid} failed. This shouldn't happen, but to not totally block the import it was skipped."))
            ps.save()

    TemplateScan2Report.delete_template_files(["og"])
    PwndocTemplateManager.update_template_db_from_pwndoc()  # Update DB to have a current state.


def process_scanner_result_using_scan2report(folder_name: str) -> bool:
    ps: Optional[ProcessingSettings] = ProcessingSettings.load_from_folder_name(folder_name)
    if ps is None:
        logger.warning(f"Scan folder doesn't exist: {folder_name}")
        return False

    ps.set_step_status(ProcessingSteps.PREPARATIONS_FOR_SCAN2REPORT, ProcessingStatus.RUNNING)

    download_templates_from_pwndoc_to_scan2report(
        "scan2report_native" if ps.audit_id == OutputFormat.DOCX.value else "scan2report_with_pwndoc_formatting",
        locale=ps.locale
    )

    files = [os.path.join(ps.get_folder_path(), f) for f in ps.stored_files]
    args = shlex.split(ps.scan2report_args)

    if "--output" in args:
        ps.user_msgs.append(('error', "Don't specify --output as a parameter, use selector of audit ID instead."))
        ps.save()
        return False

    output_path = ps.get_result_path()
    combined_args = ['--output', output_path]

    if not ("-l" in args or "--lang" in args):
        combined_args += ['--lang', ps.locale]

    combined_args += args + files

    logger.debug(f"Running scan2report with: {combined_args}")

    # todo: the http connection might time out - do as async?
    count_of_known_templates_before = len(TemplateScan2Report.list_files_in_locale_folder(ps.locale, original=False))
    ps.set_step_status(ProcessingSteps.PREPARATIONS_FOR_SCAN2REPORT, ProcessingStatus.DONE)
    ps.set_step_status(ProcessingSteps.SCAN2REPORT_FIRST_PASS, ProcessingStatus.RUNNING)

    scan2report.main(combined_args, reinit_global_vars=True)  # First run to generates OG templates

    # Metadata from the first load - extract the missing templates.
    meta_data = ps.load_meta_file()
    ps.missing_templates = meta_data["missing"]  # _upload_missing_templates
    ps.output_unspecified["missing_n_templates"] = len(ps.missing_templates)
    ps.save()

    ps.set_step_status(ProcessingSteps.SCAN2REPORT_FIRST_PASS, ProcessingStatus.DONE)
    ps.set_step_status(ProcessingSteps.UPLOAD_OF_NEW_TEMPLATES, ProcessingStatus.RUNNING)

    _upload_missing_templates(ps)
    download_templates_from_pwndoc_to_scan2report(
        output_format="scan2report_native" if ps.audit_id == OutputFormat.DOCX.value else "scan2report_with_pwndoc_formatting",
        locale='og',
        force_locale_folder=ps.locale,
        prefix_title_with_locale=True
    )
    ps.set_step_status(ProcessingSteps.UPLOAD_OF_NEW_TEMPLATES, ProcessingStatus.DONE)

    count_of_known_templates_after = len(TemplateScan2Report.list_files_in_locale_folder(ps.locale, original=False))
    if count_of_known_templates_before != count_of_known_templates_after:
        ps.set_step_status(ProcessingSteps.SCAN2REPORT_SECOND_PASS, ProcessingStatus.RUNNING)
        scan2report.main(combined_args, reinit_global_vars=True)  # Second run to regenerate the findings if during the first run we found OG templates
        ps.set_step_status(ProcessingSteps.SCAN2REPORT_SECOND_PASS, ProcessingStatus.DONE)
    else:
        ps.set_step_status(ProcessingSteps.SCAN2REPORT_SECOND_PASS, ProcessingStatus.SKIPPED)

    ps.cleanup_uploaded_files()
    return True


def get_changed_after_combine_with_existing_findings(audit_id: str, possibly_new_findings: List[dict]) -> List[dict]:
    existing_raw_findings = get_findings_from_audit(audit_id)

    fid_mapping = {}
    for single_raw_finding in existing_raw_findings:
        fid = get_fid_from_raw_finding(single_raw_finding)
        if fid in fid_mapping:
            FlashLog.warning(f"Multiple existing findings with {fid} in single audit. Overwriting.")
        fid_mapping[fid] = single_raw_finding
        single_raw_finding["_changed"] = False

    for single_raw_finding in possibly_new_findings:
        fid = get_fid_from_raw_finding(single_raw_finding)

        # Interesting tests for the HTML parsing
        # if fid not in ["burp_5243008", "HTTP_generic_XSS"]:
        #     continue

        for attr_name in template_converters.HTML_ATTRIBUTES:
            single_raw_finding[attr_name] = MdAndHTMLConvertor.html_to_pwndoc_html(single_raw_finding.get(attr_name, ""))

        if fid not in fid_mapping:
            fid_mapping[fid] = single_raw_finding
            single_raw_finding["_changed"] = True
            continue

        original_scope = fid_mapping[fid].get("scope", "").split("<br>")
        new_scope = single_raw_finding.get("scope", "").split("<br>")

        merged_scope = original_scope + new_scope
        merged_scope_deduplicated = list(dict.fromkeys(merged_scope))
        fid_mapping[fid]["scope"] = "<br>".join(merged_scope_deduplicated)

        if len(merged_scope_deduplicated) != len(new_scope):
            fid_mapping[fid]["poc"] = fid_mapping[fid].get("poc", "") + "<br><br>=====<br><br>" + single_raw_finding.get("poc", "")
            single_raw_finding["_changed"] = True

    new_or_changed = list(filter(lambda x: x.get("_changed", True), fid_mapping.values()))
    logger.info(f"Import contained {len(list(fid_mapping.values()))} unique FIDs which resulted in upsert of {len(new_or_changed)} findings.")

    return new_or_changed


def _extend_audit_services(audit_id: str, new_services: dict) -> helpers.table_of_services.Scope:
    answer = helpers.table_of_services.get_tns_table_of_services(audit_id)

    persistent_scopes_file_path = persistent_audit_scopes_file_path(audit_id)
    local_services_copy = json_safe_load(persistent_scopes_file_path)
    if local_services_copy is not None:
        for host in local_services_copy.get('scope', {}).get('hosts', []):
            for service in host.get('services', []):
                answer.add_entry(host['ip'], service['port'], service['protocol'], service['name'])

    for host_ip, port_list in new_services.items():
        for port_number, l3_protocol_list in port_list.items():
            for l3_protocol, app_protocol in l3_protocol_list.items():
                answer.add_entry(host_ip, port_number, l3_protocol, app_protocol)
    return answer


@bp.route("/force_upload_services/<audit_id>", methods=["GET"])
def force_upload_services(audit_id: str):
    services_as_pwndoc_scope = _extend_audit_services(audit_id, {})
    helpers.table_of_services.set_tns_table_of_services(audit_id, services_as_pwndoc_scope)
    return f"OK; {len(services_as_pwndoc_scope.hosts)} hosts are in the table of services"


def upload_scan_findings_to_pwndoc(folder_name: str) -> bool:
    ps: Optional[ProcessingSettings] = ProcessingSettings.load_from_folder_name(folder_name)
    if ps is None:
        logger.warning(f"Scan folder doesn't exist: {folder_name}")
        return False

    ps.set_step_status(ProcessingSteps.PWNDOC_FINDINGS_UPLOAD, ProcessingStatus.RUNNING)

    path = ps.get_result_path()
    with open(path, encoding="utf8") as f:
        file_data = json.load(f)

    findings: List[TemplateScan2Report] = []
    locale = ps.locale

    for single_finding in file_data:
        finding_parsed = TemplateScan2Report.parse_dict_custom(single_finding["fid"], locale, single_finding)
        if finding_parsed is None:
            logger.warning(f"Empty finding in audit report?")
        else:
            findings.append(finding_parsed)

    login()
    new_raw_findings = []
    for single_finding in tqdm(findings, desc="Converting findings to PwnDoc format"):
        single_finding_pwndoc = Scan2ReportConvert.convert_single_finding_to_tns_pwndoc(single_finding, locale)
        single_finding_pwndoc.set_custom_field(
            "original_description_from_scan",
            MdAndHTMLConvertor.custom_md_to_pwndoc_html(single_finding.description)
        )

        single_finding_pwndoc_dict = single_finding_pwndoc.to_pwndoc_dict()
        new_raw_findings.append(single_finding_pwndoc_dict)

    changed_raw_findings = get_changed_after_combine_with_existing_findings(ps.audit_id, new_raw_findings)
    for single_raw_finding in tqdm(changed_raw_findings, desc="Upserting findings"):
        try:
            upsert_raw_finding(ps.audit_id, single_raw_finding)
        except Exception as e:
            fid = single_raw_finding.get('fid', 'NO_FID')
            error_msg = f"Upserting finding {fid} failed. Skipping it."

            PWNDOC_UPLOAD_LIMIT = 10 * 1024 * 1024
            len_of_content = len(json.dumps(single_raw_finding))
            if len_of_content >= PWNDOC_UPLOAD_LIMIT * 0.95:
                error_msg += f"""<br>\n
                    (The finding might be too large.
                    The limit is {PWNDOC_UPLOAD_LIMIT/1024/1024} MB while the request was {len_of_content/1024/1024} MB.)
                """
            logger.exception(error_msg)
            ps.user_msgs.append(('error', error_msg))
            ps.save()

    meta_data = ps.load_meta_file()
    services_as_pwndoc_scope = _extend_audit_services(ps.audit_id, meta_data.get("services", {}))
    helpers.table_of_services.set_tns_table_of_services(ps.audit_id, services_as_pwndoc_scope)

    ps.set_step_status(ProcessingSteps.PWNDOC_FINDINGS_UPLOAD, ProcessingStatus.DONE)

    ps.user_msgs.append(('info', f'OK, added or updated {len(changed_raw_findings)} findings - you can now find them in PwnDoc.'))
    ps.save()
    return True


@bp.route("/meta_output/<folder_name>", methods=["GET"])
def show_meta_output(folder_name: str):
    ps: Optional[ProcessingSettings] = ProcessingSettings.load_from_folder_name(folder_name)
    if ps is None:
        return "Folder doesn't exist", 400

    missing_templates_splitted = [x.split("\t") for x in ps.missing_templates]

    for level, msg in ps.user_msgs:
        FlashLog.flash_loguru_msg(msg, loguru_level=level)

    if config.RQ_DISABLED is False:
        if ps.finished is False and ps.rq_is_still_in_enqueued() is False:
            FlashLog.flash_loguru_msg(
                "⚠ The processing is not marked as finished, but the background job is no longer running. It might have failed silently.",
                "warning")

    return render_template("findings_meta_output.html",
                           ps=ps,
                           missing_templates=missing_templates_splitted,
                           steps=ps.get_statuses_for_display(),
                           # import_id=folder_name,
                           )


@bp.route("/file_output/<folder_name>", methods=["GET"])
def show_file_output(folder_name: str):
    ps: Optional[ProcessingSettings] = ProcessingSettings.load_from_folder_name(folder_name)
    if ps is None:
        return "Folder doesn't exist", 400
    if ps.get_result_path().endswith(".docx"):  # todo: don't match on extension
        return send_file(ps.get_result_path(), as_attachment=True)

    with open(ps.get_result_path(), "r", encoding="utf8") as f:
        text = f.read()
    text = html.escape(text, quote=False)
    text = MdAndHTMLConvertor.unescape_custom_html(text)

    filename = os.path.basename(ps.get_result_path())

    return send_file(io.BytesIO(text.encode("utf8")), as_attachment=True, download_name=filename)


@bp.route("/list_findings/<folder_name>", methods=["GET"])
def list_findings(folder_name: str):
    ps: Optional[ProcessingSettings] = ProcessingSettings.load_from_folder_name(folder_name)
    if ps is None:
        return "Folder doesn't exist", 400
    if not ps.get_result_path().endswith(".json"):  # todo: don't match on extension
        return "Only finding outputs to JSON can be visualized in Importer"

    with open(ps.get_result_path(), "r", encoding="utf8") as f:
        data = json.load(f)

    return render_template("list_findings.html", findings=data, folder_name=folder_name)


@bp.route("/show_finding/<folder_name>/<int:finding_index>", methods=["GET"])
def show_finding(folder_name: str, finding_index: int):
    ps: Optional[ProcessingSettings] = ProcessingSettings.load_from_folder_name(folder_name)
    if ps is None:
        return "Folder doesn't exist", 400
    if not ps.get_result_path().endswith(".json"):  # todo: don't match on extension
        return "Only finding outputs to JSON can be visualized in Importer"

    with open(ps.get_result_path(), "r", encoding="utf8") as f:
        data = json.load(f)

    outputSafely = str_bool_to_bool(request.args.get('outputSafely', "false"))
    finding = data[finding_index]

    for attr_name, attr_value in finding.items():
        if isinstance(attr_value, str):  # todo: check if attr name is HTML field
            finding[attr_name] = MdAndHTMLConvertor.unescape_custom_html(attr_value)

    return render_template("single_finding.html", finding=finding, outputSafely=outputSafely)


def full_processing_background_wrapper(folder_name: str):
    from app import create_app
    with create_app().app_context():
        full_processing_native(folder_name)


def full_processing_native(folder_name: str):
    if not process_scanner_result_using_scan2report(folder_name):
        return

    if ProcessingSettings.load_from_folder_name(folder_name).upload_to_pwndoc:
        if not upload_scan_findings_to_pwndoc(folder_name):
            return

    ps = ProcessingSettings.load_from_folder_name(folder_name)
    ps.finished = True

    for step_name, step_value in ps.steps_progress.items():
        if step_value[0] == ProcessingStatus.WAITING.name:
            ps.set_step_status(ProcessingSteps["step_name"], ProcessingStatus.SKIPPED)

    ps.save()
