import os
import json
import adal
import requests
import dateutil
import datetime

__all__ = [
    'GenericWrapper',
    'Account',
    'Incident',
]


class D365ApiError(Exception):
    pass


class D36pApiWrapperError(Exception):
    pass


class BaseApiWrapper:
    def __init__(self, crmorg=None, token=None, tenant=None, client_id=None, client_secret=None, page_size=100):
        self.crmorg = crmorg or self.get_crmorg()
        self.tenant = tenant
        self.client_id = client_id
        self.client_secret = client_secret
        self._token = token or self._get_token(self.tenant, self.client_id, self.client_secret)
        self.page_size = page_size
        self.current_page = None
        self.page_urls = {}

    entity_type = None

    @staticmethod
    def get_crmorg():
        org = os.environ.get('D365_ORG_URL', None)
        if org is None:
            raise ValueError(
                '"D365_ORG_URL" must be set as environment variable in case it is not passed as a parameter'
            )
        return org

    def _get_token(self, tenant, client_id, client_secret):
        if tenant is None:
            tenant = os.environ.get('D365_TENANT', None)
        if client_id is None:
            client_id = os.environ.get('D365_CLIENT_ID', None)
        if client_secret is None:
            client_secret = os.environ.get('D365_CLIENT_SECRET', None)
        if not all([tenant, client_id, client_secret]):
            raise ValueError(
                '"D365_TENANT", "D365_CLIENT_ID" and "D365_CLIENT_SECRET" must be set as '
                'environment variable in case they are not passed as a parameters'
            )
        context = adal.AuthenticationContext(f'https://login.microsoftonline.com/{tenant}')
        token = context.acquire_token_with_client_credentials(self.crmorg, client_id, client_secret)
        self.token_expires_at = dateutil.parser.parse(token['expiresOn'])
        return token

    @property
    def token(self):
        # ToDo: Maybe implement check for token expiration time here.
        if datetime.datetime.now() + datetime.timedelta(minutes=5) > self.token_expires_at:
            self._token = self._get_token(self.tenant, self.client_id, self.client_secret)
        return self._token.get('accessToken')

    @property
    def headers(self):
        return {
            'Authorization': f'Bearer {self.token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }

    @property
    def _base_api_url(self):
        return f"{self.crmorg}/api/data/v9.0/"

    @property
    def has_next_page(self):
        return self.current_page and self.page_urls.get(self.current_page + 1) \
               and self.page_urls.get(self.current_page + 1) != '__none__'

    @property
    def api_url(self):
        if not self.entity_type:
            raise NotImplementedError('"entity_type" attribute must be defined in subclasses of BaseApiWrapper')
        return f"{self._base_api_url}{self.entity_type}"

    def get_page(self, page: int = 1, select=None, request_filter=None, order_by=None):
        """
        Used for getting a first page of list API data,
        or a specific page if such page url has been cached.

        :raises D365ApiError: if http request is not "Ok"
        :raises D36pApiWrapperError: if requested page url is not cached or is out of range
        :param page:
        :param select:
        :param request_filter:
        :param order_by:
        :return:
        """
        headers = self.headers
        headers.update(
            {'Prefer': f'odata.maxpagesize={self.page_size}'}
        )
        params = {}
        if page == 1:
            url = self.api_url
            if select:
                params['$select'] = select
            if request_filter:
                params['$filter'] = request_filter
            if order_by:
                params['$orderby'] = order_by
        else:
            if self.page_urls.get(page) == '__none__':
                raise D36pApiWrapperError(f'The page you requested is out of range.')
            if page not in self.page_urls:
                pages = sorted(self.page_urls.keys())
                raise D36pApiWrapperError(f'The url for the page you requested is not cashed. Available pages are '
                                          f'{pages[0]} trough {pages[-1]}. Try to get the highest available page and'
                                          f' then use "get_next_page()" method until you get to desired page.')
            url = self.page_urls.get(page, None)

        response = requests.get(url, params=params, headers=headers)
        if response.ok:
            self.current_page = page
            self.page_urls[page + 1] = response.json().get('@odata.nextLink', '__none__')
            return response.json()
        else:
            raise D365ApiError({'response_status_code': response.status_code, 'response_data': response.json()})

    def get_next_page(self):
        if self.current_page:
            return self.get_page(page=self.current_page + 1)
        raise D36pApiWrapperError('To call "get_next_page()" "current_page" attribute can not be None. '
                                  'Please call "get_page()" method first.')

    def get_previous_page(self):
        if not self.current_page or self.current_page == 1:
            raise D36pApiWrapperError('To call "get_next_page()" "current_page" attribute can not be None or 1. Please '
                                      'call "get_page()" method or make sure the current page is not first.')
        return self.get_page(page=self.current_page + 1)

    def get_top(self, qty: int, select=None, request_filter=None, order_by=None):
        """
        :raises D365ApiError: if http request is not "Ok"
        :param qty: the number of matching results to return
        :param select:
        :param request_filter:
        :param order_by:
        :return:
        """
        params = {'$top': qty}
        if select:
            params['$select'] = select
        if request_filter:
            params['$filter'] = request_filter
        if order_by:
            params['$orderby'] = order_by
        response = requests.get(self.api_url, params=params, headers=self.headers)
        if response.status_code == 200:
            return response.json()
        else:
            raise D365ApiError({'response_satatus_code': response.status_code, 'response_data': response.json()})

    def create(self, data):
        """
        Create entity
        :param data: python dictionary with body content
        :return: response object
        """
        headers = self.headers
        headers.update({'Prefer': 'return=representation'})
        return requests.post(self.api_url, headers=headers, json=json.dumps(data))

    def retrieve(self, entity_id):
        return requests.get(f'{self.api_url}({entity_id})', headers=self.headers)

    def update(self, entity_id, data, select=None):
        headers = self.headers
        headers.update({'Prefer': 'return=representation'})
        url = f'{self.api_url}({entity_id})'
        params = {}
        if select:
            params['$select'] = select
        return requests.patch(url, params=params, headers=headers, json=json.dumps(data))

    def delete(self, entity_id):
        return requests.delete(f'{self.api_url}({entity_id})', headers=self.headers)


class GenericWrapper(BaseApiWrapper):
    def __init__(self, entity_type, *args, **kwargs):
        super(GenericWrapper, self).__init__(*args, **kwargs)
        self.entity_type = entity_type


class Account(BaseApiWrapper):
    entity_type = 'accounts'


class Incident(BaseApiWrapper):
    entity_type = 'incidents'

    def create(self, data):
        """
        :param data: example {
            'title': 'Example title',
            'description': 'Example short description',
            'sca_fulldescription': 'Example full description',
            'sca_missiondescription_t_': 'Example something more here',
            "sca_customerid@odata.bind": "/accounts(48344912-c9ae-xxxx-xxxx-842b2bf9a00f)",
            "customerid_account@odata.bind": "/accounts(334e12ea-c34c-xxxx-xxxx-00155d56d687)"
        }
        """
        return super().create(data)
