import collections
import datetime
import os
from typing import List, Tuple, Optional
import time

from tqdm import tqdm
from loguru import logger

from flask import Blueprint, render_template, Response, send_file, url_for

from helpers.pwndoc_log_beautify import log_to_beautified_errors
from helpers.template_grouping import TemplateAliasing, TemplateGrouping
from helpers.custom_logging import FlashLog
from helpers.flask_response import basic_msg, basic_msg_html
import pwndoc_db_init
from helpers.file_utils import zip_multiple_folders_non_recursive_to_virtual, relative_path
from template_manager import PwndocTemplateManager
import template_pwndoc
from pwndoc_api import refresh_examples, delete_all_templates_in_pwndoc, load_audit_file, \
    get_findings_from_audit, download_and_return_finding, delete_finding, get_audits
from helpers.db_models import DbTemplate
from template_pwndoc import TNSTemplatePwndoc, get_fid_from_raw_finding
from template_scan2report import TemplateScan2Report
from template_converters import PwndocConverter

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


@bp.route("/", methods=["GET"])
def show_debug_actions_list():
    audits_dict = get_audits()
    return render_template('debug_actions.html', audits_dict=audits_dict)


@bp.route("/download_api", methods=["GET"])
def download_api_examples():
    refresh_examples()
    return basic_msg("API examples downloaded")


@bp.route("/force_upload_init_data", methods=["GET"])
def force_upload_init_data():
    pwndoc_db_init.InitialData.upload_initial_data()
    return basic_msg("API inited")


@bp.route("/refresh_api_examples", methods=["GET"])
def debug_refresh_api_examples():
    refresh_examples()
    return basic_msg(f"OK")


@bp.route("/download_logs/<log_name>", methods=["GET"])
def debug_download_logs(log_name: str = "main"):
    log_folder_paths = {
        'importer': 'user_data/importer-logs/',
        'pwndoc': 'user_data/pwndoc-logs/',
        'worker': 'user_data/worker-logs/',
    }

    if log_name in ["main", "gunicorn_error", "gunicorn_access", "everything"]:
        folder_path = log_folder_paths['importer']
    elif log_name in ["pwndoc"]:
        folder_path = log_folder_paths['pwndoc']
    else:
        return basic_msg("This log doesn't exist or it's download is not permitted."), 400

    if log_name != "everything":
        with open(f"{folder_path}{log_name}.log", encoding="utf8") as f:
            return Response(f.read(), mimetype='application/json')

    # todo: Downloading all audits for beautified backend error log can take too long.
    beautified_log_path = os.path.join(log_folder_paths['importer'], f'pwndoc_beautified_{int(time.time())}.log')
    with open(beautified_log_path, 'w', encoding='utf8') as f:
        beautified_text = generate_pwndoc_human_readable_log()
        f.write(beautified_text)

    memory_file = zip_multiple_folders_non_recursive_to_virtual(
        list(log_folder_paths.values())
     )

    return send_file(
        memory_file,
        download_name=f"pwndoc_importer_logs_{time.time()}.zip",
        as_attachment=True
    )


def generate_pwndoc_human_readable_log():
    folder_path = "user_data/pwndoc-logs/"
    log_path = relative_path(f"{folder_path}/pwndoc.log")
    with open(log_path, encoding="utf8") as f:
        log_lines = f.readlines()
    beautified_errors = log_to_beautified_errors(log_lines)

    text_answer = "This log is persistent - it shows all errors since the logs creation. That means some of the errors might have already been resolved."
    text_answer += "\nThe newest logs are on the top. Only errors which could be beautified are displayed here."
    text_answer += f"\nGenerated {str(datetime.datetime.now())} \n\n==================\n\n"

    text_answer += "\n\n================== NEXT ERROR ==================\n\n".join(beautified_errors)
    return text_answer


@bp.route("/pwndoc_human_readable_log", methods=["GET"])
def make_pwndoc_human_readable_log():
    text_answer = generate_pwndoc_human_readable_log()

    raw_response = Response(text_answer)
    raw_response.mimetype = 'text/plain'
    return raw_response


@bp.route("/test_db", methods=["GET"])
def debug_test_db():
    template1 = template_pwndoc.TNSTemplatePwndoc(fid="fid1", locale="cs")
    template2 = template_pwndoc.TNSTemplatePwndoc(fid="fid1", locale="og")

    DbTemplate.delete_template("fid")

    assert DbTemplate.get_template("fid", "og") is None

    res2, _ = DbTemplate.add_template("fid", "og", template1)
    assert DbTemplate.get_template("fid", "og") == res2

    DbTemplate.delete_template("fid")
    assert DbTemplate.get_template("fid", "og") is None

    res5, _ = DbTemplate.add_template("fid", "og", template1)
    res6, _ = DbTemplate.add_template("fid", "og", template2)
    assert res6.get_previous_revision() == res5

    DbTemplate.delete_template("fid")
    assert DbTemplate.get_template("fid", "og", include_deleted=True) == res6
    return basic_msg("OK")


@bp.route("/get_all_templates_from_db")
def debug_get_all_templates():
    current_templates: List[DbTemplate] = DbTemplate.get_all_current_templates()
    return basic_msg(f"{len(current_templates)} templates")


@bp.route("/delete_vulnerability_templates")
def delete_vulnerability_templates():
    dict_of_all_templates = PwndocTemplateManager.get_all_templates_as_dict_of_locales()
    for fid in dict_of_all_templates.keys():
        DbTemplate.delete_template(fid)
    delete_all_templates_in_pwndoc()
    TemplateScan2Report.delete_template_files()
    return basic_msg("Deleted")


@bp.route("/force_unescape_old_templates", methods=["GET"])
def force_unescape_old_templates():
    PwndocTemplateManager.update_template_db_from_pwndoc()
    all_templates: List[TNSTemplatePwndoc] = PwndocTemplateManager.get_all_templates()

    new_templates: List[TNSTemplatePwndoc] = []
    for single_template in tqdm(all_templates, desc="Conversion to HTML"):
        tmp = PwndocConverter.html_conversion(single_template, html_to_md=False, html_force_unescape_basic_tags=True)
        # print(single_template.description)
        # print(tmp.description)
        new_templates.append(tmp)

    n_templates_changed: int = 0

    for single_template in tqdm(new_templates, desc="Adding new templates"):
        new = PwndocTemplateManager.add_single_locale_template(single_template)
        if new:
            n_templates_changed += 1

    return basic_msg(f"{len(new_templates)} templates processed, {n_templates_changed} templates were updated.")


@bp.route("/flash_msg", methods=["GET"])
def test_flash_msg():
    FlashLog.info("test info")
    FlashLog.warning("test warning")
    FlashLog.error("test error")
    return render_template("base.html")


def skip_if_only_one_finding_in_group(newly_grouped_or_aliased_findings: List[Tuple[str, str, str]]) -> List[Tuple[str, str, str]]:
    """
        This function is a filter for the following desired, but unintuitive behaviour of scan2report:
            Grouping happens only if there are at least 2 findings from that group.
    """

    all_gids = []
    for fid, alias_or_gid, _ in newly_grouped_or_aliased_findings:
        if TemplateGrouping.is_gid(fid):
            all_gids.append(fid)
        elif TemplateGrouping.is_gid(alias_or_gid):
            all_gids.append(alias_or_gid)
        else:
            logger.debug("Unexpected grouping found.")

    all_gids_counter = collections.Counter(all_gids)

    answer = []
    for fid, alias_or_gid, pwndoc_id in newly_grouped_or_aliased_findings:
        possible_gid = TemplateGrouping.get_gid(fid)
        if all_gids_counter.get(possible_gid, 0) == 1:
            continue
        answer.append((fid, alias_or_gid, pwndoc_id))

    return answer


def list_newly_grouped_or_aliased_findings(audit_id: str) -> List[Tuple[str, str, str]]:
    refresh_examples(audit_id)
    TemplateAliasing.refresh()
    TemplateGrouping.refresh()

    audit_data = load_audit_file(audit_id)

    fids_and_pwndoc_ids: List[Tuple[str, str]] = []
    findings = audit_data.get("datas", {}).get("findings", [])
    for single_finding in findings:
        fid = get_fid_from_raw_finding(single_finding)
        pwndoc_id = single_finding.get("_id", "")
        if fid is None:
            FlashLog.warning(f"Pwndoc finding {pwndoc_id} doesn't have fid. Skipping.")
            continue
        fids_and_pwndoc_ids.append((fid, pwndoc_id))

    newly_grouped_or_aliased_findings: List[Tuple[str, str, str]] = []
    for fid, pwndoc_id in fids_and_pwndoc_ids:
        for qry_function in [TemplateAliasing.get_alias, TemplateGrouping.get_gid]:
            # noinspection PyArgumentList
            alias_or_gid = qry_function(fid)
            if alias_or_gid:
                newly_grouped_or_aliased_findings.append((fid, alias_or_gid, pwndoc_id))

    newly_grouped_or_aliased_findings = skip_if_only_one_finding_in_group(newly_grouped_or_aliased_findings)
    return newly_grouped_or_aliased_findings


@bp.route("/list_newly_grouped_or_aliased_findings/<audit_id>", methods=["GET"])
def show_newly_grouped_or_aliased_findings(audit_id: str):
    newly_grouped_or_aliased_findings = list_newly_grouped_or_aliased_findings(audit_id)
    answer = f"""
        The following should have been grouped or aliased but aren't. Did the files groups.json or alias.json change between the import time and now? <br>
        Grouping follows the scan2report behaviour, including the fact that group is only created if at least 2 findings are part of it. <br>
        If there are any rows in the table bellow, the recommended steps are to delete the findings and import the scanner reports again. <br>
        <br>
    """

    answer += '<table style="border-spacing: 15px; border-collapse: unset;"><tr><th>FID</th><th>alias or GID</th><th>PwnDoc ID</th><th></th></tr>'
    for row in newly_grouped_or_aliased_findings:
        answer += "<tr><td>" + "</td><td>".join(row) + "</td><td> "
        answer += f'<a href="{url_for("api_debug.delete_finding_endpoint", audit_id=audit_id, finding_pwndoc_id=row[2])}" target="_blank">Delete this finding</a>'
        answer += "</td></tr>"
    answer += '</table>'

    return basic_msg_html(answer)


@bp.route("/delete_finding/<audit_id>/<finding_pwndoc_id>", methods=["GET"])
def delete_finding_endpoint(audit_id: str, finding_pwndoc_id: str):
    delete_finding(audit_id, finding_pwndoc_id)
    return basic_msg("Finding deleted.")


@bp.route("/download_audit/<audit_id>", methods=["GET"])
def debug_download_audit(audit_id: str):
    findings = get_findings_from_audit(audit_id)
    for single_finding in findings:
        download_and_return_finding(audit_id, single_finding.get("_id"))
    return basic_msg("OK")


@bp.route("/reinit_importer_pwndoc_id_mapping", methods=["GET"])
def reinit_importer_pwndoc_id_mapping():
    PwndocTemplateManager.reinit_importer_pwndoc_mapping()
    return basic_msg("OK")
