from __future__ import absolute_import

import functools
import json
import logging
import requests

from github import GithubException, UnknownObjectException, Github

from lintly.constants import LINTLY_IDENTIFIER
from lintly.formatters import (
    build_pr_review_line_comment,
    build_pr_review_body,
    build_check_line_comment
)
from lintly.constants import (
    ACTION_REVIEW_REQUEST_CHANGES,
    ACTION_REVIEW_COMMENT,
    ACTION_REVIEW_APPROVE
)

from .base import BaseGitBackend
from .errors import NotFoundError, GitClientError
from .objects import PullRequest


logger = logging.getLogger(__name__)

# Get 100 items at a time so that we can make fewer API requests
DEFAULT_PER_PAGE = 100

GITHUB_API_HEADER = 'application/vnd.github.v3+json'
GITHUB_API_PR_REVIEW_HEADER = 'application/vnd.github.black-cat-preview+json'
GITHUB_DIFF_HEADER = 'application/vnd.github.3.diff'
GITHUB_CHECKS_HEADER = 'application/vnd.github.antiope-preview+json'
GITHUB_USER_AGENT = 'Lintly'

ANNOTATION_LEVEL_WARNING = 'warning'
ANNOTATION_LEVEL_FAILURE = 'failure'


def translate_github_exception(func):
    """
    Decorator to catch GitHub-specific exceptions and raise them as GitClientError exceptions.
    """

    @functools.wraps(func)
    def _wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except UnknownObjectException as e:
            logger.exception('GitHub API 404 Exception')
            raise NotFoundError(str(e))
        except GithubException as e:
            logger.exception('GitHub API Exception')
            raise GitClientError(str(e))

    return _wrapper


class GitHubAPIClient:
    """
    A wrapper class for making calls directly to the GitHub API and returning the results
    as JSON or a string (depending on the Content-Type header).
    """

    base_url = 'https://api.github.com'

    def __init__(self, token=None):
        self.token = token

    def get_headers(self):
        headers = {
            'Accept': GITHUB_API_HEADER,
            'Authorization': 'token {}'.format(self.token),
            'User-Agent': GITHUB_USER_AGENT,
        }
        return headers

    def post(self, url, data=None, headers=None):
        return self._do_request('post', url, json.dumps(data), headers)

    def get(self, url, data=None, headers=None):
        return self._do_request('get', url, data, headers)

    def put(self, url, data=None, headers=None):
        return self._do_request('put', url, json.dumps(data), headers)

    def patch(self, url, data=None, headers=None):
        return self._do_request('patch', url, json.dumps(data), headers)

    def _do_request(self, method, url, data=None, extra_headers=None):
        if data is None:
            data = dict()
        if extra_headers is None:
            extra_headers = dict()

        full_url = self.base_url + url
        headers = self.get_headers()
        headers.update(extra_headers)

        logger.debug('Sending a {} request to {}'.format(method, url))

        response = getattr(requests, method.lower())(full_url, data=data, headers=headers)
        if 200 <= response.status_code < 300:
            if 'application/json' in response.headers['Content-Type']:
                return response.json()
            else:
                return response.content
        elif response.status_code == 404:
            raise NotFoundError(response.content, status_code=response.status_code)
        else:
            raise GitClientError(response.content, status_code=response.status_code)


class GitHubBackend(BaseGitBackend):

    supports_pr_reviews = True

    def __init__(self, token, project, context):
        super(GitHubBackend, self).__init__(token, project)
        self.client = Github(token, user_agent=GITHUB_USER_AGENT, per_page=DEFAULT_PER_PAGE)
        self.context = context

    def _should_delete_comment(self, comment):
        return LINTLY_IDENTIFIER in comment.body

    @translate_github_exception
    def get_pull_request(self, pr):
        repo = self.client.get_repo(self.project.full_name)
        gh_pull = repo.get_pull(int(pr))

        pull_request = PullRequest(
            number=gh_pull.number,
            url=gh_pull.url,
            head_ref=gh_pull.head.ref,
            head_sha=gh_pull.head.sha,
            base_ref=gh_pull.base.ref,
            base_sha=gh_pull.base.sha
        )

        return pull_request

    @translate_github_exception
    def create_pull_request_comment(self, pr, comment):
        repo = self.client.get_repo(self.project.full_name)
        pull_request = repo.get_pull(int(pr))

        pull_request.create_issue_comment(
            body=comment
        )

    @translate_github_exception
    def delete_pull_request_comments(self, pr):
        repo = self.client.get_repo(self.project.full_name)
        pull_request = repo.get_issue(int(pr))
        for comment in pull_request.get_comments():
            if self._should_delete_comment(comment):
                comment.delete()

    def get_pr_diff(self, pr):
        client = GitHubAPIClient(token=self.token)
        client.base_url = 'https://api.github.com'
        diff_url = '/repos/{owner}/{repo_name}/pulls/{pr_number}'.format(
            owner=self.project.owner_login,
            repo_name=self.project.name,
            pr_number=pr
        )

        diff = client.get(diff_url, headers={'Accept': GITHUB_DIFF_HEADER})

        return diff.decode('utf-8')

    def _get_event(self, review_action):
        if review_action == ACTION_REVIEW_COMMENT:
            return 'COMMENT'
        elif review_action == ACTION_REVIEW_REQUEST_CHANGES:
            return 'REQUEST_CHANGES'
        elif review_action == ACTION_REVIEW_APPROVE:
            return 'APPROVE'

    def create_pull_request_review(self, pr, patch, all_violations, pr_review_action):
        comments = []
        for file_path in all_violations:
            violations = all_violations[file_path]

            # https://developer.github.com/v3/pulls/comments/#input
            for violation in violations:
                patch_position = patch.get_patch_position(file_path, violation.line)
                if patch_position is not None:
                    comments.append({
                        'path': file_path,
                        'position': patch_position,
                        'body': build_pr_review_line_comment(violation)
                    })

        client = GitHubAPIClient(token=self.token)
        data = {
            'body': build_pr_review_body(all_violations),
            'event': self._get_event(pr_review_action),
            'comments': comments,
        }

        url = '/repos/{owner}/{repo_name}/pulls/{pr_number}/reviews'.format(
            owner=self.project.owner_login,
            repo_name=self.project.name,
            pr_number=pr
        )
        client.post(url, data, headers={'Accept': GITHUB_API_PR_REVIEW_HEADER})

    @translate_github_exception
    def delete_pull_request_review_comments(self, pr):
        repo = self.client.get_repo(self.project.full_name)
        pull_request = repo.get_pull(int(pr))
        for comment in pull_request.get_review_comments():
            if self._should_delete_comment(comment):
                comment.delete()

    def post_status(self, state, description, sha, target_url=''):
        url = '/repos/{owner}/{repo_name}/statuses/{sha}'.format(
            owner=self.project.owner_login, repo_name=self.project.name, sha=sha)

        # Using wrapper client since PyGitHub makes unnecessary API calls
        client = GitHubAPIClient(token=self.token)
        data = {
            'state': state,
            'description': description,
            'target_url': target_url,
            'context': self.context
        }
        client.post(url, data)

    def create_check_run(self, commit_sha, description, violations):
        url = '/repos/{owner}/{repo_name}/check-runs'.format(
            owner=self.project.owner_login, repo_name=self.project.name)
        annotations = self._get_check_annotations(violations)

        client = GitHubAPIClient(token=self.token)
        data = {
            'name': self.context,
            'conclusion': 'success' if len(annotations) == 0 else 'failure',
            'head_sha': commit_sha,
            'output': {
                'title': description,
                'summary': description,
                'annotations': annotations
            }
        }
        response = client.post(url, data, headers={'Accept': GITHUB_CHECKS_HEADER})
        return response.get('id')

    # https://developer.github.com/v3/checks/runs/#update-a-check-run
    def update_check_run(self, check_run_id, description, violations):
        url = '/repos/{owner}/{repo_name}/check-runs/{check_run_id}'.format(
            owner=self.project.owner_login, repo_name=self.project.name, check_run_id=check_run_id)

        # PyGitHub does not support the Checks API
        client = GitHubAPIClient(token=self.token)
        data = {
            'output': {
                'title': description,
                'summary': description,
                'annotations': self._get_check_annotations(violations)
            }
        }
        client.patch(url, data, headers={'Accept': GITHUB_CHECKS_HEADER})

    def _get_check_annotations(self, violations):
        annotations = []
        for file_path in violations:
            file_violations = violations[file_path]

            # https://developer.github.com/v3/pulls/comments/#input
            for violation in file_violations:
                annotations.append({
                    'annotation_level': 'warning',
                    'path': file_path,
                    'start_line': violation.line,
                    'end_line': violation.line,
                    'message': build_check_line_comment(violation)
                })
        return annotations