#!/usr/bin/env python3

# Copyright 2022 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
Debusine: Interacts with debusine server.

Debusine fetches information, submit work requests and other operations.
"""
from typing import Optional

from pydantic import ValidationError

import requests

from requests.compat import json  # noqa: I202

from debusine.client import exceptions
from debusine.client.models import WorkRequest


class Debusine:
    """Class to interact with debusine server."""

    API_VERSION = '1.0'

    def __init__(self, api_url, api_token=None):
        """
        Initialize client.

        :param api_url: URL for the debusine server.
        :param api_token: optional token to be used for the calls.
        """
        self.api_url = api_url.rstrip('/')
        self.api_token = api_token

    def work_request_status(self, work_request_id) -> WorkRequest:
        """
        Fetch work_request_status for work_request_id.

        :param work_request_id: id to fetch the status of.
        :raises many:   see _api_request method documentation.
        """
        url = (
            f'{self.api_url}/{self.API_VERSION}/work-request/'
            f'{work_request_id}/'
        )

        return self._api_request('GET', url, WorkRequest)

    def _api_request(self, method, url, expected_class, data=None):
        """
        Request to the server.

        :param method: HTTP method (GET, POST, ...).
        :param url: URL to make the request to.
        :param expected_class: expected object class that the server.
          will return. Used to deserialize the response and return an object
        :return: an object of the expected_class.
        :raises exceptions.UnexpectedResponseError: the server didn't return
          a valid JSON.
        :raises exceptions.WorkRequestNotFound: the server could not find the
          work request.
        :raises exceptions.ClientConnectionError: the client could not connect
          to the server.
        :raises ValueError: invalid options passed.
        :raises exceptions.ClientForbiddenError: the server returned HTTP 403.
        :raises DebusineError: the server returned HTTP 400. Contains the
          detail message.
        """
        method_to_func = {
            'GET': requests.get,
            'POST': requests.post,
        }

        if method not in method_to_func:
            allowed_methods = ", ".join(method_to_func.keys())
            raise ValueError(f'Method must be one of: {allowed_methods}')

        if data is not None and method == 'GET':
            raise ValueError('data argument not allowed with HTTP GET')

        optional_kwargs = {}

        if data is not None:
            optional_kwargs = {'json': data}

        try:
            response = method_to_func[method](
                url, headers={'Token': self.api_token}, **optional_kwargs
            )
        except (requests.exceptions.RequestException, ConnectionError) as exc:
            raise exceptions.ClientConnectionError(
                f'Cannot connect to {url}. Error: {str(exc)}'
            ) from exc

        if response.status_code == requests.codes.ok:
            try:
                return expected_class.parse_raw(response.content)
            except ValidationError as exc:
                raise exceptions.UnexpectedResponseError(
                    f'Server did not return valid object ({url})'
                ) from exc
        elif response.status_code == requests.codes.not_found:
            raise exceptions.NotFoundError(f'Not found ({url})')
        elif response.status_code == requests.codes.forbidden:
            raise exceptions.ClientForbiddenError(
                f"HTTP 403. Token ({self.api_token}) is invalid or disabled"
            )
        elif error := self._debusine_problem(response):
            raise exceptions.DebusineError(error)
        else:
            raise exceptions.UnexpectedResponseError(
                f'Server returned unexpected status '
                f'code: {response.status_code} ({url})'
            )

    def work_request_create(self, work_request: WorkRequest):
        """
        Create a work request (via POST /work-request/).

        :return: WorkRequest returned by the server.
        :raises: see _api_request method documentation.
        """
        url = f'{self.api_url}/{self.API_VERSION}/work-request/'

        return self._api_request(
            'POST',
            url,
            WorkRequest,
            data=work_request.dict(include={'task_name', 'task_data'}),
        )

    @staticmethod
    def _debusine_problem(response) -> Optional[dict]:
        """If response is an application/problem+json returns the body JSON."""
        if response.headers["content-type"] == "application/problem+json":
            try:
                content = response.json()
            except json.JSONDecodeError:
                return None

            return content

        return None
