# (c) Copyright IBM Corp. 2022. All Rights Reserved.
"""ResilientHelper Module"""
import base64
import logging

import six
from bs4 import BeautifulSoup
from resilient import SimpleHTTPException

# Handle unicode in 2.x and 3.x
try:
    unicode
except NameError:
    unicode = str

CONFIG_DATA_SECTION = "fn_service_now"
SECOPS_TABLE_NAME = "sn_si_incident"
SECOPS_PLAYBOOK_TASK_TABLE_NAME = "sn_si_task"
SECOPS_PLAYBOOK_TASK_PREFIX = "SIT"
CP4S_CASES_REST_PREFIX = "cases-rest"

LOG = logging.getLogger(__name__)

# Define an Incident that gets sent to ServiceNow
class Incident(object):
    """Class that represents a Resilient Incident. See API notes for more"""
    def __init__(self, incident_id, incident_name, incident_description):
        self.type = "res_incident"
        self.incident_id = incident_id
        self.incident_name = incident_name
        self.incident_description = incident_description

    def as_dict(self):
        """Returns this class object as a dictionary"""
        return self.__dict__


# Define a Task that gets sent to ServiceNow
class Task(object):
    """Class that represents a Resilient Task. See API notes for more"""
    def __init__(self, incident_id, task_id, task_name, task_instructions):
        self.type = "res_task"
        self.incident_id = incident_id
        self.task_id = task_id
        self.task_name = task_name
        self.task_instructions = task_instructions

    def as_dict(self):
        """Returns this class as a Dictionary"""
        return self.__dict__


class ResilientHelper(object):
    """A helper class for sn_utilities"""
    def __init__(self, app_configs):

        log = logging.getLogger(__name__)
        log.debug("Initializing ResilientHelper")

        self.app_configs = app_configs

        self.SN_HOST = self.get_config_option("sn_host", placeholder="https://instance.service-now.com")
        self.SN_API_URI = self.get_config_option("sn_api_uri")

        # https://service-now-host.com/api/<app-name>/<custom-api-name/
        self.SN_API_URL = "{0}{1}".format(self.SN_HOST, self.SN_API_URI)
        self.SN_TABLE_NAME = str(self.get_config_option("sn_table_name"))
        self.SN_USERNAME = str(self.get_config_option("sn_username", placeholder="<ServiceNow Username>"))

        # Handle password surrounded by ' or "
        pwd = str(self.get_config_option("sn_password", placeholder="<ServiceNow Password>"))
        if (pwd.startswith("'") and pwd.endswith("'")) or (pwd.startswith('"') and pwd.endswith('"')):
            self.SN_PASSWORD = pwd[1:-1]
        else:
            self.SN_PASSWORD = pwd

        self.SN_AUTH = (self.SN_USERNAME, self.SN_PASSWORD)

        self.CP4S_PREFIX = self.get_config_option("cp4s_cases_prefix", placeholder=CP4S_CASES_REST_PREFIX, optional=True)

        # Default headers
        self.headers = {"Content-Type": "application/json", "Accept": "application/json"}

        log.debug("ResilientHelper initialized")
        log.debug("App Configs: sn_host: %s sn_api_uri: %s sn_api_url: %s sn_table_name: %s sn_username: %s cp4s_cases_prefix: %s",
            self.SN_HOST, self.SN_API_URI, self.SN_API_URL, self.SN_TABLE_NAME, self.SN_USERNAME, self.CP4S_PREFIX)

    @classmethod
    def _byteify(cls, data):
        """Function to handle json.loads object_hook for supporting Python 2 and 3"""

        if six.PY2:
            if isinstance(data, unicode):
                return data.encode("utf-8")

            elif isinstance(data, dict):
                return {cls._byteify(key): cls._byteify(value) for key, value in data.items()}

            return data

        elif six.PY3:
            data_as_utf8_str = data

            if isinstance(data, unicode):
                data_as_utf8_str = data.encode("utf-8")

                # if Python 3.x data_as_utf8_str will be bytes, so we convert back to str
                if isinstance(data_as_utf8_str, bytes):
                    data_as_utf8_str = data_as_utf8_str.decode(("utf-8"))

            elif isinstance(data, dict):
                data_as_utf8_str = {cls._byteify(key): cls._byteify(value) for key, value in data.items()}

            return data_as_utf8_str
        else:
            raise ValueError("We do not support this version of Python")

    @staticmethod
    def _encodeBase64(str_to_encode):
        """A helper function to encode a base64 string for Python 3 and 3 support"""
        if six.PY2:
            return base64.b64encode(str_to_encode)

        elif six.PY3:
            str_to_encode = base64.b64encode(str_to_encode)
            return str_to_encode.decode("utf-8")

        else:
            raise ValueError("We do not support this version of Python")

    @staticmethod
    def str_to_unicode(str_to_convert):
        if six.PY2:
            return unicode(str_to_convert)

        elif six.PY3:
            return str(str_to_convert)

        else:
            raise ValueError("We do not support this version of Python")

    def get_config_option(self, option_name, optional=False, placeholder=None):
        """Given option_name, checks if it is in appconfig. Raises ValueError if a mandatory option is missing"""
        option = self.app_configs.get(option_name, placeholder)
        err = "'{0}' is mandatory and is not set in app.config file. You must set this value to run this function".format(option_name)

        if not option and optional is False:
            raise ValueError(err)
        elif optional is False and placeholder is not None and option == placeholder:
            raise ValueError(err)
        else:
            return option

    @staticmethod
    def get_function_input(inputs, input_name, optional=False):
        """Given input_name, checks if it defined. Raises ValueError if a mandatory input is None"""

        log = logging.getLogger(__name__)
        log.debug("Trying to get function input: %s from %s. optional = %s", input_name, inputs, optional)

        the_input = inputs.get(input_name)

        if the_input is None and optional is False:
            err = "'{0}' is a mandatory function input".format(input_name)
            raise ValueError(err)
        else:
            log.debug("Got function input: %s", input_name)
            return the_input

    @staticmethod
    def generate_res_id(incident_id, task_id=None, sn_res_id=None):
        """If incident_id and task_id are valid, returns "RES-1001-2002"
        Else if task_id is None, returns "RES-1001". If sn_res_id if defined
        just return it. """

        # If sn_res_id is defined, just return it. This helps us with closing from Data Table
        if sn_res_id is not None:
            return sn_res_id

        res_id = ["RES", str(incident_id)]
        if task_id is not None:
            res_id.append(str(task_id))
        return "-".join(res_id)

    @staticmethod
    def parse_res_id(res_id):
        """Parse res_id (RES-1001-2002) and return a dict with incident_id and task_id"""
        incident_id, task_id = None, None
        ids = res_id.split("-")

        incident_id = ids[1]
        if len(ids) == 3:
            task_id = ids[2]

        return {"incident_id": incident_id, "task_id": task_id}

    def generate_res_link(self, incident_id, host, org_id, task_id=None):
        """Function that generates a https URL to the incident or task"""

        # for CP4S cases endpoint, remove the cases-rest prefix (CP4S_CASES_REST_PREFIX)
        if self.CP4S_PREFIX in host:
            base_host = host.replace(self.CP4S_PREFIX + ".", "")
            link = "https://{0}/app/respond/#cases/{1}".format(base_host, incident_id)
        else:
            link = "https://{0}/#incidents/{1}".format(host, incident_id)

        if task_id is not None:
            link += "?taskId={0}&tabName=details&orgId={1}".format(task_id, org_id)

        return link

    def generate_sn_link(self, sn_query, sn_table_name=None):
        """Function that generates a URL to the record in ServiceNow"""
        if not sn_table_name:
            sn_table_name = self.SN_TABLE_NAME

            # https://devxxxx.service-now.com/nav_to.do?uri=incident.do?sysparm_query=number=INC0000009
            uri = "{0}/nav_to.do?uri={1}.do?sysparm_query={2}".format(self.SN_HOST, sn_table_name, sn_query)
        return uri

    @staticmethod
    def convert_text_to_richtext(text, color="green"):
        """Converts text to richtext and adds a color"""

        colors = {
            "green": "#00b33c",
            "orange": "#ff9900",
            "yellow": "#e6e600",
            "red": "#e60000"
        }

        color = colors.get(color)

        return """<div style="color:{0}">{1}</div>""".format(color, text)

    @staticmethod
    def state_to_text(state):
        """Converts O/A to Active and C to Closed"""
        if state == "O" or state == "A":
            return "Active"
        elif state == "C":
            return "Closed"
        else:
            raise ValueError("{0} is not a valid Resilient State. O=Open Task, A=Active Incident, C=Closed Incident/Task".format(state))

    @staticmethod
    def get_incident(client, incident_id):
        """Function that gets the incident from Resilient"""

        log = logging.getLogger(__name__)
        err_msg = None
        get_url = "/incidents/{0}?text_content_output_format=always_text&handle_format=names".format(incident_id)

        # Get the incident from resilient api
        try:
            log.debug("GET Incident from Resilient: ID %s URL: %s", incident_id, get_url)
            incident = client.get(get_url)
            log.debug("Incident got successfully: %s", incident)
        except Exception as err:
            err_msg = "Error trying to get Incident {0}.".format(incident_id)

            if err.message and "not found" in err.message.lower():
                err_msg = "{0} Could not find Incident with ID {1}".format(err_msg, incident_id)
            elif isinstance(err, SimpleHTTPException):
                err_msg = "{0}\nServer Error.\nStatus Code: {1}\nURL: {2}\n{3}".format(err_msg, err.response.status_code, err.response.url, err.message)
            else:
                err_msg = "{0} {1}".format(err_msg, err)

            raise ValueError(err_msg)

        return Incident(incident_id, incident["name"], incident["description"])

    @staticmethod
    def rename_incident(client, incident_id, new_incident_name):
        """
        Static function to update the name of a incident.
        Primarily used for prepending sn_ref_id to the incident's name
        for easy filtering when viewing incident list
        """

        def change_func(data):
            data["name"] = new_incident_name

        url = "/incidents/{0}?text_content_output_format=always_text&handle_format=names".format(incident_id)

        # Use the get_put option to GET the data, apply the change, and PUT it back to the server
        try:
            LOG.debug("PUT Incident from Resilient: ID: %s URL: %s New Name: %s", incident_id, url, new_incident_name)
            client.get_put(url, change_func)
            LOG.info("Incident was successfully renamed to '%s'", new_incident_name)
        except Exception as err:
            raise ValueError(str(err))

    @staticmethod
    def get_task(client, task_id, incident_id):
        """Function that gets the task from Resilient. Gets the task's instructions too"""

        log = logging.getLogger(__name__)
        err_msg = None

        # Get the task from resilient api
        try:
            get_url = "/tasks/{0}?text_content_output_format=always_text&handle_format=names".format(task_id)
            log.debug("GET Task from Resilient: ID %s URL: %s", task_id, get_url)
            task = client.get(get_url)
            log.debug("Task got successfully: %s", task)
        except Exception as err:
            err_msg = "Error trying to get Task {0}.".format(task_id)

            if err.message and "not found" in err.message.lower():
                err_msg = "{0} Could not find Task with ID {1}".format(err_msg, task_id)
            elif isinstance(err, SimpleHTTPException):
                err_msg = "{0}\nServer Error.\nStatus Code: {1}\nURL: {2}\n{3}".format(err_msg, err.response.status_code, err.response.url, err.message)
            else:
                err_msg = "{0} {1}".format(err_msg, err)

            raise ValueError(err_msg)


        # Get the task_instructions in plaintext
        try:
            get_url = "/tasks/{0}/instructions_ex".format(task_id)
            log.debug("GET task_instructions for: ID %s URL: %s", task_id, get_url)
            task_instructions = client.get_content(get_url)
            soup = BeautifulSoup(unicode(task_instructions, "utf-8"), 'html.parser')
            soup = soup.get_text()
            # BeautifulSoup decoding of the HTML includes quotation marks and non-breaking spaces
            # so we need to remove those for the instructions text that will go to SNOW
            task_instructions = soup.replace(u'\xa0', u' ').replace(u'"',u'')
            log.debug("task_instructions got successfully")
        except Exception as err:
            err_msg = "Error trying to get task_instructions for Task {0}.".format(task_id)

            if isinstance(err, SimpleHTTPException):
                err_msg = "{0}\nServer Error.\nStatus Code: {1}\nURL: {2}\n{3}".format(err_msg, err.response.status_code, err.response.url, err.message)
            else:
                err_msg = "{0} {1}".format(err_msg, err)

            raise ValueError(err_msg)


        return Task(incident_id, task_id, task["name"], task_instructions)

    @staticmethod
    def rename_task(client, task_id, new_task_name):
        """
        Static function to update the name of a task.
        Primarily used for prepending sn_ref_id to the task's name
        for easy filtering when viewing task list
        """

        def change_func(data):
            data["name"] = new_task_name

        url = "/tasks/{0}?text_content_output_format=always_text&handle_format=names".format(task_id)

        # Use the get_put option to GET the data, apply the change, and PUT it back to the server
        try:
            LOG.debug("PUT Task from Resilient: ID: %s URL: %s New Name: %s", task_id, url, new_task_name)
            client.get_put(url, change_func)
            LOG.info("Task was successfully renamed to '%s'", new_task_name)
        except Exception as err:
            raise ValueError(str(err))


    @classmethod
    def get_attachment(cls, res_client, attachment_id, incident_id=None, task_id=None):
        """Function that gets incident/task attachment"""

        log = logging.getLogger(__name__)
        attachment = {"id": None, "name": None, "content_type": None, "contents": None}
        err_msg, get_url = None, None

        # Get attachment metadata url
        if task_id:
            get_url = "/tasks/{0}/attachments/{1}".format(task_id, attachment_id)
        elif incident_id:
            get_url = "/incidents/{0}/attachments/{1}".format(incident_id, attachment_id)
        else:
            raise ValueError("Failed to get_attachment. task_id or incident_id must be specified with attachment_id")

        # Get attachment metadata
        try:
            log.debug("GET Attachment metadata: ID %s URL: %s", attachment_id, get_url)
            meta_data = res_client.get(get_url)
            log.debug("Attachment metadata got successfully")
        except Exception as err:
            err_msg = "Error trying to get Attachment {0}.".format(attachment_id)

            if err.message and "not found" in err.message.lower():
                err_msg = "{0} Could not find Attachment with ID {1}. incident_id: {2} task_id: {3}".format(err_msg, attachment_id, incident_id, task_id)
            elif isinstance(err, SimpleHTTPException):
                err_msg = "{0}\nServer Error.\nStatus Code: {1}\nURL: {2}\n{3}".format(err_msg, err.response.status_code, err.response.url, err.message)
            else:
                err_msg = "{0} {1}".format(err_msg, err)

            raise ValueError(err_msg)

        attachment["content_type"] = meta_data["content_type"]
        attachment["id"] = attachment_id
        attachment["name"] = meta_data["name"]

        # Get attachment contencts url
        get_contents_url = "{0}/contents".format(get_url)

        # Get attachment contents
        try:
            log.debug("GET Attachment contents: ID %s URL: %s", attachment_id, get_url)
            attachment["contents"] = cls._encodeBase64(res_client.get_content(get_contents_url))
            log.debug("Attachment contents got successfully")
        except Exception as err:
            err_msg = "Error trying to get Attachment contents for ID: {0}.".format(attachment_id)

            if err.message and "not found" in err.message.lower():
                err_msg = "{0} Could not find Attachment with ID {1}. incident_id: {2} task_id: {3}".format(err_msg, attachment_id, incident_id, task_id)
            elif isinstance(err, SimpleHTTPException):
                err_msg = "{0}\nServer Error.\nStatus Code: {1}\nURL: {2}\n{3}".format(err_msg, err.response.status_code, err.response.url, err.message)
            else:
                err_msg = "{0} {1}".format(err_msg, err)

        return attachment

    @classmethod
    def generate_sn_request_data(cls, res_client, res_datatable, incident_id, sn_table_name, res_link, task_id=None, init_note=None, sn_optional_fields=None):
        """Function that generates the data that is sent in the request to the /create endpoint in ServiceNow"""

        log = logging.getLogger(__name__)
        err_msg, request_data = None, None

        log.debug("Generating request for ServiceNow. incident_id: %s task_id: %s sn_table_name: %s res_link: %s init_note: %s sn_optional_fields: %s res_datatable: %s",
            incident_id, task_id, sn_table_name, res_link, init_note, sn_optional_fields, res_datatable)

        # Generate the res_id
        res_id = cls.generate_res_id(incident_id, task_id)

        log.debug("res_id: {0}".format(res_id))

        # Check if already exists in data table
        sn_ref_id = res_datatable.get_sn_ref_id(res_id)

        if sn_ref_id:
            err_msg = "Failed to create a ServiceNow Record. This {0} already exists in ServiceNow. {0} ID: {1}. sn_ref_id: {2}"

            if task_id:
                err_msg = err_msg.format("Task", task_id, sn_ref_id)

            else:
                err_msg = err_msg.format("Incident", incident_id, sn_ref_id)

            return {
                "success": False,
                "data": err_msg
            }

        if task_id is not None:
            # Get the task
            task = cls.get_task(res_client, task_id, incident_id)

            # Add task to the request_data
            request_data = task.as_dict()

        else:
            incident = cls.get_incident(res_client, incident_id)

            # Add incident to the request_data
            request_data = incident.as_dict()

        # Add sn_table_name, resilient_id, link and init_note to the request_data
        request_data["sn_table_name"] = sn_table_name
        request_data["id"] = res_id
        request_data["link"] = res_link
        request_data["sn_init_work_note"] = init_note

        # Extend request_data if there is data in 'sn_optional_fields'
        if sn_optional_fields is not None and len(sn_optional_fields) > 0:
            fields = []
            for field in sn_optional_fields:
                fields.append({"name": field, "value": sn_optional_fields[field]})
            request_data["sn_optional_fields"] = fields
        else:
            request_data["sn_optional_fields"] = None

        log.debug("sn_request_data %s", request_data)

        return {
            "success": True,
            "data": request_data
        }

    def get_table_name(self, sn_ref_id):

        if self.SN_TABLE_NAME == SECOPS_TABLE_NAME and sn_ref_id.startswith(SECOPS_PLAYBOOK_TASK_PREFIX):
            return SECOPS_PLAYBOOK_TASK_TABLE_NAME
        
        return self.SN_TABLE_NAME

    def sn_api_request(self, rc, method, endpoint, params=None, data=None, headers=None):
        """Method to handle resquests to our custom APIs in ServiceNow"""
        log = logging.getLogger(__name__)

        res, return_value = None, None

        SUPPORTED_METHODS = ["GET", "POST", "PATCH"]

        if method not in SUPPORTED_METHODS:
            raise ValueError("{0} is not a supported ServiceNow API Request. Supported methods are: {1}".format(method, SUPPORTED_METHODS))

        headers = self.headers if headers is None else headers
        url = "{0}{1}".format(self.SN_API_URL, endpoint)

        res = rc.execute(
            method=method,
            url=url,
            auth=self.SN_AUTH,
            headers=headers,
            params=params,
            data=data
        )

        log.info("SN REQUEST:\nmethod: %s\nurl: %s\nbody: %s", res.request.method, res.request.url, res.request.body)

        if method is "GET":
            log.info("SN RESPONSE: %s", res)
            return_value = res

        elif method in ("POST", "PATCH"):
            log.info("SN RESPONSE: %s", res.json())
            return_value = res.json()["result"]

        return return_value
