# -*- coding: utf-8 -*-

from urllib.parse import unquote

import os
import requests
import tempfile
import shutil

try:
    from urllib import quote_plus
except ImportError:
    from urllib.parse import quote_plus

from ..client import client, _handle_api_error, _handle_request_error
from ..utils.tabulate import tabulate
from ..utils.printing import pager
# from solvebio.errors import NotFoundError
from ..errors import NotFoundError

from .util import class_to_api_name
from .solveobject import SolveObject, convert_to_solve_object


class APIResource(SolveObject):
    """Abstract Class for an API Resource"""

    @classmethod
    def retrieve(cls, id, **params):
        instance = cls(id, **params)
        instance.refresh()
        return instance

    def refresh(self):
        self.refresh_from(self.request('get', self.instance_url()))
        return self

    @classmethod
    def class_name(cls):
        if cls == APIResource:
            raise NotImplementedError(
                'APIResource is an abstract class.  You should perform '
                'actions on its subclasses (e.g. Vault, Object, Dataset)')
        return str(quote_plus(cls.__name__))

    @classmethod
    def class_url(cls):
        """Returns a versioned URI string for this class"""
        base = 'v{0}'.format(getattr(cls, 'RESOURCE_VERSION', '1'))
        return "/{0}/{1}".format(base, class_to_api_name(cls.class_name()))

    def instance_url(self):
        """Get instance URL by ID"""
        id_ = self.get(self.ID_ATTR)
        base = self.class_url()

        if id_:
            return '/'.join([base, str(id_)])
        else:
            raise Exception(
                'Could not determine which URL to request: %s instance '
                'has invalid ID: %r' % (type(self).__name__, id_),
                self.ID_ATTR)


class ListObject(SolveObject):

    def all(self, **params):
        """Lists all items in a class that you have access to"""
        return self.request('get', self['url'], params=params)

    def create(self, **params):
        return self.request('post', self['url'], data=params)

    def first_page(self, **params):
        url = getattr(self, '_first_page_url', self['url'])
        return self.request('get', url, params=params)

    def next_page(self, **params):
        if self['links']['next']:
            return self.request('get', self['links']['next'], params=params)
        return None

    def prev_page(self, **params):
        if self['links']['prev']:
            return self.request('get', self['links']['prev'], params=params)
        return None

    def solve_objects(self):
        return convert_to_solve_object(self['data'], client=self._client)

    def set_tabulate(self, fields, **kwargs):
        self._tabulate = lambda data: \
            tabulate([[d[i] for i in fields] for d in data], **kwargs)

    def __len__(self):
        return self['total']

    def __str__(self):
        if getattr(self, '_tabulate', None):
            return '\n' + self._tabulate(self['data'])
        return super(ListObject, self).__str__()

    def __iter__(self):
        self._i = 0

        # Store the URL of the first page if we haven't yet.
        self._first_page_url = getattr(self, '_first_page_url', self['url'])
        if self._first_page_url != self['url']:
            self.refresh_from(self.first_page())

        return self

    def __next__(self):
        """Python 3"""
        return self.next()

    def next(self):
        if not getattr(self, '_i', None):
            self._i = 0

        if self._i >= len(self['data']):
            # get the next page of results
            next_page = self.next_page()
            if next_page is None:
                raise StopIteration
            self.refresh_from(next_page)
            self._i = 0

        obj = convert_to_solve_object(self['data'][self._i],
                                      client=self._client)
        self._i += 1
        return obj


class SingletonAPIResource(APIResource):

    @classmethod
    def retrieve(cls, **kwargs):
        _client = kwargs.pop('client', None) or cls._client or client
        return super(SingletonAPIResource, cls).retrieve(None, client=_client)

    @classmethod
    def class_url(cls):
        """
        Returns a versioned URI string for this class,
        and don't pluralize the class name.
        """
        base = 'v{0}'.format(getattr(cls, 'RESOURCE_VERSION', '1'))
        return "/{0}/{1}".format(base, class_to_api_name(
            cls.class_name(), pluralize=False))

    def instance_url(self):
        return self.class_url()


class CreateableAPIResource(APIResource):

    @classmethod
    def create(cls, **params):
        _client = params.pop('client', None) or cls._client or client
        url = cls.class_url()
        response = _client.post(url, data=params)
        return convert_to_solve_object(response, client=_client)


class DeletableAPIResource(APIResource):

    def delete(self, **params):
        printable_name = class_to_api_name(
            self.class_name(), pluralize=False).replace('_', ' ')
        if not params.pop('force', False):
            res = input('Are you sure you want to delete this %s? '
                        '[y/N] ' % printable_name)
            if res.strip().lower() != 'y':
                print('Not performing deletion.')
                return

        response = self.request('delete', self.instance_url(), params=params)
        return convert_to_solve_object(response, client=self._client)


class DownloadableAPIResource(APIResource):

    def download(self, path=None, **kwargs):
        """
        Download the file to the specified directory or file path.
        Downloads to a temporary directory if no path is specified.

        Returns the absolute path to the file.
        """
        download_url = self.download_url(**kwargs)
        try:
            # For vault objects, use the object's filename
            # as the fallback if none is specified.
            filename = self.filename
        except AttributeError:
            # If the object has no filename attribute,
            # extract one from the download URL.
            filename = download_url.split('%3B%20filename%3D')[1]
            # Remove additional URL params from the name and "unquote" it.
            filename = unquote(filename.split('&')[0])

        if path:
            path = os.path.expanduser(path)
            # If the path is a dir, use the extracted filename
            if os.path.isdir(path):
                path = os.path.join(path, filename)
        else:
            # Create a temporary directory for the file
            path = os.path.join(tempfile.gettempdir(), filename)

        try:
            # use streaming to prevent automatic decompression
            response = requests.request(method='get', url=download_url, stream=True)
        except Exception as e:
            _handle_request_error(e)

        if not (200 <= response.status_code < 400):
            _handle_api_error(response)

        with open(path, 'wb') as fileobj:
            if filename.endswith('.gz'):
                # Don't automatically decompress gzipped files
                shutil.copyfileobj(response.raw, fileobj)
            else:
                for chunk in response.iter_content(chunk_size=1024 * 8):
                    if chunk:
                        fileobj.write(chunk)

        return path

    def download_url(self, **kwargs):
        download_url = self.instance_url()
        if 'version_id' in kwargs:
            download_url += '/versions/{}'.format(kwargs['version_id'])
        else:
            download_url += '/download'
        # Don't redirect, just return the signed S3 URL
        kwargs.update({'redirect': ''})
        response = self.request(
            'get', download_url, params=kwargs, allow_redirects=False)

        return response.get('url') or response.get('download_url')


class ListableAPIResource(APIResource):
    """Has one method: *all()* which lists everything in the resource."""

    @classmethod
    def all(cls, **params):
        _client = params.pop('client', None) or cls._client or client
        url = cls.class_url()
        response = _client.get(url, params)
        results = convert_to_solve_object(response, client=_client)

        # If the object has LIST_FIELDS, setup tabulate
        if len(results.data) > 0:
            list_fields = getattr(results.data[0], 'LIST_FIELDS', None)
            if list_fields:
                fields, headers = list(zip(*list_fields))
                results.set_tabulate(fields, headers=headers, sort=False)
        return results

    @classmethod
    def _retrieve_helper(cls, model_name, field_name, error_value, **params):
        _client = params.pop('client', None) or cls._client or client
        url = cls.class_url()
        response = _client.get(url, params)
        results = convert_to_solve_object(response, client=_client)
        objects = results.data
        allow_multiple = params.pop('allow_multiple', None)
        if len(objects) > 1:
            if allow_multiple:
                return objects
            else:
                raise Exception('Multiple {0}s found with {1} "{2}"'
                                .format(model_name, field_name, error_value))
        elif len(objects) == 1:
            return objects[0]
        else:
            raise NotFoundError('No {0} found with {1} "{2}"'
                                .format(model_name, field_name, error_value))

    @classmethod
    def pager(cls, **params):
        return pager(cls.all, **params)

    def __repr__(self):
        return tabulate(list(self.items()), ['Fields', 'Data'],
                        aligns=['right', 'left'], sort=True)


class SearchableAPIResource(APIResource):

    @classmethod
    def search(cls, query='', **params):
        _client = params.pop('client', None) or cls._client or client
        params.update({'q': query})
        url = cls.class_url()
        response = _client.get(url, params)
        results = convert_to_solve_object(response, client=_client)

        # If the object has LIST_FIELDS, setup tabulate
        if len(results.data) > 0:
            list_fields = getattr(results.data[0], 'LIST_FIELDS', None)
            if list_fields:
                fields, headers = list(zip(*list_fields))
                results.set_tabulate(fields, headers=headers)

        return results


class UpdateableAPIResource(APIResource):

    def save(self):
        self.refresh_from(self.request('patch', self.instance_url(),
                                       data=self.serialize(self)))
        return self

    def serialize(self, obj):
        params = {}
        unsaved_values = obj._unsaved_values

        # adding 'metadata' attribute to be serialized
        if getattr(obj, 'metadata', None) is not None:
            unsaved_values.add('metadata')

        for k in unsaved_values:
            if k == 'id' or k == self.ID_ATTR:
                continue

            attr = getattr(obj, k)

            if isinstance(attr, SolveObject):
                params[k] = dict(attr)
            else:
                params[k] = attr

        return params
