from typing import List, Tuple
from loguru import logger
from tqdm import tqdm

from pwndoc_api import get_audits, download_and_return_audit


class AuditFindingLookup:
    def __init__(self):
        self.audit_list: List[Tuple[str, str]] = get_audits()
        self.audits = {}

        for audit_id, audit_name in tqdm(self.audit_list, desc="Downloading audits"):
            audit_content = download_and_return_audit(audit_id)
            self.audits[audit_id] = audit_content

    def lookup_field_by_text(self, text: str) -> List[Tuple[str, str, str, str, str]]:
        possible_answers: List[Tuple[str, str, str, str, str]] = []

        for audit_id, audit_content in self.audits.items():
            audit_name = audit_content.get("name", "")

            for finding in audit_content.get('findings', []):
                finding_id = finding.get("_id", "")
                finding_name = finding.get("title", "")

                for attr_name in finding.keys():
                    try:
                        if isinstance(finding[attr_name], str):
                            if finding[attr_name].startswith(text):
                                possible_answers.append((audit_id, audit_name, finding_id, finding_name, attr_name))

                        if attr_name == 'customFields':
                            for custom_field in finding[attr_name]:
                                if custom_field.get('text') is not None and custom_field.get('text').startswith(text):
                                    possible_answers.append((audit_id, audit_name, finding_id, finding_name,
                                                             custom_field.get("customFields", {}).get("label", "Custom field with no label.")
                                                             ))
                    except Exception as e:
                        logger.warning(f"Exception in search for invalid HTML somewhere in audits. Skipping the finding. | {e}")
        return possible_answers


def log_to_beautified_errors(log_lines: List[str]) -> List[str]:
    pwndoc_raw_errors = _extract_pwndoc_errors(log_lines)
    pwndoc_raw_errors.sort(reverse=True)

    lookup = AuditFindingLookup()

    pwndoc_beautified_errors = _beautify_errors(pwndoc_raw_errors, lookup)
    return pwndoc_beautified_errors


def _extract_pwndoc_errors(lines: List[str]) -> List[List[str]]:

    def n_interesting_lines_with_this_one(line: str) -> int:
        n_lines_after_interesting_line = {
            'properties: {': 7,  # 7 is is only if the Finding doesn't have any \n
            'Template Error:': 3,
        }
        return n_lines_after_interesting_line.get(line.strip(), -2)

    all_findings = []
    remaining_interesting_lines = 0

    for line in lines:
        new_interesting_lines_count = n_interesting_lines_with_this_one(line)
        if new_interesting_lines_count > 0:
            all_findings.append([])

        remaining_interesting_lines = max(remaining_interesting_lines, new_interesting_lines_count)
        if remaining_interesting_lines > 0:
            all_findings[-1].append(line)
        remaining_interesting_lines -= 1

    return all_findings


def _beautify_errors(pwndoc_errors: List[List[str]], lookup: AuditFindingLookup) -> List[str]:
    beautified_errors = []

    lines_to_parsing_functions = {
        'properties: {': _parse_json_like_log,
        'Template Error:': _template_error_simple,
    }

    for lines_of_single_error in pwndoc_errors:
        try:
            first_line = lines_of_single_error[0].strip()
            parsing_function = lines_to_parsing_functions.get(first_line, None)

            if parsing_function:
                beautified_errors.append(parsing_function(lines_of_single_error, lookup))
            else:
                raise NotImplementedError(f"No custom msg for this format of error msg is available.")

        except NotImplementedError:
            beautified_errors.append("".join(lines_of_single_error))
            logger.debug("Non implemented PwnDoc log format. Returning raw output.")

        except Exception as e:
            beautified_errors.append("".join(lines_of_single_error))
            logger.warning(f"Exception in best effort parsing of pwndoc error msgs. Returning raw output. | {e}")

    return beautified_errors


def _parse_json_like_log(lines_of_single_error: List[str], lookup: AuditFindingLookup) -> str:
    # I can't load it as JSON - it looks like it, but it's not. For example the property names are not quoted.
    _, error_id, _ = lines_of_single_error[1].strip().split("'", 2)
    error_text = lines_of_single_error[4].split("'", 1)[1].rsplit("'")[0]

    if error_id == "scopeparser_execution_failed":
        answer = f"""
Error: Invalid or unsupported HTML in a PwnDoc finding.

Description:
Some field from a finding in the audit contains HTML that Pwndoc is unable to parse.
It can be anything from mismatched HTML tags, to unsupported tags like <a href="something">.
Importer is already trying to sanitize input from scan2report, but as Pwndoc doesn't support full HTML,
it's hard to get all edge cases.
Recommendation: Opening the finding in Pwndoc using web browser and clicking Save sometimes fixes the HTML enough
so that PwnDoc succeeds.

The offending field has content:
<======>
{error_text} 
<======>

Location:
"""
        possible_findings = lookup.lookup_field_by_text(error_text)
        if possible_findings:
            answer += "Search across all audits found the text in:\n audit_id | audit_name | finding_id | finding_name | field\n"
            for single_occurrence in possible_findings:
                answer += " | ".join(single_occurrence) + "\n"
        else:
            answer += "The text was not found in any field of any finding in any audit. This might be a bug, or the findings might have been modified in the meantime.\n"

        return answer

    raise NotImplementedError(f"No custom msg for {error_id} is available.")


def _template_error_simple(lines_of_single_error: List[str], _: AuditFindingLookup) -> str:
    return """
Error: Invalid or unsupported HTML in a PwnDoc finding.

This error usually appears twice in log output - somewhere above should be more detailed description of the same error.

""" + "".join(lines_of_single_error)
