import json
import argparse
import sys
from collections import OrderedDict
from pathlib import Path

import cmd2
from cmd2 import Fg as COLORS
from simple_rest_client.exceptions import NotFoundError
from tabulate import tabulate
from faraday_cli.extras.halo.halo import Halo
from faraday_cli.shell.utils import (
    IGNORE_SEVERITIES,
    SEVERITIES,
)
from faraday_cli.api_client.exceptions import RequestError
from faraday_cli.api_client.filter import FaradayFilter
from faraday_cli.config import active_config
from faraday_cli.shell import utils

VULN_UPDATE_JSON_SCHEMA = {
    "properties": {
        "status",
        "severity",
        "desc",
        "name",
        "tag",
        "confirmed",
    },
}


@cmd2.with_default_category("Vulnerabilities")
class VulnerabilityCommands(cmd2.CommandSet):
    def __init__(self):
        super().__init__()

    # List Vulnerabilities
    list_vulns_parser = cmd2.Cmd2ArgumentParser()
    list_vulns_parser.add_argument(
        "-j", "--json-output", action="store_true", help="Show output in json"
    )
    list_vulns_parser.add_argument(
        "-p",
        "--pretty",
        action="store_true",
        help="Show table in a pretty format",
    )
    list_vulns_parser.add_argument(
        "-w", "--workspace-name", type=str, help="Workspace"
    )
    list_vulns_parser.add_argument(
        "--ignore-info",
        action="store_true",
        help=f"Ignore {'/'.join(IGNORE_SEVERITIES)} vulnerabilities",
    )
    list_vulns_parser.add_argument(
        "--severity",
        type=str,
        help="Filter by severity",
        choices=SEVERITIES,
        default=[],
        nargs="*",
    )
    list_vulns_parser.add_argument(
        "--confirmed",
        action="store_true",
        help="Show confirmed vulnerabilities",
    )

    @cmd2.as_subcommand_to(
        "vuln", "list", list_vulns_parser, help="list vulnerabilities"
    )
    def list_vulns(self, args: argparse.Namespace):
        """List Vulnerabilities"""

        @Halo(
            text="Gathering data",
            text_color="green",
            spinner="dots",
            stream=sys.stderr,
        )
        def get_data(workspace_name, filter_to_apply):
            vulns = self._cmd.api_client.get_vulns(
                workspace_name, filter_to_apply
            )
            return vulns

        if not args.workspace_name:
            if active_config.workspace:
                workspace_name = active_config.workspace
            else:
                self._cmd.perror("No active Workspace")
                return
        else:
            workspace_name = args.workspace_name
        query_filter = FaradayFilter()
        for severity in args.severity:
            query_filter.require_severity(severity)
        if args.ignore_info:
            for severity in IGNORE_SEVERITIES:
                query_filter.ignore_severity(severity)
        if args.confirmed:
            query_filter.filter_confirmed()
        filter_to_apply = query_filter.get_filter()
        try:
            vulns = get_data(workspace_name, filter_to_apply)
        except NotFoundError:
            self._cmd.perror("Workspace not found")
        else:
            if args.json_output:
                self._cmd.poutput(
                    json.dumps(vulns["vulnerabilities"], indent=4)
                )
            else:
                if not vulns["count"]:
                    self._cmd.perror(
                        f"No vulnerabilities in workspace: {workspace_name}"
                    )
                else:
                    data = [
                        OrderedDict(
                            {
                                "ID": x["id"],
                                "NAME": x["value"]["name"],
                                "SEVERITY": cmd2.style(
                                    x["value"]["severity"].upper(),
                                    fg=utils.get_severity_color(
                                        x["value"]["severity"]
                                    ),
                                ),
                                "STATUS": x["value"]["status"],
                                "CONFIRMED": x["value"]["confirmed"],
                                "ASSET": f"{x['value']['target']} [{x['value']['parent_type']} "  # noqa: E501
                                + (
                                    f"- ID:{x['value']['parent']}]"
                                    if x["value"]["parent_type"] == "Host"
                                    else f"- {x['value']['service']['summary']}]"  # noqa: E501
                                ),
                                "HOSTNAMES": "\n".join(
                                    x["value"]["hostnames"]
                                ),
                            }
                        )
                        for x in vulns["vulnerabilities"]
                    ]
                    self._cmd.print_output(
                        tabulate(
                            data,
                            headers="keys",
                            tablefmt="psql" if args.pretty else "simple",
                        )
                    )

    # Add evidence to Vulnerability
    add_evidence_parser = cmd2.Cmd2ArgumentParser()
    add_evidence_parser.add_argument(
        "image", type=str, help="Path of the image"
    )
    add_evidence_parser.add_argument(
        "-w", "--workspace-name", type=str, help="Workspace"
    )
    add_evidence_parser.add_argument(
        "-id",
        "--vulnerability-id",
        type=int,
        help="Vulnerability ID",
        required=True,
    )

    @cmd2.as_subcommand_to(
        "vuln",
        "add-evidence",
        add_evidence_parser,
        help="add evidence to vulnerability",
    )
    def add_evidence(self, args: argparse.Namespace):
        """Add evidence to Vulnerability"""

        if not args.workspace_name:
            if active_config.workspace:
                workspace_name = active_config.workspace
            else:
                self._cmd.perror("No active Workspace")
                return
        else:
            workspace_name = args.workspace_name
        image_path = Path(args.image)
        if not image_path.is_file():
            self._cmd.perror(f"File {args.image} not found")
            return
        self._cmd.api_client.get_vuln(workspace_name, args.vulnerability_id)
        try:
            response = self._cmd.api_client.upload_evidence_to_vuln(
                workspace_name, args.vulnerability_id, args.image
            )
            self._cmd.poutput(response["message"])
        except RequestError as e:
            self._cmd.perror(e.message)

    update_vuln_parser = cmd2.Cmd2ArgumentParser()
    update_vuln_parser.add_argument(
        "-w", "--workspace-name", type=str, help="Workspace"
    )
    update_vuln_parser.add_argument(
        "vulnerability_id",
        type=int,
        help="Vulnerability ID",
    )
    update_vuln_parser.add_argument(
        "--status",
        type=str,
        help="Status of the vuln: open, closed, re-opened, risk-accepted",
        choices=["open", "closed", "re-opened", "risk-accepted"],
    )
    update_vuln_parser.add_argument(
        "--severity",
        type=str,
        help="Severity of the vuln",
        choices=["unclassified", "info", "low", "med", "high", "critical"],
    )
    update_vuln_parser.add_argument(
        "--desc",
        type=str,
        help="Description of the vuln",
    )
    update_vuln_parser.add_argument(
        "--name",
        type=str,
        help="Name of the vuln",
    )
    update_vuln_parser.add_argument(
        "--tag",
        type=str,
        help="Tag to add to vulnerability",
        action="append",
    )
    update_vuln_parser.add_argument(
        "--confirmed",
        type=str,
        choices=["True", "False"],
        help="Indicates if the vuln is confirmed",
    )

    @cmd2.as_subcommand_to(
        "vuln",
        "update",
        update_vuln_parser,
        help="update existing vulnerability",
    )
    def update_vuln(self, args: argparse.Namespace):
        """Add evidence to Vulnerability"""

        if not args.workspace_name:
            if active_config.workspace:
                workspace_name = active_config.workspace
            else:
                self._cmd.perror("No active Workspace")
                return
        else:
            workspace_name = args.workspace_name
        body = {}
        for k in VULN_UPDATE_JSON_SCHEMA["properties"]:
            if not (
                args.__getattribute__(k) is None
                or args.__getattribute__(k) == []
            ):
                body[k] = args.__getattribute__(k)
        if "confirmed" in body:
            body["confirmed"] = body["confirmed"] == "True"
        if "tag" in body:
            body["tags"] = body.pop("tag")
        try:
            if len(body) != 0:
                if "desc" in body:
                    body["description"] = body["desc"]
                self._cmd.api_client.update_vuln(
                    workspace_name, args.vulnerability_id, body
                )
                self._cmd.poutput("Vulnerability updated")
        except RequestError as e:
            self._cmd.perror(e.message)

    delete_vuln_parser = cmd2.Cmd2ArgumentParser()
    delete_vuln_parser.add_argument(
        "vuln_id", type=int, help="ID of the vulnerability"
    )
    delete_vuln_parser.add_argument(
        "-w", "--workspace-name", type=str, help="Workspace"
    )

    @cmd2.as_subcommand_to(
        "vuln",
        "delete",
        delete_vuln_parser,
        help="Delete existing vulnerability",
    )
    def delete_vuln(self, args: argparse.Namespace):
        """Delete Vulnerability"""

        if not args.workspace_name:
            if active_config.workspace:
                workspace_name = active_config.workspace
            else:
                self._cmd.perror("No active Workspace")
                return
        else:
            workspace_name = args.workspace_name
        try:
            self._cmd.api_client.delete_vuln(workspace_name, args.vuln_id)
        except NotFoundError:
            self._cmd.perror("Vuln not found")
        except Exception as e:
            self._cmd.perror(f"{e}")
        else:
            self._cmd.poutput(
                cmd2.style("Vulnerability deleted", fg=COLORS.GREEN)
            )
