# By: Riasat Ullah
# This file contains variables and functions for assisting Jira integration processes.

from dbqueries.integrations import db_jira
from requests.auth import HTTPBasicAuth
from taskcallrest import settings
from utils import logging, url_paths, var_names
import json
import requests


# Jira standard request headers
standard_headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}

# url paths
url_add_comment = '{0}/rest/api/2/issue/{1}/comment'
url_create_issue = '{0}/rest/api/2/issue'
url_edit_issue = '{0}/rest/api/2/issue/{1}'
url_fields = '{0}/rest/api/2/field'
url_issue_types = '{0}/rest/api/2/issuetype'
url_projects_search = '{0}/rest/api/2/issue/createmeta'
url_status_list = '{0}/rest/api/2/status'
url_transitions = '{0}/rest/api/2/issue/{1}/transitions'
url_webhook = '{0}/rest/webhooks/1.0/webhook'

# taskcall incoming url
tc_jira_incoming_url = settings.INTEGRATIONS_API_BASE_URL + '/jira/server/issue/{0}'


def jira_server_get_request(url, username, password):
    '''
    Makes a GET request to the api and returns the response.
    :param url: api url
    :param username: admin username
    :param password: admin password
    :return: [status code, response]
    '''
    try:
        response = requests.get(url, headers=standard_headers, auth=HTTPBasicAuth(username, password))
        return [response.status_code, response.json()]
    except (ConnectionRefusedError, ConnectionError) as e:
        logging.exception('Failed to connect with Jira Server')
        logging.exception(str(e))
        raise ConnectionRefusedError
    except Exception as e:
        logging.exception('Jira Server API request failed...')
        logging.exception(str(e))
        raise ConnectionRefusedError


def jira_server_post_request(url, username, password, body, status_only=False):
    '''
    Makes a POST request to the api and returns the response.
    :param url: api url
    :param username: admin username
    :param password: admin password
    :param body: the JSON body to send with the request
    :param status_only: True if the json body of the response is not needed
    :return: [status code, response]
    '''
    try:
        response = requests.post(url, headers=standard_headers, auth=HTTPBasicAuth(username, password),
                                 data=json.dumps(body))
        if status_only:
            return response.status_code
        else:
            return [response.status_code, response.json()]
    except (ConnectionRefusedError, ConnectionError) as e:
        logging.exception('Failed to connect with Jira Server')
        logging.exception(str(e))
        raise ConnectionRefusedError
    except Exception as e:
        logging.exception('Jira Server API request failed...')
        logging.exception(str(e))
        raise ConnectionRefusedError


def jira_server_delete_request(url, username, password, body):
    '''
    Makes a DELETE request to the api and returns the response status.
    :param url: api url
    :param username: admin username
    :param password: admin password
    :param body: the JSON body to send with the request
    :return: [status code, response]
    '''
    try:
        response = requests.delete(url, headers=standard_headers, auth=HTTPBasicAuth(username, password),
                                   data=json.dumps(body))
        return response.status_code
    except (ConnectionRefusedError, ConnectionError) as e:
        logging.exception('Failed to connect with Jira Server')
        logging.exception(str(e))
        raise
    except Exception as e:
        logging.exception('Jira Server API request failed...')
        logging.exception(str(e))
        raise ConnectionRefusedError


def jira_server_put_request(url, username, password, body):
    '''
    Makes a PUT request to the api and returns the response status.
    :param url: api url
    :param username: admin username
    :param password: admin password
    :param body: the JSON body to send with the request
    :return: [status code, response]
    '''
    try:
        response = requests.put(url, headers=standard_headers, auth=HTTPBasicAuth(username, password),
                                data=json.dumps(body))
        return response.status_code
    except (ConnectionRefusedError, ConnectionError) as e:
        logging.exception('Failed to connect with Jira Server')
        logging.exception(str(e))
        raise ConnectionRefusedError
    except Exception as e:
        logging.exception('Jira Server API request failed...')
        logging.exception(str(e))
        raise ConnectionRefusedError


def create_jira_issue(conn, timestamp, org_id, integ_key, integ_info, org_instance_id, task_title, text_msg,
                      instance_state, urgency):
    '''
    Creates a Jira issue.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param integ_key: the integration key of this Jira integration
    :param integ_info: additional info of this integration
    :param org_instance_id: organization instance ID
    :param task_title: instance task title that will be used as the summary of the issue
    :param text_msg: instance text details that will be used as the description of the issue
    :param instance_state: the current state of the instance
    :param urgency: the urgency level of the instance
    :return: (str) -> issue key (None if issue cannot be created)
    '''
    jira_integ_det = db_jira.get_jira_server_host_details(conn, timestamp, org_id, integration_key=integ_key)
    srv_host = jira_integ_det[var_names.vendor_endpoint]
    srv_admin = jira_integ_det[var_names.username]
    srv_pwd = jira_integ_det[var_names.password]

    project_id = integ_info[var_names.jira_project_id]
    issue_type_id = integ_info[var_names.issue_type]
    issue_type_hierarchy = integ_info[var_names.issue_type_hierarchy]
    issue_status_id = integ_info[var_names.status][instance_state]
    priority_id = integ_info[var_names.urgency_level][str(urgency)]

    inc_url = url_paths.web_incidents_details + '/' + str(org_instance_id)
    desc = 'TaskCall URL: [' + inc_url + ']'
    if text_msg is not None:
        desc += ' \n\n' + text_msg
    issue_body = {
        'fields': {
            'project': {'id': project_id},
            'issuetype': {'id': issue_type_id},
            'priority': {'id': priority_id},
            'summary': task_title,
            'description': desc
        }
    }
    if issue_type_hierarchy == 1:
        epic_field_name = get_epic_field_name(srv_host, srv_admin, srv_pwd)
        if epic_field_name is not None:
            issue_body['fields'][epic_field_name] = task_title

    status, output = jira_server_post_request(url_create_issue.format(srv_host), srv_admin, srv_pwd, issue_body)
    issue_key = None
    if status in [200, 201]:
        issue_key = output['key']
        update_status_url = url_transitions.format(srv_host, issue_key)
        tr_sts, tr_output = jira_server_get_request(update_status_url, srv_admin, srv_pwd)
        if tr_sts == 200:
            matches = [[x['id'], x['name']] for x in tr_output['transitions'] if x['to']['id'] == issue_status_id]
            if len(matches) > 0:
                transition_req_body = {"transition": {"id": matches[0][0]}}
                tr_2_sts = jira_server_post_request(update_status_url, srv_admin, srv_pwd, transition_req_body,
                                                    status_only=True)
                if tr_2_sts not in [200, 204]:
                    logging.error('Failed - Jira transition to status - ' + matches[0][1])
        else:
            logging.error('Jira issue transition error (' + str(tr_sts) + ') - ' + str(tr_output))
    else:
        logging.error('Jira issue creation error (' + str(status) + ') - ' + str(output))

    return issue_key, status, output


def add_jira_comment(conn, timestamp, org_id, integ_key, integ_info, issue_key, comment):
    '''
    Get the body of the request to add a comment to a Jira issue.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param integ_key: the integration key of this Jira integration
    :param integ_info: additional info of this integration
    :param issue_key: Jira issue key
    :param comment: (str) the comment
    '''
    if integ_info[var_names.to_sync_notes]:
        jira_integ_det = db_jira.get_jira_server_host_details(conn, timestamp, org_id, integration_key=integ_key)
        srv_host = jira_integ_det[var_names.vendor_endpoint]
        srv_admin = jira_integ_det[var_names.username]
        srv_pwd = jira_integ_det[var_names.password]

        comment_body = {'body': comment}
        jira_server_post_request(url_add_comment.format(srv_host, issue_key), srv_admin, srv_pwd, comment_body)


def create_jira_webhook(conn, timestamp, org_id, integration_url, jql, integ_key=None, srv_host=None, srv_admin=None,
                        srv_pwd=None, webhook_to_del=None):
    '''
    Creates new a Jira webhook.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param integration_url: the incoming integration url created by TaskCall
    :param jql: the JQL based on which the webhook will be triggered when issues are created
    :param integ_key: the integration key of this Jira integration
    :param srv_host: address of the server
    :param srv_admin: admin username
    :param srv_pwd: password of the admin user
    :param webhook_to_del: ID of the old webhook to delete if new webhook is created; deletion is not done if none
    '''
    if integ_key is not None:
        jira_integ_det = db_jira.get_jira_server_host_details(conn, timestamp, org_id, integration_key=integ_key)
        srv_host = jira_integ_det[var_names.vendor_endpoint]
        srv_admin = jira_integ_det[var_names.username]
        srv_pwd = jira_integ_det[var_names.password]
    else:
        assert srv_host is not None

    web_url = url_webhook.format(srv_host)
    web_req_body = {
        "name": integration_url,
        "url": integration_url,
        "filters": {"issue-related-events-section": jql},
        "events": ["jira:issue_created"],
    }

    jira_status, jira_output = jira_server_post_request(web_url, srv_admin, srv_pwd, web_req_body)
    webhook_id = None
    if jira_status in [200, 201]:
        webhook_id = jira_output['self'].split('/')[-1]
        if webhook_to_del is not None:
            web_url = web_url + '/' + webhook_to_del
            jira_server_delete_request(web_url, srv_admin, srv_pwd, dict())

    return webhook_id


def delete_jira_webhook(conn, timestamp, org_id, integ_key, webhook_id):
    '''
    Deletes a Jira webhook.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param integ_key: the integration key of this Jira integration
    :param webhook_id: ID of the webhook to delete
    '''
    jira_integ_det = db_jira.get_jira_server_host_details(conn, timestamp, org_id, integration_key=integ_key)
    srv_host = jira_integ_det[var_names.vendor_endpoint]
    srv_admin = jira_integ_det[var_names.username]
    srv_pwd = jira_integ_det[var_names.password]

    web_url = url_webhook.format(srv_host) + '/' + webhook_id
    jira_server_delete_request(web_url, srv_admin, srv_pwd, dict())


def update_jira_status(conn, timestamp, org_id, integ_key, integ_info, instance_state, issue_key):
    '''
    Update the status of a Jira issue.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param integ_key: the integration key of this Jira integration
    :param integ_info: additional info of this integration
    :param instance_state: the current state of the instance
    :param issue_key: Jira issue key
    '''
    jira_integ_det = db_jira.get_jira_server_host_details(conn, timestamp, org_id, integration_key=integ_key)
    srv_host = jira_integ_det[var_names.vendor_endpoint]
    srv_admin = jira_integ_det[var_names.username]
    srv_pwd = jira_integ_det[var_names.password]

    issue_status_id = integ_info[var_names.status][instance_state]
    update_status_url = url_transitions.format(srv_host, issue_key)
    tr_sts, tr_output = jira_server_get_request(update_status_url, srv_admin, srv_pwd)
    if tr_sts == 200:
        matches = [[x['id'], x['name']] for x in tr_output['transitions'] if x['to']['id'] == issue_status_id]
        if len(matches) > 0:
            transition_req_body = {"transition": {"id": matches[0][0]}}
            tr_2_sts = jira_server_post_request(update_status_url, srv_admin, srv_pwd, transition_req_body,
                                                status_only=True)
            if tr_2_sts not in [200, 204]:
                logging.error('Failed - Jira ' + issue_key + ' issue transition to status - ' + matches[0][1])


def update_jira_priority(conn, timestamp, org_id, integ_key, integ_info, urgency, issue_key):
    '''
    Update the status of a Jira issue.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param integ_key: the integration key of this Jira integration
    :param integ_info: additional info of this integration
    :param urgency: TaskCall urgency
    :param issue_key: Jira issue key
    '''
    jira_integ_det = db_jira.get_jira_server_host_details(conn, timestamp, org_id, integration_key=integ_key)
    srv_host = jira_integ_det[var_names.vendor_endpoint]
    srv_admin = jira_integ_det[var_names.username]
    srv_pwd = jira_integ_det[var_names.password]

    priority_id = integ_info[var_names.urgency_level][str(urgency)]
    edit_priority_url = url_edit_issue.format(srv_host, issue_key)
    edit_body = {
        "update": {
            "priority": [
                {
                    "set": {
                        "id": priority_id
                    }
                }
            ]
        }
    }
    status = jira_server_put_request(edit_priority_url, srv_admin, srv_pwd, edit_body)
    if status not in [200, 204]:
        logging.error('Failed - Jira ' + issue_key + ' issue priority update - ' + priority_id)


def get_epic_field_name(server_host, username, password):
    '''
    Get the name of the field that has to be included in the payload to create an Epic.
    :param server_host: address of the server host
    :param username: admin username
    :param password: admin password
    :return: (str) -> name of the field
    '''
    url = url_fields.format(server_host)
    status, output = jira_server_get_request(url, username, password)
    field_name = None
    if status == 200:
        for item in output:
            if 'untranslatedName' in item and item['untranslatedName'].lower() == 'epic link':
                field_name = item['id']
                break
    return field_name
