# -*- coding: utf-8 -*-

import os

from masappcli.elevenpaths_auth import mASAPP_CI_auth
from tabulate import tabulate
import json
import time


class mASAPP_CI():
    """

    TODO ADD MASAPP_CI INFO
    """

    LANGUAGES = ["en", "es"]
    MASAPP_LINK = "https://masapp.elevenpaths.com/"



    def __init__(self, key, secret):
        """
        :param key:
        :param secret:
        :var auth_user
        :var scan_info:
        :var scan_result:
        :var exceeded_limit:
        """
        self.key = key
        self.secret = secret
        self.auth_user = mASAPP_CI_auth(self.key, self.secret)
        self.scan_info = {
            'wg': None,
            'hashPath': None,
            'scanId': None,
            'appKey': None,
            'scanDate': None,
            'lang': None,
            'packageNameOrigin': None
        }

        self.scan_result = {
            'riskScore': None,
            'behaviorals': {'critical': [], 'high': [], 'medium': [], 'low': []},
            'vulnerabilities': {'critical': [], 'high': [], 'medium': [], 'low': []}
        }

        self.exceeded_limit = {
            "expected": None,
            "obtained": None
        }


    def _print_excess(self):
        """

        :return: It prints the information that is stored in self.exceeded_limit. This printing will vary depending on
                 the type of the values contained in self.exceeded_limit["expected"] and self.exceeded_limit["obtained"].

                * If both elements are dictionaries it would print the elements that surpass the limit that the user\
                has defined.

                    **Example:**

                    .. code-block:: bash

                         ELEMENT                MAX_EXPECTED    OBTAINED
                        ----------------------  --------------  ----------
                        High vulnerabilities         0             1
                        Medium vulnerabilities       0             2
                        Low vulnerabilities          0             5
                        Low behaviorals              0             1

               * If not, it would print the maximum riskscore defined by the user and the obtained riskscore.


        """

        expected = self.exceeded_limit["expected"]
        obtained = self.exceeded_limit["obtained"]

        to_print = [["ELEMENT", "MAX_EXPECTED", "OBTAINED"]]

        if type(obtained) == dict and type(expected) == dict:
            for key in obtained.keys():
                for risk_key, value in obtained[key].items():
                    to_print.append(
                        ['{risk_key} {key}'.format(risk_key=risk_key, key=str(key)).capitalize(),
                         int(expected[key][risk_key]),
                         value
                         ]
                    )

            return (tabulate(to_print, headers='firstrow', stralign='center'))
        elif type(obtained) != dict and type(expected) != dict:
            to_print.append(['risk', expected, obtained])
            return (tabulate(to_print, headers='firstrow', stralign='center'))
        else:
            raise TypeError("Error in the expected and obtained values type")


    def _print_details(self, mode, max_values=None):
        """

        :param mode:
        :return:
        """

        v_to_print = [[]]

        for category in self.scan_result['vulnerabilities'].keys():
            for element in self.scan_result['vulnerabilities'][category]:
                v_to_print.append(['Title', element['title']])
                v_to_print.append(['Risk', element['riskLevel']])
                v_to_print.append(['nOcurrences', element['count']])
                v_to_print.append(['Recommendation', element['recommendation']])
                v_to_print.append(['Ocurrences:', ""])

                for occurrence in element['result']:
                    occurrence_path = ""

                    for ocurrence_path_element in occurrence['source'][0]:
                        occurrence_path += ocurrence_path_element + " > "

                    v_to_print.append(['>>>> Source', occurrence_path[:-2]])
                    v_to_print.append(['>>>> Evidence', occurrence['value']])
                    v_to_print.append([' ', ' '])

                v_to_print.append([' ', ' '])
                v_to_print.append([' ', ' '])

        print("VULNERABILITIES DETECTED")
        print(tabulate(v_to_print, stralign='left', tablefmt='plain'))

        b_to_print = [[]]

        for category in self.scan_result['behaviorals'].keys():
            for element in self.scan_result['behaviorals'][category]:
                b_to_print.append(['Title', element['title']])
                b_to_print.append(['Ocurrences', element['count']])
                b_to_print.append(['Impact', element['impact']])

                for occurrence in element['result']:
                    occurrence_path = ""

                    for ocurrence_path_element in occurrence['source'][0]:
                        occurrence_path += ocurrence_path_element + " > "

                    b_to_print.append(['>>>> Source', occurrence_path[:-2]])
                    b_to_print.append(['>>>> Evidence', occurrence['value']])
                    b_to_print.append([' ', ' '])

                b_to_print.append([' ', ' '])
                b_to_print.append([' ', ' '])

        print("BEHAVIORS DETECTED")
        print(tabulate(b_to_print, stralign='left', tablefmt='plain'))

        vulnerabilities = self.scan_result['vulnerabilities']
        v_critical = len(vulnerabilities['critical'])
        v_high = len(vulnerabilities['high'])
        v_medium = len(vulnerabilities['medium'])
        v_low = len(vulnerabilities['low'])

        behaviorals = self.scan_result['behaviorals']
        b_critical = len(behaviorals['critical'])
        b_high = len(behaviorals['high'])
        b_medium = len(behaviorals['medium'])
        b_low = len(behaviorals['low'])

        if mode == 'riskscoring':
            nvulns_to_print = [['Risk category', 'Obtained']]
            nvulns_to_print.append(['Critical', v_critical])
            nvulns_to_print.append(['High', v_high])
            nvulns_to_print.append(['Medium', v_medium])
            nvulns_to_print.append(['Low', v_low])
            print(u'\n\nVulnerabilities')
            print(tabulate(nvulns_to_print, headers='firstrow', stralign='medium', tablefmt='simple'))

            nbehav_to_print = [['Risk category', 'Obtained']]
            nbehav_to_print.append(['Critical', b_critical])
            nbehav_to_print.append(['High', b_high])
            nbehav_to_print.append(['Medium', b_medium])
            nbehav_to_print.append(['Low', b_low])
            print(u'\n\nBehaviors')
            print(tabulate(nbehav_to_print, headers='firstrow', stralign='medium', tablefmt='simple'))


        elif mode == 'standard' and max_values is not None:
            nvulns_to_print = [['Risk category', 'Expected', 'Obtained']]
            nvulns_to_print.append(['Critical', max_values['vulnerabilities']['critical'], v_critical])
            nvulns_to_print.append(['High', max_values['vulnerabilities']['high'], v_high])
            nvulns_to_print.append(['Medium', max_values['vulnerabilities']['medium'], v_medium])
            nvulns_to_print.append(['Low', max_values['vulnerabilities']['low'], v_low])
            print(u'\n\nVulnerabilities')
            print(tabulate(nvulns_to_print, headers='firstrow', stralign='center', tablefmt='simple'))

            nbehav_to_print = [['Risk category', 'Expected', 'Obtained']]
            nbehav_to_print.append(['Critical', max_values['behaviorals']['critical'], b_critical])
            nbehav_to_print.append(['High', max_values['behaviorals']['high'], b_high])
            nbehav_to_print.append(['Medium', max_values['behaviorals']['medium'], b_medium])
            nbehav_to_print.append(['Low', max_values['behaviorals']['low'], b_low])
            print(u'\n\nBehaviors')
            print(tabulate(nbehav_to_print, headers='firstrow', stralign='center', tablefmt='simple'))


    def _lower_than_scan_result(self, element, key, max_expected_value):
        """

        :param element:          The element to check from the scan_result dict
        :type  element:          "vulnerabilities"/"behaviorals"/"behaviors"
        :param key:              The key from scan_result[element][key] for checking the value of the already done scan
        :type  key:              String
        :param expected_value:   The value to compare with scan_result[element][key].
        :type  expected_value:   String
        :return:                 It returns True if the given value max_expected_value is  greater or equal than the
                                 number of vulnerabilities or behaviors (depending on the value of "element") for a
                                 criticality level given by the param "key".


        """
        possible_element_values = ["vulnerabilities", "behaviorals", "behaviors"]
        possible_key_values = ["critical", "high", "medium", "low"]

        if element not in possible_element_values:
            raise ValueError("Element must be 'vulnerabilities', 'behaviorals' or 'behaviors'")

        if key not in possible_key_values:
            raise ValueError("Element must be 'critical', 'high', 'medium', 'low'")

        if element == "behaviors":
            element = "behaviorals"

        if not max_expected_value == "":
            return int(max_expected_value) >= len(self.scan_result[element][key])

        return True


    def _check_not_api_error(self, api_response):
        """

        :param api_response:  An API response that contains the fields "_status" and "_body".
        :type  api_response:  Dictionary
        :return:              Some of the errors from mASAPPs API are shown in the response body, so this method\
                              checks that the status and the body don't contains any error code.
        """

        if api_response is None:
            raise TypeError("ERROR API Response is None")

        if api_response == "":
            raise ValueError("ERROR API Response is empty")

        if api_response == json.loads("{}"):
            raise ValueError("ERROR API Response dict is empty")

        if api_response._status is not None:
            if api_response._status != 200:
                raise ValueError("ERROR in API response: status is {0}".format(api_response._status))

        if api_response._body is not None:
            if 'error' in json.loads(api_response._body):
                raise ValueError("ERROR in API response: body is {0}".format(api_response._body))

        return True


    def store_workgroup(self, wg_name):
        """

        :param wg_name: The name of the workgroup that the user wants to use in the scan.
        :type  wg_name: String
        :return:        It returns the workgroup in the position given in wg_number.

        """
        wg = self.auth_user.get_auth_workgroup()
        self._check_not_api_error(wg)
        workgroups_names = []

        for workgroup in wg.data['data']['workgroups']:
            workgroups_names.append(workgroup['name'])
            if workgroup['name'] == wg_name:
                self.scan_info['wg'] = workgroup['workgroupId']
                return True
        raise ValueError(
            "[X] Workgroup not found \n [!] Workgroups where you belong to: \n {}".format(workgroups_names))



    def upload_app(self, app_path):
        """

        :param app_path: The absolute path to the application which the user wants to upload.
        :type  app_path: String
        :return:         It makes a check that would throw an error if there is any error in the api response.

        """
        filePath = os.path.abspath(app_path)

        if self.scan_info["wg"] is None:
            api_response = self.auth_user.post_auth_upload_app(allowTacyt="false", app_path=filePath)
        else:
            api_response = self.auth_user.post_auth_upload_app(allowTacyt="false", app_path=filePath,
                                                               workgroup=self.scan_info["wg"])
        self._check_not_api_error(api_response)
        self.scan_info['hashPath'] = api_response.data['data']['result']



    def store_scan_info_from_package_name_origin(self, package_name_origin):
        """

        :param package_name_origin: The packageNameOrigin that mASAPP gives to the app.
        :type  package_name_origin: String
        :return:                    It obtains all the scans from the user with the given credentials and look for the
                                    scan with the same packageNameOrigin than the given as package_name_origin and store
                                    the scanId and scanDate in scan_info. It could throw errors due to: an error in the
                                    API request (/scans) or if there is not any scan with the given packageNameOrigin.

        """

        if self.scan_info["wg"] is None:
            user_scans = self.auth_user.get_auth_scans()
        else:
            user_scans = self.auth_user.get_auth_scans(self.scan_info["wg"])

        self._check_not_api_error(user_scans)
        for scan in user_scans.data['data']['scans']:
            if scan['packageNameOrigin'] == package_name_origin:
                self.scan_info['scanId'] = scan['scanId']
                self.scan_info['scanDate'] = scan['lastScanDate']
                self.scan_info['packageNameOrigin'] = scan['packageNameOrigin']
                return True
        raise ValueError("Application {package_name_origin} not found".format(package_name_origin=package_name_origin))


    def store_scan_info_from_app_hashPath(self, app_hasPath):
        """

        :param app_hasPath: The sha1 of the application to analyse
        :type  app_hasPath: String
        :return:         It obtains all the scans from the user with the given credentials and look for the
                         scan whose hashPath is equal to the given one.

        """
        time.sleep(10)  # The upload and analysis process is asynchronous so it will wait for the analysis

        if self.scan_info["wg"] is None:
            user_scans = self.auth_user.get_auth_scan_by_hashPath(app_hasPath)
        else:
            user_scans = self.auth_user.get_auth_scan_by_hashPath(app_hasPath, self.scan_info["wg"])

        self._check_not_api_error(user_scans)

        result_scans = user_scans.data['data']['scans']

        if len(result_scans) > 1:
            print(
                '[X] There are {} scans with the same sha1 than your app! Choose the scan that you want to get from the following list: '.format(
                    len(result_scans)))
            for scan in result_scans:
                print(scan)
                print(" ")
            return False
        elif len(result_scans) == 1:
            self.scan_info['scanId'] = result_scans[0]['scanId']
            self.scan_info['scanDate'] = result_scans[0]['lastScanDate']
            self.scan_info['packageNameOrigin'] = result_scans[0]['packageNameOrigin']
            return True
        else:
            print('[X] Your scan has not been uploaded to mASAPP')
            return False


    def store_scan_summary_from_scan_id(self, scan_id):
        """

        :param scan_id: The scanId of the scan that the user wants to analyse
        :type  scan_id: String
        :return:        If the scanId is found, it stores the appKey, which is the last necessary field for getting the
                        scan result.

        """

        if self.scan_info["wg"] is None:
            user_scan_summary = self.auth_user.get_scan_summary(scan_id=scan_id)
        else:
            user_scan_summary = self.auth_user.get_scan_summary(scan_id=scan_id, workgroup=self.scan_info["wg"])

        self._check_not_api_error(user_scan_summary)
        for scan_summary in user_scan_summary.data['data']['scanSummaries']:
            if scan_summary['scanDate'] == self.scan_info['scanDate']:
                if len(scan_summary['scannedVersions']) != 0:
                    self.scan_info['appKey'] = scan_summary['scannedVersions'][0]['appKey']
                    return True
        return False


    def store_scan_result(self):
        """

        :return: It get the scan result and store it in "scan_result". The info that is stored is:

                * riskScore

                * Vulnerabilities

                    * Critical
                    * High
                    * Medium
                    * Low

                * Behaviors

                    * Critical
                    * High
                    * Medium
                    * Low

        """

        if self.scan_info['lang'].lower() not in self.LANGUAGES:
            raise ValueError(
                "Language {language} Only supported languages: en , es".format(language=self.scan_info['lang']))

        if self.scan_info["wg"] is None:
            scan_result = self.auth_user.get_scan_result(scan_id=self.scan_info['scanId'],
                                                         scan_date=self.scan_info['scanDate'],
                                                         app_key=self.scan_info['appKey'], lang=self.scan_info['lang'])
        else:
            scan_result = self.auth_user.get_scan_result(scan_id=self.scan_info['scanId'],
                                                         scan_date=self.scan_info['scanDate'],
                                                         app_key=self.scan_info['appKey'], lang=self.scan_info['lang'],
                                                         workgroup=self.scan_info['wg'])

        self._check_not_api_error(scan_result)

        self.scan_result['riskScore'] = scan_result.data['data']['riskScore']

        for vulnerability in scan_result.data['data']['vulnerabilities']:
            risk = vulnerability['riskLevel'].lower()
            if risk in self.scan_result['vulnerabilities'].keys():
                self.scan_result['vulnerabilities'][risk].append(vulnerability)

        for behavioral in scan_result.data['data']['behaviorals']:
            risk = behavioral['riskLevel'].lower()
            if risk in self.scan_result['behaviorals'].keys():
                self.scan_result['behaviorals'][risk].append(behavioral)


    def upload_and_analyse_app(self, app_path, package_name_origin=None, workgroup=None, lang=None):
        """

        :param app_path:            The absolute path to the application which the user wants to upload.
        :type  app_path:            String
        :param package_name_origin: The packageNameOrigin that mASAPP gives to the app.
        :type  package_name_origin: String
        :param workgroup:           The name of the workgroup that the user wants to use in the scan.
        :type  workgroup:           Integer
        :param lang:                The language in which the user wants to get the analysis result.
        :type  lang:                "en", "es"
        :return:                    It store the app scan result in "scan_result". The process that it follow is:

                                        1. It store the workgroup in the position given (or the first workgroup by default)
                                        2. Application uploading to mASAPP
                                        3. Storing of the scan info looking for it in the user scans
                                        4. Language setting
                                        5. Scan summary storing
                                        6. Storing the scan result using the info stored in steps 1,3,5 for making a \
                                        request to mASAPP API



        """

        if workgroup is not None:
            self.store_workgroup(workgroup)

        retries = 0
        scan_found = False

        while retries < 5 and not scan_found:
            retries += 1
            print("[!] Uploading and analysing the app to mASAPP - retry:{}".format(retries))
            self.upload_app(app_path)
            time.sleep(10)

            if package_name_origin != None:
                self.store_scan_info_from_package_name_origin(package_name_origin)

            else:
                app_hasPath = self.scan_info['hashPath']
                if self.store_scan_info_from_app_hashPath(app_hasPath):
                    self.scan_info['lang'] = lang or 'en'
                    if self.store_scan_summary_from_scan_id(self.scan_info['scanId']):
                        scan_found = True

        if not scan_found:
            raise ValueError(
                "[X] There is an error in mASAPP and your application hasn't been successfully processed yet")
        self.store_scan_result()


    def _print_link_to_app(self):
        try:
            scan_link = self.MASAPP_LINK + "scans/" + self.scan_info['packageNameOrigin']
            print("[!] Link of your scan in mASAPP: {} \n".format(scan_link))
        except:
            pass


    def riskscoring_execution(self, maximum_riskscoring, app_path, package_name_origin=None, workgroup=None, lang=None,
                              detail=None):
        """

        :param maximum_riskscoring: The maximum risk score allowed without throing an error.
        :type  maximum_riskscoring: Float
        :param app_path:            The absolute path to the application which the user wants to upload.
        :type  app_path:            String
        :param package_name_origin: The packageNameOrigin that mASAPP gave to the app. If is the first uploading of the\
                                    app, don't add this parameter.
        :type  package_name_origin: String
        :param workgroup:           The name of the workgroup that the user wants to use in the scan.
        :type  workgroup:           Integer
        :param lang:                The language in which the user wants to get the analysis result.
        :type  lang:                "en", "es"
        :param detail:              If the user wants a detailed execution or not.
        :type  detail:              Boolean
        :return:
                                        * If package_name_origin is sent, it returns an static analysis from the app\
                                        code showing the following information:

                                            * RISKSCORING SUCCESS or RISKSCORING ERROR depending on whether the\
                                             obtained riskscore surpass the maximum given as maximum_riskscoring.

                                                * The standard error includes a table with all the defined limits surpassed

                                            * If detail is equal to True, it will add below two lists:

                                                * A list of vulnerabilities, adding the tittle, risk, number of\
                                                occurrences and the different occurrences with their evidences.

                                                * A list of behaviors, adding the tittle, number of occurrences, impact\
                                                and the different occurrences with their evidences.


                                        * If the package_name_origin is not sent, the script will search in the users\
                                        scans for the correct package_name_origin using the package name.

                                            * If it is correctly found, the app would be analysed

                                            * If not, it would throw an error asking the user for the packageNameOrigin.\
                                            In order to facilitate the user to find the packageNameOrigin it would throw\
                                            a list of all the user scans.

                                            The **packageNameOrigin error text** is exactly: *Sometimes the\
                                            packageNameOrigin is not correctly generated by mASAPP and the application\
                                            is saved without it, so, please add the packageNameOrigin of your\
                                            application with the param -p*


        """
        if workgroup == None:
            self.upload_and_analyse_app(app_path=app_path, package_name_origin=package_name_origin, lang=lang)
        else:
            self.upload_and_analyse_app(app_path=app_path, package_name_origin=package_name_origin, workgroup=workgroup,
                                        lang=lang)

        correct_execution = True

        self._print_link_to_app()

        if self.scan_result['riskScore'] < maximum_riskscoring:
            print("---- RISKSCORING SUCCESS ----\n")
        else:
            self.exceeded_limit["expected"] = maximum_riskscoring
            self.exceeded_limit["obtained"] = self.scan_result['riskScore']
            correct_execution = False

        if detail == True:
            self._print_details('riskscoring')

        if not correct_execution:
            raise ValueError("---- RISKSCORING ERROR ----\n {excess}".format(excess=self._print_excess()))


    def standard_execution(self, scan_maximum_values, app_path, package_name_origin=None, workgroup=None, lang=None,
                           detail=None):
        """

        :param scan_maximum_values: Maximum results allowed without throwing an error.

            **Example**:

             .. code-block:: json

                {
                    "vulnerabilities":
                        {
                            "critical":"maximum of critical vulnerabilities",
                            "high":"maximum of high vulnerabilities",
                            "medium":"maximum of medium vulnerabilities",
                            "low":"maximum of low vulnerabilities"

                        },
                    "behaviorals":
                        {
                            "critical":"maximum of critical behaviors",
                            "high":"maximum of high behaviors",
                            "medium":"maximum of medium behaviors",
                            "low":"maximum of low behaviors"
                        }
                }

        :type  scan_maximum_values: Dictionary
        :param app_path:            The absolute path to the application which the user wants to upload.
        :type  app_path:            String
        :param package_name_origin: The packageNameOrigin that mASAPP gave to the app. If is the first uploading of the\
                                    app, don't add this parameter.
        :type  package_name_origin: String
        :param workgroup:           The name of the workgroup that the user wants to use in the scan.
        :type  workgroup:           Integer
        :param lang:                The language in which the user wants to get the analysis result.
        :type  lang:                "en", "es"
        :param detail:              If the user wants a detailed execution or not.
        :type  detail:              Boolean
        :return:
                                        * If package_name_origin is sent, it returns an static analysis from the app\
                                        code showing the following information:

                                            * STANDARD SUCCESS or STANDARD ERROR depending on whether or not there are\
                                            elements that exceed the limits contained in the scan_maximum_values json.

                                                * The standard error includes a table with all the defined limits surpassed

                                            * If detail is equal to True, it will add below two lists:

                                                * A list of vulnerabilities, adding the tittle, risk, number of\
                                                occurrences and the different occurrences with their evidences
                                                * A list of behaviors, adding the tittle, number of occurrences, impact\
                                                and the different occurrences with their evidences


                                        * If the package_name_origin is not sent, the script will search in the users\
                                        scans for the correct package_name_origin using the package name.

                                            * If it is correctly found, the app would be analysed

                                            * If not, it would throw an error asking the user for the packageNameOrigin.\
                                            In order to facilitate the user to find the packageNameOrigin it would throw\
                                            a list of all the user scans.

                                            The **packageNameOrigin error text** is exactly: *Sometimes mASAPP\
                                            can not generate all the necessary fields for unequivocally automatic \
                                            finding of the application, so, please add the packageNameOrigin of your\
                                            application with the param -p*

        """

        if workgroup == None:
            self.upload_and_analyse_app(app_path=app_path, package_name_origin=package_name_origin, lang=lang)
        else:
            self.upload_and_analyse_app(app_path=app_path, package_name_origin=package_name_origin, workgroup=workgroup,
                                        lang=lang)

        self.exceeded_limit["expected"] = {"vulnerabilities": {}, "behaviorals": {}}
        self.exceeded_limit["obtained"] = {"vulnerabilities": {}, "behaviorals": {}}
        correct_execution = True

        for key, value in scan_maximum_values['vulnerabilities'].items():
            if not self._lower_than_scan_result('vulnerabilities', key, value):
                self.exceeded_limit["expected"]['vulnerabilities'][key] = value
                self.exceeded_limit["obtained"]['vulnerabilities'][key] = len(self.scan_result['vulnerabilities'][key])
                correct_execution = False

        for key, value in scan_maximum_values['behaviorals'].items():
            if not self._lower_than_scan_result('behaviorals', key, value):
                self.exceeded_limit["expected"]['behaviorals'][key] = value
                self.exceeded_limit["obtained"]['behaviorals'][key] = len(self.scan_result['behaviorals'][key])
                correct_execution = False

        if detail == True:
            self._print_details('standard', max_values=scan_maximum_values)

        self._print_link_to_app()

        if not correct_execution:
            raise ValueError("---- STANDARD ERROR ----\n {excess}".format(excess=self._print_excess()))
        else:
            print("---- STANDARD SUCCESS ----")
