"""REST Handler."""
import math
from asyncio import iscoroutine
from urllib.parse import urlencode

from aiohttp.web import StreamResponse, Response
from muffin.handler import Handler
from muffin.utils import json, dumps

from .filters import Filters
from .exceptions import RESTNotFound, RESTBadRequest


VAR_PAGE = 'page'
VAR_PER_PAGE = 'per_page'
VAR_SORT = 'sort'
VAR_WHERE = 'where'


class RESTOptions(object):

    """Prepare resource options."""

    def __init__(self, cls, **params):
        """Process meta options."""
        # Store link to self.meta
        self.meta = getattr(cls, "Meta", None)

        self.cls = cls

        # Inherit meta from parents
        for base in reversed(cls.mro()):
            if not hasattr(base, "Meta"):
                continue

            for k, v in base.Meta.__dict__.items():
                if k.startswith('__'):
                    continue
                setattr(self, k, v)

        # Generate name
        cls.name = getattr(cls, 'name', None) or cls.__name__.lower().split('resource', 1)[0]

        self.per_page = int(self.per_page or 0)

        # Setup schema_meta
        self.schema_meta = self.schema_meta or {
            k[7:]: self.__dict__[k] for k in self.__dict__
            if k.startswith('schema_') and not k == 'schema_meta'
        }

        # Setup filters
        self.filters = self.filters_converter(*self.filters, handler=cls)

        # Setup sorting
        if not isinstance(self.sorting, dict):
            self.sorting = dict(
                n if isinstance(n, (list, tuple)) else (n, n) for n in self.sorting)

    def __repr__(self):
        """String representation."""
        return "<Options %s>" % self.cls


class RESTHandlerMeta(type(Handler)):

    """Create options class."""

    def __new__(mcs, name, bases, params):
        """Initialize options class."""
        params_ = dict(params)
        cls = super(RESTHandlerMeta, mcs).__new__(mcs, name, bases, params)
        cls.meta = cls.OPTIONS_CLASS(cls, **params_)
        return cls


class RESTHandler(Handler, metaclass=RESTHandlerMeta):

    """Implement a common handler for REST operations."""

    OPTIONS_CLASS = RESTOptions

    Schema = None

    class Meta:

        """Default options."""

        # per_page: Paginate results (set to None for disable pagination)
        per_page = None
        page_links = False

        # Resource filters
        filters = ()

        # Filters converter class
        filters_converter = Filters

        # Define allowed resource sorting params
        sorting = ()

        # Redefine Schema.Meta completely
        schema_meta = None

    def __init__(self, *args, **kwargs):
        """Init the resource state."""
        super(RESTHandler, self).__init__(*args, **kwargs)
        self.filters = self.auth = self.collection = None

    @classmethod
    def bind(cls, app, *paths, methods=None, name=None, **kwargs):
        """Bind to the application.

        Generate URL, name if it's not provided.
        """
        paths = paths or ['/%s(/{%s})?/?' % (cls.name, cls.name)]
        name = name or "api.%s" % cls.name
        return super(RESTHandler, cls).bind(app, *paths, methods=methods, name=name, **kwargs)

    async def dispatch(self, request, view=None, **kwargs):
        """Process request."""
        # Authorization endpoint
        self.auth = await self.authorize(request, **kwargs)  # noqa

        # Load collection
        self.collection = await self.get_many(request, **kwargs)

        if request.method == 'POST' and view is None:
            return await super(RESTHandler, self).dispatch(request, **kwargs)

        # Load resource
        resource = await self.get_one(request, **kwargs)

        headers = {}

        if request.method == 'GET' and resource is None:

            # Filter resources
            if VAR_WHERE in request.query:
                self.collection = await self.filter(request, **kwargs)

            # Sort resources
            if VAR_SORT in request.query:
                sorting = [
                    (name.strip('-'), name.startswith('-'))
                    for name in request.query[VAR_SORT].split(',')
                    if name.strip('-') in self.meta.sorting]

                self.collection = await self.sort(*sorting, **kwargs)

            # Paginate resources
            per_page = request.query.get(VAR_PER_PAGE, self.meta.per_page)
            if per_page:
                try:
                    per_page = int(per_page)
                    page = int(request.query.get(VAR_PAGE, 0))
                    if per_page and page >= 0:
                        offset = page * per_page
                        self.collection, total = await self.paginate(request, offset, per_page)
                        headers = make_pagination_headers(
                            request, per_page, page, total, self.meta.page_links)
                except ValueError:
                    raise RESTBadRequest(reason='Pagination params are invalid.')

        response = await super(RESTHandler, self).dispatch(
            request, resource=resource, view=view, **kwargs)
        response.headers.update(headers)
        return response

    async def make_response(self, request, response, **response_kwargs):
        """Convert a handler result to web response."""
        while iscoroutine(response):
            response = await response

        if isinstance(response, StreamResponse):
            return response

        response_kwargs.setdefault('content_type', 'application/json')
        return Response(text=dumps(response), **response_kwargs)

    async def authorize(self, request, **kwargs):
        """Base point for authorization."""
        return True

    async def get_many(self, request, **kwargs):
        """Base point for collect data."""
        return []

    async def get_one(self, request, **kwargs):
        """Load resource."""
        return request.match_info.get(self.name)

    async def filter(self, request, **kwargs):
        """Filter collection."""
        try:
            data = json.loads(request.query.get(VAR_WHERE))
        except (ValueError, TypeError):
            return self.collection

        self.filters, collection = self.meta.filters.filter(
            data, self.collection, resource=self, **kwargs)

        return collection

    async def sort(self, *sorting, **kwargs):
        """Sort collection."""
        return self.collection

    async def paginate(self, request, offset=0, limit=0):
        """Paginate collection.

        :param request: client's request
        :param offset: current offset
        :param limit: limit items per page
        :returns: (paginated collection, count of resources)
        """
        return self.collection[offset: offset + limit], len(self.collection)

    async def get(self, request, resource=None, **kwargs):
        """Get resource or collection of resources.

        ---
        parameters:
            - name: resource
              in: path
              type: string

        """
        if resource is not None and resource != '':
            return self.to_simple(request, resource, **kwargs)

        return self.to_simple(request, self.collection, many=True, **kwargs)

    def to_simple(self, request, data, many=False, **kwargs):
        """Serialize response to simple object (list, dict)."""
        schema = self.get_schema(request, **kwargs)
        return schema.dump(data, many=many).data if schema else data

    def get_schema(self, request, resource=None, **kwargs):
        """Create schema instance."""
        return self.Schema and self.Schema()

    async def parse(self, request):
        """Catch an exception from request parsing."""
        try:
            return await super(RESTHandler, self).parse(request)
        except ValueError as exc:
            raise RESTBadRequest(reason=str(exc))

    async def post(self, request, resource=None, **kwargs):
        """Create a resource."""
        resource = await self.load(request, resource=resource, **kwargs)
        resource = await self.save(request, resource=resource, **kwargs)
        return self.to_simple(request, resource, many=isinstance(resource, list), **kwargs)

    async def load(self, request, resource=None, **kwargs):
        """Load resource from given data."""
        data = await self.parse(request)

        schema = self.get_schema(request, resource=resource, **kwargs)
        if not schema:
            return data

        resource, errors = schema.load(
            data, partial=resource is not None, many=isinstance(data, list))
        if errors:
            raise RESTBadRequest(reason='Bad request', json={'errors': errors})
        return resource

    async def save(self, request, resource=None, **kwargs):
        """Create a resource."""
        return resource

    async def put(self, request, resource=None, **kwargs):
        """Update a resource.

        ---
        parameters:
            - name: resource
              in: path
              type: string
        """
        if resource is None:
            raise RESTNotFound(reason='Resource not found')

        return await self.post(request, resource=resource, **kwargs)

    patch = put

    async def delete(self, request, resource=None, **kwargs):
        """Delete a resource."""
        if resource is None:
            raise RESTNotFound(reason='Resource not found')
        self.collection.remove(resource)

    async def batch(self, request):
        """Make group operations."""


def make_pagination_headers(request, limit, curpage, total, links=False):
    """Return Link Hypermedia Header."""
    lastpage = math.ceil(total / limit) - 1
    headers = {'X-Total-Count': str(total), 'X-Limit': str(limit),
               'X-Page-Last': str(lastpage), 'X-Page': str(curpage)}
    if links:
        base = "{}?%s".format(request.path)
        links = {}
        links['first'] = base % urlencode(dict(request.query, **{VAR_PAGE: 0}))
        links['last'] = base % urlencode(dict(request.query, **{VAR_PAGE: lastpage}))
        if curpage:
            links['prev'] = base % urlencode(dict(request.query, **{VAR_PAGE: curpage - 1}))
        if curpage < lastpage:
            links['next'] = base % urlencode(dict(request.query, **{VAR_PAGE: curpage + 1}))
        headers['Link'] = ",".join(['<%s>; rel="%s"' % (v, n) for n, v in links.items()])
    return headers


#  pylama:ignore=W0201
