#!/usr/bin/python

import click
import configparser
import json
import requests
import sys
import time
import uuid
from integrations.sonarqube import SonarqubeBase
from os import path
from tabulate import tabulate
from urllib.parse import urlencode


version = "0.1.7"


class State(object):
    def __init__(self):
        self.api_key = None
        self.headers = None
        self.hackedu_url = None
        self.username = None
        self.password = None
        self.token = None
        self.organization = None
        self.url = None
        self.app = None
        self.edition = None
        self.branch = None
        self.source = None
        self.config_path = "~/.hackedu"
        self.url_prefix = "v1"

pass_state = click.make_pass_decorator(State, ensure=True)

def set_state(config, state, section, option):
    if config.has_section(section):
        if config.has_option(section, option):
            if not state.__dict__[option]:
                state.__dict__[option] = config[section][option]

def set_headers(config, state):
    if config.has_section("default"):
        if config.has_option("default", "api_key"):
            if not state.headers:
                state.headers = {"X-API-Key": config["default"]["api_key"]}

def profile_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        config = configparser.ConfigParser()

        config_path = path.expanduser(state.config_path)
        config.read(config_path)
        if not path.exists(config_path):
            print("Error")
            print("Config file does not exist: {}".format(value))
            sys.exit()

        if not config.has_section(value):
            print("Error")
            print("Config file does not have profile: {}".format(value))
            sys.exit()

        for key, _ in config.items(value):
            set_state(config, state, value, key)

    return click.option("--profile",
                        expose_value=False,
                        default="default",
                        help="Determines which config profile to use.",
                        callback=callback)(f)

def config_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        config = configparser.ConfigParser()

        config_path = path.expanduser(value)
        config.read(config_path)
        state.config_path = config_path
        if not path.exists(config_path):
            print("Error")
            print("Config file does not exist: {}".format(value))
            sys.exit()

        if config.has_section("default"):
            set_headers(config, state)
            set_state(config, state, "default", "hackedu_url")

        if config.has_section("sonarqube"):
            set_state(config, state, "sonarqube", "username")
            set_state(config, state, "sonarqube", "password")
            set_state(config, state, "sonarqube", "token")
            set_state(config, state, "sonarqube", "url")
            set_state(config, state, "sonarqube", "app")
            set_state(config, state, "sonarqube", "branch")
            set_state(config, state, "sonarqube", "source")
            set_state(config, state, "sonarqube", "edition")

    return click.option("--config",
                        expose_value=False,
                        default="~/.hackedu",
                        help="Configuration file used for pre-defined options. ",
                        callback=callback)(f)


def hackedu_url_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            state.hackedu_url = value

    return click.option("--hackedu_url",
                        expose_value=False,
                        help="Environment used to determine HackEDU public API host. ",
                        callback=callback)(f)


def api_key_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            state.headers = {"X-API-Key": value}

    return click.option("--api_key",
                        expose_value=False,
                        help="X-API-Key header for interacting with the HackEDU public API.",
                        callback=callback)(f)

def common_options(f):
    f = config_option(f)
    f = profile_option(f)
    f = api_key_option(f)
    f = hackedu_url_option(f)

    return f

@click.group()
@click.version_option(version)
def hackedu():
    """A CLI wrapper for the HackEDU public API."""

@hackedu.command("config")
def config():
    """Command to create your .hackedu config file."""
    api_key = click.prompt("Please enter your HackEDU API Key", type=str)
    if api_key:
        config_location = click.prompt("Where would you like to store your HackEDU config file? Leave empty for "
                                       "default location:", type=str, default="~/")
        file_contents = "[default]\napi_key={}\nhackedu_url=https://api.hackedu.com".format(api_key)
        if config_location != "~/":
            with open(path.expanduser("{}/.hackedu".format(config_location)), "w") as config_file:
                config_file.write(file_contents)
        else:
            with open(path.expanduser("~/.hackedu"), "w") as config_file:
                config_file.write(file_contents)


@hackedu.group()
@pass_state
def issue_source(state):
    """Issue Source commands"""

@issue_source.command("ls")
@common_options
@pass_state
def ls(state):
    """List Issue Sources"""
    issue_source_url = "{}/{}/issue-sources".format(state.hackedu_url, state.url_prefix)

    response = requests.get(issue_source_url, headers=state.headers)
    if response.status_code != 200:
        print("Failed!")
        print(response.json())
        return
    items = response.json()["issue_sources"]
    if items:
        header = list(items[0].keys())[0:3]
        rows = [list(x.values())[0:3] for x in items]
        print(tabulate(rows, header))
    else:
        print("Warning: No issue sources returned.")

@issue_source.command("create")
@click.option("--title", help="Name of issue source.")
@click.option("--type", help="Type of issue source.", type=click.Choice(["sonarqube"]))
@common_options
@pass_state
def create(state, title, type):
    """Create Issue Source"""
    issue_source_url = "{}/{}/issue-sources".format(state.hackedu_url, state.url_prefix)
    issue_source_types_path = "{}/{}/issue-sources/types".format(state.hackedu_url, state.url_prefix)
    if not title:
        print("Error: Missing required option '--title'")
        return

    print("creating issue source...")

    payload = {
        "uuid":uuid.uuid4(),
        "title": title,
        "settings": json.dumps({}),
    }

    if type == "sonarqube":
        response = requests.get("{}?key={}".format(issue_source_types_path, type), headers=state.headers)
        issue_source_type_id = response.json()["types"][0]["id"]
        payload["issue_source_type_id"] = issue_source_type_id

    response = requests.post(issue_source_url, data=payload, headers=state.headers)
    if response.status_code != 200:
        print("Failed!")
        print(response.json())
        return

    print("Success!")
    print(response.json()["uuid"])


@hackedu.group()
@pass_state
def issues(state):
    """Issues command"""

@issues.command("ls")
@click.option("--source", help="HackEDU Issue source uuid.")
@common_options
@pass_state
def ls(state, source):
    """List Issues"""
    issues_url = "{}/{}/issues".format(state.hackedu_url, state.url_prefix)
    if source:
        issues_url = "{}/{}/issues?source={}".format(state.hackedu_url, state.url_prefix, source)

    response = requests.get(issues_url, headers=state.headers)
    if response.status_code != 200:
        print("Failed!")
        print(response.json())
        return

    issues = response.json()["issues"]
    header = ["issue uuid", "unique id"]
    rows = []
    for issue in issues:
        rows.append([issue["uuid"], issue["issue_source_unique_id"]])
    print(tabulate(rows, header))



@issues.group()
@pass_state
def sync(state):
    """Sync Issues"""

@sync.command("sonarqube")
@click.option("--source", help="HackEDU Issue source uuid.")
@click.option("--organization", help="Sonarqube Organization")
@click.option("--url", help="Sonarqube URL.")
@click.option("--username", help="Sonarqube username.")
@click.option("--password", help="Sonarqube password")
@click.option("--token", help="Sonar authentication token. Can be used in place of username and password")
@click.option("--branch", help="Repository branch name that Sonarqube will analyze.")
@click.option("--app", help="Sonarqube app name.")
@click.option("--edition", help="Sonarqube edition.", type=click.Choice(["community", "enterprise", "cloud"],
                                                                        case_sensitive=False))
@common_options
@pass_state
def sonarqube(state, source, organization, url, username, password, token, branch, app, edition):
    """Sync Issues from Sonarqube to HackEDU"""

    if source:
        state.source = source

    if organization:
        state.organization = organization

    if url:
        state.url = url

    if username:
        state.username = username

    if password:
        state.password = password

    if token:
        state.token = token

    if branch:
        state.branch = branch

    if app:
        state.app = app

    if edition:
        state.edition = edition

    sonarqube = SonarqubeBase(
        state.organization,
        state.url,
        state.username,
        state.password,
        state.token,
        state.app,
        state.branch,
        state.edition,
    )

    issues_url = "{}/{}/issues".format(state.hackedu_url, state.url_prefix)
    vulnerabilities_url = "{}/{}/vulnerabilities".format(state.hackedu_url, state.url_prefix)
    if not state.source:
        print("Error: Missing required option '--source'")
        return

    print("syncing sonarqube...")
    sonarqube_vulnerabilities = sonarqube.get_vulnerabilities()
    print("found {} issues".format(len(sonarqube_vulnerabilities)))
    print("syncing issues to hackedu...")

    for sonarqube_vulnerability in sonarqube_vulnerabilities:
        response = requests.get("{}?{}".format(vulnerabilities_url,
                                               urlencode(sonarqube_vulnerability["vulnerability_types"])),
                                headers=state.headers)
        if response.status_code != 200:
            print("Failed!")
            print(response.json())
            return

        hackedu_vulnerabilities = response.json()["vulnerabilities"]

        if not hackedu_vulnerabilities:
            print("could not find {} in HackEDU database, skipping..."
                  .format("".join(sonarqube_vulnerability["vulnerabilities"])))
            continue

        for hackedu_vulnerability in hackedu_vulnerabilities:
            payload = {
                "title": sonarqube_vulnerability["title"],
                "description": "",
                "issue_source_uuid": state.source,
                "issue_source_unique_id": uuid.uuid4(),
                "app_id": state.app,
                "severity": sonarqube_vulnerability["severity"],
                "vulnerability": hackedu_vulnerability["id"],
                "timestamp": sonarqube_vulnerability["timestamp"],
                "url": "{}/project/issues?id={}&types=VULNERABILITY".format(state.url, state.app)
            }
            response = requests.post(issues_url, data=payload, headers=state.headers)
            if response.status_code != 200:
                print("Failed!")
                print(response.json())
                return


            time.sleep(.5)

        time.sleep(.5)


    print("Success!")

if __name__ == "__main__":
    hackedu(prog_name="hackedu")
