# file eulfedora/api.py
#
#   Copyright 2010,2011 Emory University Libraries
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

from __future__ import unicode_literals
import csv
import logging
import requests
import time
import warnings

from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor, \
    user_agent

import six
from six.moves.urllib.parse import urljoin

try:
    from django.dispatch import Signal
except ImportError:
    Signal = None

from eulfedora import __version__ as eulfedora_version
from eulfedora.util import datetime_to_fedoratime, \
    RequestFailed, ChecksumMismatch, PermissionDenied, parse_rdf, \
    ReadableIterator, force_bytes

logger = logging.getLogger(__name__)

# low-level wrappers

# bind a signal for tracking api calls; used by debug panel
if Signal is not None:
    api_called = Signal(providing_args=[
        "time_taken", "method", "url", "args", "kwargs"])
else:
    api_called = None


class HTTP_API_Base(object):

    def __init__(self, base_url, username=None, password=None, retries=None):
        # standardize url format; ensure we have a trailing slash,
        # adding one if necessary
        if not base_url.endswith('/'):
            base_url = base_url + '/'

        # create a new session and add to global sessions
        self.session = requests.Session()
        # Set headers to be passed with every request
        # NOTE: only headers that will be common for *all* requests
        # to this fedora should be set in the session
        # (i.e., do NOT include auth information here)

        # NOTE: ssl verification is turned on by default

        self.session.headers = {
            # use requests-toolbelt user agent
            'User-Agent': user_agent('eulfedora', eulfedora_version),
        }
        # no retries is requests current default behavior, so only
        # customize if a value is set
        if retries is not None:
            adapter = requests.adapters.HTTPAdapter(max_retries=retries)
            self.session.mount('http://', adapter)
            self.session.mount('https://', adapter)

        self.base_url = base_url
        self.username = username
        self.password = password
        self.request_options = {}
        if self.username is not None:
            # store basic auth option to pass when making requests
            self.request_options['auth'] = (self.username, self.password)

    def absurl(self, rel_url):
        return urljoin(self.base_url, rel_url)

    def prep_url(self, url):
        return self.absurl(url)

    # thinnest possible wrappers around requests calls
    # - add auth, make urls absolute

    def _make_request(self, reqmeth, url, *args, **kwargs):
        # copy base request options and update with any keyword args
        rqst_options = self.request_options.copy()
        rqst_options.update(kwargs)
        start = time.time()
        response = reqmeth(self.prep_url(url), *args, **rqst_options)
        total_time = time.time() - start
        logger.debug('%s %s=>%d: %f sec', reqmeth.__name__.upper(), url,
                     response.status_code, total_time)

        # if django signals are available, send api called
        if api_called is not None:
            api_called.send(sender=self.__class__, time_taken=total_time,
                            method=reqmeth, url=url, response=response,
                            args=args, kwargs=kwargs)

        # NOTE: currently doesn't do anything with 3xx  responses
        # (likely handled for us by requests)
        if response.status_code >= requests.codes.bad:  # 400 or worse
            # separate out 401 and 403 (permission errors) to enable
            # special handling in client code.
            if response.status_code in (requests.codes.unauthorized,
                                        requests.codes.forbidden):
                raise PermissionDenied(response)
            elif response.status_code == requests.codes.server_error:
                # check response content to determine if this is a
                # ChecksumMismatch or a more generic error
                if 'Checksum Mismatch' in response.text:
                    raise ChecksumMismatch(response)
                else:
                    raise RequestFailed(response)
            else:
                raise RequestFailed(response)
        return response

    def get(self, *args, **kwargs):
        return self._make_request(self.session.get, *args, **kwargs)

    def head(self, *args, **kwargs):
        return self._make_request(self.session.head, *args, **kwargs)

    def put(self, *args, **kwargs):
        return self._make_request(self.session.put, *args, **kwargs)

    def post(self, *args, **kwargs):
        return self._make_request(self.session.post, *args, **kwargs)

    def delete(self, *args, **kwargs):
        return self._make_request(self.session.delete, *args, **kwargs)

    # also available: head, patch


class REST_API(HTTP_API_Base):
    """Python object for accessing
    `Fedora's REST API <https://wiki.duraspace.org/display/FEDORA38/REST+API>`_.

    Most methods return an HTTP :class:`requests.models.Response`, which
    provides access to status code and headers as well as content.  Many
    responses with XML content can be loaded using models in
    :mod:`eulfedora.xml`.
    """

    # always return xml response instead of html version
    format_xml = {'format': 'xml'}

    ### API-A methods (access) ####
    # describeRepository not implemented in REST, use API-A-LITE version

    def findObjects(self, query=None, terms=None, pid=True, chunksize=None, session_token=None):
        """
        Wrapper function for `Fedora REST API findObjects <http://fedora-commons.org/confluence/display/FCR30/REST+API#RESTAPI-findObjects>`_
        and `Fedora REST API resumeFindObjects <http://fedora-commons.org/confluence/display/FCR30/REST+API#RESTAPI-resumeFindObjects>`_

        One and only one of query or terms must be specified.

        :param query: string of fields and terms to search for
        :param terms: phrase search across all fields
        :param pid: include pid in search results
        :param chunksize: number of objects to return at a time
        :param session_token: get an additional chunk of results from a prior search
        :param parse: optional data parser function; defaults to returning
                      raw string data
        :rtype: :class:`requests.models.Response`
        """
        if query is not None and terms is not None:
            raise Exception("Cannot findObject with both query ('%s') and terms ('%s')" % (query, terms))

        http_args = {'resultFormat': 'xml'}