import json
import urllib.parse as urlparse
from copy import deepcopy

import trafaret
from aiohttp import web, ClientSession, MultiDict
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden, HTTPNotFound

from . import settings
from .badge import render_badge
from .api_validators import (
    cover_rage_client_validator,
    GITHUB_PULL_REQUEST_TRIGGER_CREATE_UPDATE_ACTIONS,
    GITHUB_PULL_REQUEST_TRIGGER_REMOVE_ACTIONS,
    github_push_event_validator,
    github_pull_request_event_validator,
    github_status_validator,
    GITHUB_AVAILABLE_STATUSES,
    GITHUB_ERROR_STATUS,
    GITHUB_FAILURE_STATUS,
    GITHUB_PENDING_STATUS,
    GITHUB_SUCCESS_STATUS,
    cover_rage_client_to_redis,
    github_push_to_redis,
    github_pull_request_to_redis,
)
from .storage import (
    get_repo, update_coverage_for_repo, split_repo_id, get_commit, update_commit, update_coverage_for_commit
)

HTTP_OK = 200
HTTP_ACCEPTED = 202
HTTP_BAD_REQUEST = 400


COVER_RAGE_CONTEXT = 'continuous-integration/cover_rage'


FAILURE_CHECK_DESCRIPTION = 'Not all lines are covered with tests'
PENDING_CHECK_DESCRIPTION = 'Pending coverage check'
SUCCESS_CHECK_DESCRIPTION = 'All new lines are covered with tests'
ERROR_CHECK_CLOSED_PULL_REQUEST_DESCRIPTION = 'Pull request was closed'


def cover_rage_status_url(public_token: str, sha: str) -> str:
    """
    Helper to get status url for `sha` commit
    :param public_token: Cover-Rage app public token
    :param sha: git commit hash
    :return: absolute url for `sha` commit check
    """
    url = urlparse.urlunparse(
        (
            settings.SRV_SCHEME,
            settings.SRV_HOST,
            settings.SRV_STATUS_URL.format(public_token=public_token, sha=sha),
            '',
            '',
            ''
        )
    )
    return url


# TODO: cover with tests
async def send_github_status(
    loop: object,
    repo_owner: str,
    repo_name: str,
    sha: str,
    state: str,
    target_url: str,
    description: str,
    context: str,
    github_access_token: str
) -> bool:
    """
    Helper to set Github status
    :param loop: event loop
    :param repo_owner: repo owner
    :param repo_name: repo name
    :param sha: commit hash
    :param state: status, could be one of: error / failure / pending / success
    :param target_url: url for Cover-Rage check of commit
    :param description: short description of status
    :param context: context identifier
    :param github_access_token: Github access token
    :raises: AssertationError
    :return: True if setting status was successful
    """
    assert isinstance(repo_owner, str)
    assert isinstance(repo_name, str)
    assert isinstance(sha, str)
    assert isinstance(state, str)
    assert isinstance(target_url, str)
    assert isinstance(description, str)
    assert isinstance(context, str)
    assert isinstance(github_access_token, str)
    assert repo_owner
    assert repo_name
    assert sha
    assert state in GITHUB_AVAILABLE_STATUSES
    assert target_url
    assert description
    assert context
    assert github_access_token

    github_url = 'https://api.github.com/repos/{repo_owner}/{repo_name}/statuses/{sha}'.format(
        repo_owner=repo_owner, repo_name=repo_name, sha=sha
    )

    async with ClientSession(loop=loop) as session:
        headers = {
            'Authorization': 'token {token}'.format(token=github_access_token),
            'Content-Type': 'application/json'
        }
        data = {
            'state': state,
            'target_url': target_url,
            'description': description,
            'context': context,
        }
        async with session.post(github_url, headers=headers, data=json.dumps(data)) as response:
            return response.status == 201


# TODO: cover with tests
async def get_github_status(loop: object, repo_owner: str, repo_name: str, sha: str, github_access_token: str) -> str:
    """
    Helper to get Github status
    :param loop: event loop
    :param repo_owner: repo owner
    :param repo_name: repo name
    :param sha: commit hash
    :param github_access_token: Github access token
    :raises: AssertationError
    :return: Github status, could be one of: error / failure / pending / success
    """
    assert isinstance(repo_owner, str)
    assert isinstance(repo_name, str)
    assert isinstance(sha, str)
    assert isinstance(github_access_token, str)
    assert repo_owner
    assert repo_name
    assert sha
    assert github_access_token

    github_url = 'https://api.github.com/repos/{repo_owner}/{repo_name}/commits/{sha}/status'.format(
        repo_owner=repo_owner, repo_name=repo_name, sha=sha
    )

    async with ClientSession(loop=loop) as session:
        headers = {
            'Authorization': 'token {token}'.format(token=github_access_token),
            'Content-Type': 'application/json'
        }
        async with session.get(github_url, headers=headers) as response:
            if response.status == 200:
                response_json = await response.json()
                try:
                    validated_response = github_status_validator.check(response_json)
                    if validated_response['sha'] == sha:
                        return validated_response['status']
                except trafaret.DataError:
                    pass


class BadgeApiView(web.View):
    """
    View that handles coverage badge
    """

    encoding = 'utf-8'
    content_type = 'image/svg+xml'
    public_token = None
    sha = None
    repo_id = None
    project = None
    commit = None

    async def get(self):
        self.public_token = self.request.match_info.get('token')
        if not self.public_token:
            raise HTTPNotFound

        self.repo_id, self.project = await get_repo(self.request.app.redis_pool, token=self.public_token)
        if self.project is None:
            raise HTTPNotFound

        coverage = self.project.get('coverage', 0)
        min_good_coverage = self.project.get('min_good_coverage', settings.MIN_GOOD_COVERAGE_PERCENTAGE)
        badge = render_badge(coverage, coverage >= min_good_coverage)

        return web.Response(
            body=badge,
            headers=MultiDict({'Content-Disposition': 'Inline'}),
            status=HTTP_OK,
            content_type=self.content_type
        )


class _CoverRageCommonApiView(web.View):
    """
    Common class for all Cover-Rage API views:
      - request data validation (`self.validator` should be set)
      - authorization (optional, set `self.check_authorization` to True)
      - searching repo in Redis
      - custom handling (should be overridden)
      - response / errors handling
    """

    validator = None
    raw_data = None
    public_token = None
    repo_id = None
    project = None
    validated_data = {}
    errors = {}
    encoding = 'utf-8'
    content_type = 'application/json'
    response_success_status = HTTP_OK
    response_error_status = HTTP_BAD_REQUEST
    check_authorization = False

    def __await__(self):  # pragma: no cover
        pass

    def __init__(self, request):
        super(_CoverRageCommonApiView, self).__init__(request)
        assert self.validator

    def auth(self):
        """
        Check that `Authorization` header is set with correct Cover-Rage private token.
        :raises: HTTPUnauthorized, HTTPForbidden
        """
        if 'Authorization' in self.request.headers:
            private_token = self.request.headers['Authorization']
        else:
            raise HTTPUnauthorized
        has_permission = (
            'rage_private_token' in self.project and
            self.project['rage_private_token'] == private_token
        )
        if not has_permission:
            raise HTTPForbidden

    def is_valid(self):
        try:
            self.validated_data = self.validator.check(self.raw_data)
            return True
        except trafaret.DataError:
            self.errors = trafaret.extract_error(self.validator, self.raw_data)
            return False

    async def handle(self):  # pragma: no cover
        raise NotImplemented

    async def post(self):
        """
        Common POST handler:
        --------------------
        1) Get project from public token.
        2) Check authorization (if `self.check_authorization` is True).
        3) Validate input data with `self.validator`.
        4) Do something with this data - override `handle` method.
        5) Return response.

        :raises: HTTPUnauthorized, HTTPForbidden, HTTPNotFound
        """

        # Get public token from url
        self.public_token = self.request.match_info.get('token')
        if not self.public_token:
            raise HTTPNotFound

        # Get repo id and data from Redis or raise 404
        self.repo_id, self.project = await get_repo(self.request.app.redis_pool, token=self.public_token)
        if self.project is None:
            raise HTTPNotFound

        # Check authorization if needed
        if self.check_authorization:
            self.auth()

        # Validate input data with `self.validator`
        self.raw_data = await self.request.json()
        if self.is_valid():
            response, response_status = await self.handle()
        else:
            response = deepcopy(self.errors)
            response_status = self.response_error_status

        # Return response
        return web.Response(
            body=bytes(json.dumps(response), self.encoding),
            status=response_status or self.response_success_status,
            content_type=self.content_type
        )


# TODO: Implement Bitbucket pull request events handling
class BitbucketPullRequestEventApiView(_CoverRageCommonApiView):

    pass


# TODO: Implement Bitbucket push events handling
class BitbucketPushEventApiView(_CoverRageCommonApiView):

    pass


class GithubPullRequestEventApiView(_CoverRageCommonApiView):
    """
    Handle Github `PULL REQUEST` event:
      - create / update mapping for commit (for actions: edited, opened, reopened / status: 200)
      - ignore other actions (status: 202)
    """

    validator = github_pull_request_event_validator

    async def handle(self):
        commit_data = github_pull_request_to_redis(self.validated_data)
        git_commit = commit_data.pop('sha')

        assert 'action' in commit_data
        if commit_data['action'] in GITHUB_PULL_REQUEST_TRIGGER_CREATE_UPDATE_ACTIONS:
            description = PENDING_CHECK_DESCRIPTION  # TODO: cover with tests
            status = GITHUB_PENDING_STATUS  # TODO: cover with tests
        elif commit_data['action'] in GITHUB_PULL_REQUEST_TRIGGER_REMOVE_ACTIONS:
            description = ERROR_CHECK_CLOSED_PULL_REQUEST_DESCRIPTION  # TODO: cover with tests
            status = GITHUB_ERROR_STATUS  # TODO: cover with tests
        else:
            # Ignore other Github pull request actions
            description = None
            status = None

        if all((description, status)):
            if not await update_commit(self.request.app.redis_pool, self.repo_id, git_commit, commit_data):
                return {'git_commit': 'Could not update commit'}, HTTP_BAD_REQUEST

            _, repo_owner, repo_name = split_repo_id(self.repo_id)
            if await send_github_status(
                self.request.app.loop,
                repo_owner,
                repo_name,
                git_commit,
                status,
                cover_rage_status_url(self.public_token, git_commit),
                description,
                COVER_RAGE_CONTEXT,
                self.project['repo_access_token']
            ):
                return {}, HTTP_OK
            else:
                return {'git_commit': 'Could not set status for commit'}, HTTP_BAD_REQUEST
        else:
            return {}, HTTP_ACCEPTED


class GithubPushEventApiView(_CoverRageCommonApiView):
    """
    Handle Github `PUSH` event - create mapping for commit.
    """

    validator = github_push_event_validator

    async def handle(self):
        commit_data = github_push_to_redis(self.validated_data)
        git_commit = commit_data.pop('sha')

        if not await update_commit(self.request.app.redis_pool, self.repo_id, git_commit, commit_data):
            return {'git_commit': 'Could not update commit'}, HTTP_BAD_REQUEST

        _, repo_owner, repo_name = split_repo_id(self.repo_id)
        if await send_github_status(
            self.request.app.loop,
            repo_owner,
            repo_name,
            git_commit,
            GITHUB_PENDING_STATUS,
            cover_rage_status_url(self.public_token, git_commit),
            PENDING_CHECK_DESCRIPTION,
            COVER_RAGE_CONTEXT,
            self.project['repo_access_token']
        ):
            return {}, HTTP_OK
        else:
            return {'git_commit': 'Could not set status for commit'}, HTTP_BAD_REQUEST


class ResultsApiView(_CoverRageCommonApiView):
    """
    Update repo information with coverage data from Cover-Rage client.
    """

    validator = cover_rage_client_validator
    check_authorization = True

    async def handle(self):
        coverage_data = cover_rage_client_to_redis(self.validated_data)
        git_commit = coverage_data.pop('git_commit')

        if not await update_coverage_for_commit(self.request.app.redis_pool, self.repo_id, git_commit, coverage_data):
            return {'git_commit': 'Could not update commit'}, HTTP_BAD_REQUEST

        current_coverage = int(round(self.validated_data['overall_coverage'] * 100))

        if await update_coverage_for_repo(self.request.app.redis_pool, self.repo_id, current_coverage):
            _, repo_owner, repo_name = split_repo_id(self.repo_id)
            if self.validated_data['uncovered_lines']:
                description = FAILURE_CHECK_DESCRIPTION  # TODO: cover with tests
                status = GITHUB_FAILURE_STATUS  # TODO: cover with tests
            else:
                description = SUCCESS_CHECK_DESCRIPTION  # TODO: cover with tests
                status = GITHUB_SUCCESS_STATUS  # TODO: cover with tests
            if await send_github_status(
                self.request.app.loop,
                repo_owner,
                repo_name,
                git_commit,
                status,
                cover_rage_status_url(self.public_token, git_commit),
                description,
                COVER_RAGE_CONTEXT,
                self.project['repo_access_token']
            ):
                return {}, HTTP_OK
            else:
                return {'git_commit': 'Could not set status for commit'}, HTTP_BAD_REQUEST
        else:
            return {'git_commit': 'Could not update repo'}, HTTP_BAD_REQUEST


class StatusApiView(web.View):
    """
    View that handles status of check for required commit
    """

    encoding = 'utf-8'
    content_type = 'application/json'
    public_token = None
    sha = None
    repo_id = None
    project = None
    commit = None

    async def get(self):
        """
        Get commit data from Redis
        :raises: HTTPNotFound
        :return:
        """
        self.public_token = self.request.match_info.get('token')
        if not self.public_token:
            raise HTTPNotFound

        self.sha = self.request.match_info.get('sha')
        if not self.sha:
            raise HTTPNotFound

        self.repo_id, self.project = await get_repo(self.request.app.redis_pool, token=self.public_token)
        if self.project is None:
            raise HTTPNotFound

        self.commit = await get_commit(self.request.app.redis_pool, self.repo_id, self.sha)
        if self.commit is None:
            raise HTTPNotFound

        return web.Response(
            body=bytes(json.dumps(self.commit), self.encoding),
            status=HTTP_OK,
            content_type=self.content_type
        )
