import json
import os
from pathlib import Path
import time
from collections import defaultdict
from typing import Dict, List, Optional, Tuple

from flask import request, redirect, url_for, render_template, flash, Blueprint, abort, send_file
from tqdm import tqdm
from loguru import logger

import config
from template_manager import PwndocTemplateManager
from template_scan2report import TemplateScan2Report
from helpers.flask_multifile_upload import save_multiple_uploaded_files
from helpers.file_utils import json_dump, relative_path, extract_fid_from_filename, zip_folder_recursively, generate_random_folder_path
from helpers.db_models import DbTemplate
from template_converters import Scan2ReportConvert, PwndocConverter
from template_pwndoc import TNSTemplatePwndoc
from helpers.custom_logging import FlashLog

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


@bp.route("/download_from_pwndoc")
@bp.route("/download_from_pwndoc/<output_format>")
@bp.route("/download_from_pwndoc/<output_format>/<locale>")
def download_templates_from_pwndoc_to_scan2report(
        output_format: str = "scan2report_native",
        locale: str = config.LOCALES[0],
        force_locale_folder: Optional[str] = None,
        prefix_title_with_locale: bool = False
):
    """
        This function deletes all local templates and downloads the relevant locale from pwndoc to replace them.
    """
    OUTPUT_FORMAT_MAPPING = {"scan2report_native": True, "scan2report_with_pwndoc_formatting": False}
    PwndocTemplateManager.update_template_db_from_pwndoc()
    TemplateScan2Report.recreate_template_folders_if_necessary()

    if force_locale_folder is None:
        force_locale_folder = locale

    if output_format not in OUTPUT_FORMAT_MAPPING.keys():
        abort(400, description="The selected output format is not supported.")
        return  # this is return is unreachable, but included to silence PyCharm warning

    TemplateScan2Report.delete_template_files(list({locale, "og"}))

    s2r_dicts: List[dict] = PwndocConverter.convert_to_list_of_scan2report_combined_dict(
        output_without_html_tags=OUTPUT_FORMAT_MAPPING[output_format],
        locale=locale
    )
    output_folder = TemplateScan2Report.native_folder_name_for_locale(force_locale_folder)
    for single_template_dict in s2r_dicts:
        output_file_path = os.path.join(output_folder, f"{single_template_dict.get('fid')}.json")
        if Path(output_file_path).is_file():
            logger.warning(f"Template for {single_template_dict.get('fid')} with locale {force_locale_folder} already exists on disk. Skipping to avoid overwriting it.")
            continue

        if prefix_title_with_locale:
            single_template_dict['name'] = f"[{locale.upper()}] {single_template_dict.get('name', '')}"

        json_dump(single_template_dict, output_file_path)

    return f"OK; {len(s2r_dicts)} templates downloaded; Converted to format {output_format} for locale {locale}"


@bp.route("/import_scan2report_to_pwndoc", methods=["GET", "POST"])
def import_scan2report_templates_to_pwndoc():
    if request.method == "GET":
        return render_template("import_templates.html", locales=config.LOCALES)

    locale_to_import = request.form.get("template_locale")
    if locale_to_import not in config.LOCALES:
        FlashLog.warning("This locale doesn't exist.")
        return redirect(url_for('api_templates.import_scan2report_templates_to_pwndoc'))

    output_folder = generate_random_folder_path(f"template_import_{locale_to_import}")

    error_msg, saved_filenames = save_multiple_uploaded_files(output_folder, reserved_filenames=set(), allowed_extensions={"json"})
    if len(error_msg) > 0:
        FlashLog.error(error_msg)
        return redirect(url_for('api_templates.import_scan2report_templates_to_pwndoc'))

    PwndocTemplateManager.update_template_db_from_pwndoc()  # necessary for mapping new uploads to existing templates

    bundled_templates: Dict[str, Dict[str, TNSTemplatePwndoc]] = defaultdict(dict)

    for single_filename in tqdm(saved_filenames, desc="Converting templates"):
        fid = extract_fid_from_filename(single_filename)
        with open(os.path.join(output_folder, single_filename), encoding="utf8") as f:
            template_json = json.load(f)
        lang_and_templates: Dict[str, TemplateScan2Report] = TemplateScan2Report.parse_dict_for_locale_and_og(fid, locale_to_import, template_json)
        # I don't need to do DB refresh (for pwndoc ID) after every addition because I'm processing only single locale (or locale and og at the same time).
        for locale, single_template in lang_and_templates.items():
            single_tns_template: TNSTemplatePwndoc = Scan2ReportConvert.convert_single_finding_to_tns_pwndoc(single_template, locale)
            assert fid == single_tns_template.fid
            assert locale == single_tns_template.locale
            bundled_templates[fid][locale] = single_tns_template
    del single_filename, fid, single_tns_template, locale, template_json, lang_and_templates, f, output_folder

    for fid in tqdm(bundled_templates, desc="Uploading templates"):
        dict_of_locales: Dict[str, TNSTemplatePwndoc] = bundled_templates[fid]
        PwndocTemplateManager.add_locales_for_fid(list(dict_of_locales.values()))

    PwndocTemplateManager.update_template_db_from_pwndoc()  # todo: this is workaround for not using PwndocTemplateManager.add_single_locale_template

    FlashLog.info(f"OK; {len(bundled_templates)} templates uploaded")
    return redirect(url_for('api_templates.import_scan2report_templates_to_pwndoc'))


@bp.route("/export_as_scan2report_native", methods=["GET"])
def export_as_scan2report_native():
    for locale in config.LOCALES:
        download_templates_from_pwndoc_to_scan2report("scan2report_native", locale)
    zip_path = zip_folder_recursively(config.TEMPLATE_FOLDER_SCAN2REPORT)
    return send_file(
        zip_path,
        download_name=f"scan2report_templates_{int(time.time())}.zip",
        as_attachment=True
    )


@bp.route("/all_versions/<fid>/<locale>", methods=["GET"])
def count_versioned_template(fid: str, locale: str):
    PwndocTemplateManager.update_template_db_from_pwndoc()
    history = DbTemplate.get_history(fid, locale)
    return [{
        'history_id': i,
        'timestamp': x.timestamp,
        'data': json.loads(x.content)
    } for i, x in enumerate(history)]


@bp.route("/version/<fid>/<locale>", methods=["GET"])
@bp.route("/version/<fid>/<locale>/<history_id>", methods=["GET"])
def show_versioned_template(fid: str, locale: str, history_id: str = "latest"):
    skip_db_refresh = request.args.get('skipDbRefresh', default=False, type=lambda v: v.lower() == 'true')

    if not skip_db_refresh:
        PwndocTemplateManager.update_template_db_from_pwndoc()
    history = DbTemplate.get_history(fid, locale)
    if history_id == "latest":
        history_id = str(len(history)-1)

    try:
        history_id = int(history_id)
    except ValueError:
        return "History ID is not a number", 500

    try:
        return json.loads(history[history_id].content)
    except IndexError:
        if history_id == 0:
            return "This combination of FID and Locale doesn't exist.", 500
        return "This history ID for combination of FID and Locale doesn't exist.", 500


@bp.route("/compare_versions", methods=["GET"])
def compare_versions(fid: str = None, locale: str = None):
    PwndocTemplateManager.update_template_db_from_pwndoc()
    all_latest_versions = DbTemplate.get_all_current_templates()
    fids = set([x.fid for x in all_latest_versions])

    # history: List[Tuple[str, str]] = []  # history_id, "history_id: date and time"

    return render_template('version_history.html', fids=fids, locales=config.LOCALES
                           # , chosen_fid=fid, chosen_locale=locale, history=history
    )
