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

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


# jira s3 credential file variables
jira_s3_bucket = 'taskcall-prod-data'
jira_s3_key = 'credentials/jira_credentials.json'

# variable names that are sent in jira payload
str_created_webhook_id = 'createdWebhookId'
str_errors = 'errors'
str_webhook_registration_result = 'webhookRegistrationResult'

# url paths
url_add_comment = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/issue/{1}/comment'
url_create_issue = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/issue'
url_edit_issue = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/issue/{1}'
url_fields = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/field'
url_issue_types = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/issuetype'
url_projects_search = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/project/search'
url_status_list = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/status'
url_token_retrieval = 'https://auth.atlassian.com/oauth/token'
url_transitions = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/issue/{1}/transitions'
url_webhook = 'https://api.atlassian.com/ex/jira/{0}/rest/api/3/webhook'

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


def jira_standard_header_params(access_token):
    '''
    Get the standard header params for a Jira request.
    :param access_token: the access token to be passed with the request
    :return: (dict) of header params
    '''
    return {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': "Bearer " + access_token
    }


def jira_get_api_request(conn, timestamp, organization_id, jira_cloud_id, url, access_token, refresh_token):
    '''
    Makes a GET request to the api and returns the response.
    :param conn: db connection
    :param timestamp: timestamp when this action is being taken
    :param organization_id: ID of the organization
    :param jira_cloud_id: Jira cloud ID of the account the request is being made for
    :param url: api url
    :param access_token: the token that can be used to verify the request
    :param refresh_token: current refresh token
    :return: [status code, response]
    '''
    try:
        response = requests.get(url, headers=jira_standard_header_params(access_token))

        if response.status_code == 401:
            new_acc, new_ref = refresh_jira_token(conn, timestamp, organization_id, jira_cloud_id, refresh_token)
            if new_acc is not None:
                response = requests.get(url, headers=jira_standard_header_params(new_acc))
                return [response.status_code, response.json()]

        return [response.status_code, response.json()]
    except Exception as e:
        logging.exception('Jira API request failed...')
        logging.exception(str(e))
        raise


def jira_post_api_request(conn, timestamp, organization_id, jira_cloud_id, url, access_token, refresh_token, body,
                          status_only=False):
    '''
    Makes a POST request to the api and returns the response.
    :param conn: db connection
    :param timestamp: timestamp when this action is being taken
    :param organization_id: ID of the organization
    :param jira_cloud_id: Jira cloud ID of the account the request is being made for
    :param url: api url
    :param access_token: the token that can be used to verify the
    :param refresh_token: current refresh token
    :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=jira_standard_header_params(access_token), data=json.dumps(body))

        if response.status_code == 401:
            new_acc, new_ref = refresh_jira_token(conn, timestamp, organization_id, jira_cloud_id, refresh_token)
            if new_acc is not None:
                response = requests.post(url, headers=jira_standard_header_params(new_acc), data=json.dumps(body))
                if status_only:
                    return response.status_code
                else:
                    return [response.status_code, response.json()]

        if status_only:
            return response.status_code
        else:
            return [response.status_code, response.json()]
    except Exception as e:
        logging.exception('Jira API request failed...')
        logging.exception(str(e))
        raise


def jira_delete_api_request(conn, timestamp, organization_id, jira_cloud_id, url, access_token, refresh_token, body):
    '''
    Makes a DELETE request to the api and returns the response status.
    :param conn: db connection
    :param timestamp: timestamp when this action is being taken
    :param organization_id: ID of the organization
    :param jira_cloud_id: Jira cloud ID of the account the request is being made for
    :param url: api url
    :param access_token: the token that can be used to verify the
    :param refresh_token: current refresh token
    :param body: the JSON body to send with the request
    :return: [status code, response]
    '''
    try:
        response = requests.delete(url, headers=jira_standard_header_params(access_token), data=json.dumps(body))

        if response.status_code == 401:
            new_acc, new_ref = refresh_jira_token(conn, timestamp, organization_id, jira_cloud_id, refresh_token)
            if new_acc is not None:
                response = requests.delete(url, headers=jira_standard_header_params(new_acc), data=json.dumps(body))
                return response.status_code

        return response.status_code
    except Exception as e:
        logging.exception('Jira API request failed...')
        logging.exception(str(e))
        raise


def jira_put_api_request(conn, timestamp, organization_id, jira_cloud_id, url, access_token, refresh_token, body):
    '''
    Makes a PUT request to the api and returns the response status.
    :param conn: db connection
    :param timestamp: timestamp when this action is being taken
    :param organization_id: ID of the organization
    :param jira_cloud_id: Jira cloud ID of the account the request is being made for
    :param url: api url
    :param access_token: the token that can be used to verify the
    :param refresh_token: current refresh token
    :param body: the JSON body to send with the request
    :return: [status code, response]
    '''
    try:
        response = requests.put(url, headers=jira_standard_header_params(access_token), data=json.dumps(body))

        if response.status_code == 401:
            new_acc, new_ref = refresh_jira_token(conn, timestamp, organization_id, jira_cloud_id, refresh_token)
            if new_acc is not None:
                response = requests.put(url, headers=jira_standard_header_params(new_acc), data=json.dumps(body))
                return response.status_code

        return response.status_code
    except Exception as e:
        logging.exception('Jira API request failed...')
        logging.exception(str(e))
        raise


def refresh_jira_token(conn, timestamp, organization_id, jira_cloud_id, refresh_token):
    '''
    Refresh the access token.
    :param conn: db connection
    :param timestamp: timestamp when this action is being taken
    :param organization_id: ID of the organization
    :param jira_cloud_id: the Jira cloud ID the token is for
    :param refresh_token: current refresh token
    '''
    try:
        jira_creds = s3.read_json(jira_s3_bucket, jira_s3_key)
        header_params = {'Content-Type': 'application/json'}
        body = {
            "grant_type": "refresh_token",
            "client_id": jira_creds["client_id"],
            "client_secret": jira_creds["client_secret"],
            "refresh_token": refresh_token
        }
        response = requests.post(url_token_retrieval, headers=header_params, data=json.dumps(body))

        if response.status_code == 200:
            data = response.json()
            acc_token = data[var_names.access_token]
            ref_token = data[var_names.refresh_token]
            db_jira.update_jira_account_tokens(conn, timestamp, organization_id, jira_cloud_id,
                                               data[var_names.access_token], data[var_names.refresh_token])

            return acc_token, ref_token

        return None, None
    except Exception as e:
        logging.exception('Failed to refresh Jira token...')
        logging.exception((str(e)))
        raise


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_request_acceptance_details(conn, timestamp, org_id, integration_key=integ_key)
    cloud_id = jira_integ_det[var_names.external_id]
    access_token = jira_integ_det[var_names.access_token]
    refresh_token = jira_integ_det[var_names.refresh_token]

    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]

    inc_url = url_paths.web_incidents_details + '/' + str(org_instance_id)
    desc_content = [
        {
            "type": "text",
            "text": "TaskCall URL: "
        },
        {
            "type": "text",
            "text": inc_url,
            "marks": [{"type": "link", "attrs": {"href": inc_url}}]
        }
    ]
    if text_msg is not None:
        desc_content.append({
            "type": "text",
            "text": "\n\n" + text_msg
        })

    issue_body = {
        'fields': {
            'project': {'id': project_id},
            'issuetype': {'id': issue_type_id},
            'summary': task_title,
            'description': {
                'version': 1,
                'type': 'doc',
                'content': [{
                    'type': 'paragraph',
                    'content': desc_content
                }]
            }
        }
    }
    if issue_type_hierarchy == 1:
        epic_field_name = get_epic_field_name(conn, timestamp, org_id, cloud_id, access_token, refresh_token)
        issue_body['fields'][epic_field_name] = task_title

    status, output = jira_post_api_request(conn, timestamp, org_id, cloud_id, url=url_create_issue.format(cloud_id),
                                           access_token=access_token, refresh_token=refresh_token, body=issue_body)
    issue_key = None
    if status in [200, 201]:
        issue_key = output['key']
        update_jira_priority(conn, timestamp, org_id, integ_key, integ_info, urgency, issue_key)

        update_status_url = url_transitions.format(cloud_id, issue_key)
        tr_sts, tr_output = jira_get_api_request(conn, timestamp, org_id, cloud_id, update_status_url,
                                                 access_token, refresh_token)
        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_post_api_request(conn, timestamp, org_id, cloud_id, update_status_url, access_token,
                                                 refresh_token, 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_request_acceptance_details(conn, timestamp, org_id, integration_key=integ_key)
        cloud_id = jira_integ_det[var_names.external_id]
        access_token = jira_integ_det[var_names.access_token]
        refresh_token = jira_integ_det[var_names.refresh_token]

        comment_body = {
            'body': {
                'type': 'doc',
                'version': 1,
                'content': [{
                    'type': 'paragraph',
                    'content': [{
                        'text': comment,
                        'type': 'text'
                    }]
                }]
            }
        }
        jira_post_api_request(conn, timestamp, org_id, cloud_id, url_add_comment.format(cloud_id, issue_key),
                              access_token, refresh_token, comment_body)


def create_jira_webhook(conn, timestamp, org_id, integration_url, jql, integ_key=None, cloud_id=None, access_token=None,
                        refresh_token=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 cloud_id: Jira cloud ID of the account the request is being made for
    :param access_token: the token that can be used to verify the
    :param refresh_token: current refresh token
    :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_request_acceptance_details(conn, timestamp, org_id, integration_key=integ_key)
        cloud_id = jira_integ_det[var_names.external_id]
        access_token = jira_integ_det[var_names.access_token]
        refresh_token = jira_integ_det[var_names.refresh_token]
    else:
        assert cloud_id is not None

    web_url = url_webhook.format(cloud_id)
    web_req_body = {
        "url": integration_url,
        "webhooks": [
            {
                "jqlFilter": jql,
                "events": ["jira:issue_created"],
            }
        ],
    }

    jira_status, jira_output = jira_post_api_request(conn, timestamp, org_id, cloud_id, web_url, access_token,
                                                     refresh_token, web_req_body)
    webhook_id, error = None, None
    if jira_status == 200:
        if str_errors in jira_output[str_webhook_registration_result][0]:
            error = jira_output[str_webhook_registration_result][0][str_errors][0]
            logging.error(error)
        else:
            webhook_id = jira_output[str_webhook_registration_result][0][str_created_webhook_id]
            if webhook_to_del is not None:
                jira_delete_api_request(conn, timestamp, org_id, cloud_id, web_url, access_token, refresh_token,
                                        {"webhookIds": [webhook_to_del]})

    return webhook_id, error


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_request_acceptance_details(conn, timestamp, org_id, integration_key=integ_key)
    cloud_id = jira_integ_det[var_names.external_id]
    access_token = jira_integ_det[var_names.access_token]
    refresh_token = jira_integ_det[var_names.refresh_token]

    web_url = url_webhook.format(cloud_id)
    jira_delete_api_request(conn, timestamp, org_id, cloud_id, web_url, access_token, refresh_token,
                            {"webhookIds": [webhook_id]})


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_request_acceptance_details(conn, timestamp, org_id, integration_key=integ_key)
    cloud_id = jira_integ_det[var_names.external_id]
    access_token = jira_integ_det[var_names.access_token]
    refresh_token = jira_integ_det[var_names.refresh_token]

    issue_status_id = integ_info[var_names.status][instance_state]
    update_status_url = url_transitions.format(cloud_id, issue_key)
    tr_sts, tr_output = jira_get_api_request(conn, timestamp, org_id, cloud_id, update_status_url,
                                             access_token, refresh_token)
    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_post_api_request(conn, timestamp, org_id, cloud_id, update_status_url, access_token,
                                             refresh_token, 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_request_acceptance_details(conn, timestamp, org_id, integration_key=integ_key)
    cloud_id = jira_integ_det[var_names.external_id]
    access_token = jira_integ_det[var_names.access_token]
    refresh_token = jira_integ_det[var_names.refresh_token]

    priority_id = integ_info[var_names.urgency_level][str(urgency)]
    edit_priority_url = url_edit_issue.format(cloud_id, issue_key)
    edit_body = {
        "update": {
            "priority": [
                {
                    "set": {
                        "id": priority_id
                    }
                }
            ]
        }
    }
    status = jira_put_api_request(conn, timestamp, org_id, cloud_id, edit_priority_url, access_token, refresh_token,
                                  edit_body)
    if status not in [200, 204]:
        logging.error('Failed - Jira ' + issue_key + ' issue priority update - ' + priority_id)


def get_epic_field_name(conn, timestamp, organization_id, jira_cloud_id, access_token, refresh_token):
    '''
    Get the name of the field that has to be included in the payload to create an Epic.
    :param conn: db connection
    :param timestamp: timestamp when this action is being taken
    :param organization_id: ID of the organization
    :param jira_cloud_id: Jira cloud ID of the account the request is being made for
    :param access_token: the token that can be used to verify the request
    :param refresh_token: current refresh token
    :return: (str) -> name of the field
    '''
    url = url_fields.format(jira_cloud_id)
    status, output = jira_get_api_request(conn, timestamp, organization_id, jira_cloud_id, url,
                                          access_token, refresh_token)
    field_name = None
    if status == 200:
        for item in output:
            if 'untranslatedName' in item and item['untranslatedName'].lower() == 'epic name':
                field_name = item['id']
    return field_name
