from __future__ import annotations
import html
from typing import TYPE_CHECKING, Optional, TextIO

from .logger_config import logger
from .context import Context
from gettext import ngettext


if TYPE_CHECKING:
    from . import cli


RUNNING_ANIMATION = "|/-\\"
RUN_IDX = 0


def format_time(duration: int):
    if duration < 60:
        return f"{duration} " + ngettext("second   ", "seconds   ", duration)

    duration = duration // 60
    if duration < 60:
        return f"{duration} " + ngettext("minute   ", "minutes   ", duration)

    duration = duration // 60
    return f"{duration} " + ngettext("hour", "hours", duration)


def str_crimson(ctx: Context, text: str) -> str:
    """Turns a string in crimson rgb(151, 50, 50) using ANSI escape sequences."""
    if ctx.args.nocolor:
        return text
    # ANSI 256-color code for crimson-ish (color 131)
    return "\x1b[38;5;131m" + text + "\033[m"


def str_red(ctx: Context, text: str) -> str:
    """Turns a string in red using ANSI escape sequences."""
    if ctx.args.nocolor:
        return text
    return "\x1b[31m" + text + "\033[m"


def str_green(ctx: Context, text: str) -> str:
    """Turns a string in green using ANSI escape sequences."""
    if ctx.args.nocolor:
        return text
    return "\x1b[32m" + text + "\033[m"


def str_orange(ctx: Context, text: str) -> str:
    """Turns a string in orange using ANSI escape sequences."""
    if ctx.args.nocolor:
        return text
    return "\x1b[38;5;214m" + text + "\033[m"


def str_yellow(ctx: Context, text: str) -> str:
    """Turns a string in yellow using ANSI escape sequences."""
    if ctx.args.nocolor:
        return text
    return "\x1b[38;5;226m" + text + "\033[m"


def format_evidence(evidence: dict, prefix: str = "") -> str:
    """Given a JSON of an evidence, generate a string containing the information.

    Args:
        evidence: The JSON obtained from the API.
        prefix: string to append to the beggining of each line.

    Returns the pretty formatted string.
    """
    msg = ""

    if evidence["type"] == "text":
        if not evidence["data"]:
            return msg
        return evidence["data"]

    if evidence["type"] == "table":
        headers = evidence["data"]["headers"]
        rows = evidence["data"]["rows"]
        title_idx = -1
        if "CVE" in headers:
            title_idx = headers.index("CVE")
        elif "Description" in headers:
            title_idx = headers.index("Description")
        elif "Summary" in headers:
            title_idx = headers.index("Summary")

        msg = ""
        for row_idx, row in enumerate(rows, start=1):
            if title_idx == -1:
                msg += prefix + f"- Evidence {row_idx}:\n"
            else:
                msg += (
                    prefix + "- " + html.unescape(row[title_idx])
                    if isinstance(row[title_idx], str)
                    else str(row[title_idx]) + ":\n"
                )

            for idx, (header, data) in enumerate(zip(headers, row)):
                if idx == title_idx:
                    continue

                msg += (
                    prefix + "\t- " + header + ": " + html.unescape(data) if isinstance(data, str) else str(data) + "\n"
                )
            msg += "\n"

    return msg


def format_finding(ctx: Context, index, name, description, risk, recommendation, evidence) -> str:
    """Given finding parameters, generate a string containing the information.

    Returns the pretty formatted string.
    """
    risk_to_severity = ["Info", "Low", "Medium", "High", "Critical"]
    severity = risk_to_severity[risk]

    if severity == "Info":
        msg = str_green(ctx, f"[{index}] {name}")
    elif severity == "Low":
        msg = str_yellow(ctx, f"[{index}] {name}")
    elif severity == "Medium":
        msg = str_orange(ctx, f"[{index}] {name}")
    elif severity == "High":
        msg = str_red(ctx, f"[{index}] {name}")
    else:
        msg = str_crimson(ctx, f"[{index}] {name}")

    if risk == 0 and not recommendation:
        return msg

    msg += "\n"

    if severity == "Info":
        msg += str_green(ctx, f"\t- Risk Level: {risk} ({severity})\n\n")
    elif severity == "Low":
        msg += str_yellow(ctx, f"\t- Risk Level: {risk} ({severity})\n\n")
    elif severity == "Medium":
        msg += str_orange(ctx, f"\t- Risk Level: {risk} ({severity})\n\n")
    elif severity == "High":
        msg += str_red(ctx, f"\t- Risk Level: {risk} ({severity})\n\n")
    else:
        msg += str_crimson(ctx, f"\t- Risk Level: {risk} ({severity})\n\n")

    msg += "\tVulnerability Details:\n"
    evidence_str = format_evidence(evidence, prefix="\t")
    if evidence_str:
        msg += evidence_str + "\n"

    if description != name:
        msg += f"\t- Description: {description}\n"
    if recommendation:
        msg += f"\t- Recommendation: {recommendation}\n"

    msg += "\n"

    return msg


def log_runtime_status(ctx: Context, status: cli.ScanStatus, del_prev=False):
    """Prints the status of a current scan. Shows a bar that fills from 0% to 100% progress. Has
    rotating bar animation."""
    global RUN_IDX

    progress_line = "*" * int((status.progress / 100) * 50) + "_" * int((1 - status.progress / 100) * 50)

    # The extra spaces below are necessary to avoid some formatting duplication

    msg = f"Progress: {progress_line} {status.progress}%  \n"
    if status.status_name == "finished":
        msg += "  Status: Finished    \n"
    else:
        msg += "  Status: " + str_green(ctx, f"Running {RUNNING_ANIMATION[RUN_IDX]}\n")
    RUN_IDX = (RUN_IDX + 1) % len(RUNNING_ANIMATION)
    msg += f"  Start time: {status.start_time}\n"
    msg += f"  Duration: {format_time(status.duration)}\n"
    msg += f"  Total tests: {status.num_tests}\n"
    msg += f"  Finished tests: {status.num_finished_tests}\n"

    if del_prev:
        logger.error("\033\x5bF" * 7)
    logger.error("%s\n", msg)


def print_report(ctx, scan_output, outfile):
    """Prints a the findings of a scan to the given output file. The findings are taken from a JSON
    that was requested previously.

    Args:
        outfile: File to print to. It can also be sys.stdout.
    """
    if scan_output is None:
        return

    data = scan_output["data"]
    if data["output_type"] != "finding_list":
        return
    findings = data["output_data"]["findings"]

    title = "+" + "-" * 58 + "+\n"
    title += "|" + "Vulnerability Scan Report".center(58) + "|\n"
    title += "+" + "-" * 58 + "+\n\n"
    print(title, file=outfile)

    findings.sort(key=lambda x: (-x["risk_level"], not bool(x["recommendation"])))

    for index, finding in enumerate(findings, start=1):
        msg = format_finding(
            ctx,
            index,
            finding["name"],
            finding["vuln_description"],
            finding["risk_level"],
            finding["recommendation"],
            finding["vuln_evidence"],
        )
        print(msg, file=outfile)
    print("", file=outfile)


def print_summary(summary: Optional[cli.ScanStatus], target: str, outfile: TextIO) -> None:
    """Prints a summary table about a finished scan. The summary is a JSON file that was
    requested previously from the API."""
    if summary is None or summary.result_summary is None:
        return

    critical_cnt = summary.result_summary.critical
    high_cnt = summary.result_summary.high
    medium_cnt = summary.result_summary.medium
    low_cnt = summary.result_summary.low
    info_cnt = summary.result_summary.info
    start_time = summary.start_time
    end_time = summary.end_time

    sq_width = 50
    msg = "+" + " TEST summary ".center(sq_width - 2, "-") + "+\n"
    msg += "|" + " " * (sq_width - 2) + "|\n"
    msg += f"|  URL: {target}".ljust(sq_width - 1) + "|\n"
    msg += f"|  Critical Risk Findings: {critical_cnt}".ljust(sq_width - 1) + "|\n"
    msg += f"|  High Risk Findings: {high_cnt}".ljust(sq_width - 1) + "|\n"
    msg += f"|  Medium Risk Findings: {medium_cnt}".ljust(sq_width - 1) + "|\n"
    msg += f"|  Low Risk Findings: {low_cnt}".ljust(sq_width - 1) + "|\n"
    msg += f"|  Info Risk Findings: {info_cnt}".ljust(sq_width - 1) + "|\n"
    msg += f"|  Start time: {start_time}".ljust(sq_width - 1) + "|\n"
    msg += f"|  End time: {end_time}".ljust(sq_width - 1) + "|\n"
    msg += "|" + " " * (sq_width - 2) + "|\n"
    msg += "+" + "-" * (sq_width - 2) + "+\n"

    print(msg, file=outfile)
