# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import inspect

from django.urls import reverse
from django.utils.dateparse import parse_date
from django.utils.safestring import mark_safe

# make APIError available from this module
from .jsonresponse import APIError  # noqa pylint: disable=unused-import


class endpoint:
    do_not_call_in_templates = True

    def __init__(
        self,
        serializer_type='json-api',
        perm=None,
        methods=None,
        name=None,
        pattern=None,
        wrap_response=False,
        description=None,
        description_get=None,
        description_put=None,
        description_post=None,
        description_patch=None,
        description_delete=None,
        long_description=None,
        long_description_get=None,
        long_description_put=None,
        long_description_post=None,
        long_description_patch=None,
        long_description_delete=None,
        example_pattern=None,
        parameters=None,
        cache_duration=None,
        post=None,
        show=True,
        show_undocumented_params=True,
        display_order=0,
        display_category='',
        json_schema_response=None,
        datasource=False,
        # helper to define the POST json schema
        post_json_schema=None,
    ):
        self.perm = perm
        self.methods = methods or ['get']
        self.serializer_type = serializer_type
        self.pattern = pattern
        self.name = name
        self.wrap_response = wrap_response
        self.descriptions = {
            'get': description_get or description,
            'put': description_put or description,
            'post': description_post or description,
            'patch': description_patch or description,
            'delete': description_delete or description,
        }
        self.long_descriptions = {
            'get': long_description_get or long_description,
            'put': long_description_put or long_description,
            'post': long_description_post or long_description,
            'patch': long_description_patch or long_description,
            'delete': long_description_delete or long_description,
        }
        self.example_pattern = example_pattern
        self.parameters = parameters or {}
        self.cache_duration = cache_duration
        if post_json_schema:
            post = post or {}
            schema = post.setdefault('request_body', {}).setdefault('schema', {})
            assert not schema.get('application/json'), 'a json schema was already set in the post argument'
            schema['application/json'] = post_json_schema
        self.post = post
        if post:
            self.methods = ['post']
            if post.get('description'):
                self.descriptions['post'] = post.get('description')
            if post.get('long_description'):
                self.long_descriptions['post'] = post.get('long_description')
        self.show = show
        self.show_undocumented_params = show_undocumented_params
        self.display_order = display_order
        self.display_category = display_category
        self.response_schemas = {}
        if json_schema_response:
            self.response_schemas['application/json'] = json_schema_response
        self.datasource = datasource

    def __call__(self, func):
        func.endpoint_info = self
        if not self.name:
            self.name = func.__name__
        self.func = func
        return func

    @property
    def display_category_order(self):
        if not self.display_category:
            # no category, put it at the end
            return 99999999
        # self.object is attached in BaseResource.get_endpoints_infos method
        if self.display_category not in self.object._category_ordering:
            # category without ordering, put it at the end, just before no category
            return 99999998
        return self.object._category_ordering.index(self.display_category)

    def get_example_params(self):
        return {
            x: self.parameters[x]['example_value']
            for x in self.parameters or {}
            if x in self.parameters and 'example_value' in self.parameters[x]
        }

    def get_query_parameters(self):
        query_parameters = []
        for param, param_value in self.get_example_params().items():
            if param in (self.example_pattern or ''):
                continue
            query_parameters.append((param, param_value))
        return query_parameters

    def example_url(self):
        kwargs = {
            'connector': self.object.get_connector_slug(),
            'slug': self.object.slug,
            'endpoint': self.name,
        }
        if self.example_pattern:
            kwargs['rest'] = self.example_pattern.format(**self.get_example_params())

        query_string = ''
        query_parameters = self.get_query_parameters()
        if query_parameters:
            query_string = '?' + '&'.join(['%s=%s' % x for x in query_parameters])

        return reverse('generic-endpoint', kwargs=kwargs) + query_string

    def example_url_as_html(self):
        kwargs = {
            'connector': self.object.get_connector_slug(),
            'slug': self.object.slug,
            'endpoint': self.name,
        }
        if self.example_pattern:
            kwargs['rest'] = self.example_pattern.format(
                **{x: '$%s$' % x for x in self.get_example_params().keys()}
            )

        url = reverse('generic-endpoint', kwargs=kwargs)
        for param in self.get_example_params():
            url = url.replace('$%s$' % param, '<i class="varname">%s</i>' % param)

        query_string = ''
        query_parameters = self.get_query_parameters()
        if query_parameters:
            query_string = '?' + '&amp;'.join(
                ['%s=<i class="varname">%s</i>' % (x[0], x[0]) for x in query_parameters]
            )

        return mark_safe(url + query_string)

    def has_params(self):
        argspec = inspect.getfullargspec(self.func)
        return len(argspec.args) > 2  # (self, request)

    @property
    def description(self):
        return self.descriptions.get(self.http_method)

    @property
    def long_description(self):
        return self.long_descriptions.get(self.http_method)

    @property
    def body_schemas(self):
        if (
            self.http_method == 'post'
            and self.post
            and 'request_body' in self.post
            and 'schema' in self.post['request_body']
        ):
            return self.post['request_body']['schema']
        return {}

    def get_params(self):
        def type_to_str(value):
            if isinstance(value, bool):
                return 'boolean'
            elif isinstance(value, int):
                return 'integer'
            elif isinstance(value, float):
                return 'float'
            elif isinstance(value, str):
                try:
                    if parse_date(value):
                        return 'date'
                except ValueError:
                    pass

        params = []
        available_params = self.parameters
        spec = inspect.getfullargspec(self.func)
        defaults = dict(zip(reversed(spec.args), reversed(spec.defaults or [])))
        if self.show_undocumented_params:
            available_params = {
                arg: {} for arg in spec.args[2:] if arg != 'post_data' and not arg in self.parameters
            }
            available_params.update(self.parameters)
        for param, info in available_params.items():
            param_info = {'name': param}
            if info.get('description'):
                param_info['description'] = info['description']
            if 'blank' in info:
                param_info['blank'] = bool(info['blank'])
            typ = None
            if 'type' in info:
                typ = info['type']
                if typ == 'int':
                    typ = 'integer'
                elif typ == 'bool':
                    typ = 'boolean'
            elif 'example_value' in info:
                typ = type_to_str(info['example_value'])
            if param in defaults:
                param_info['optional'] = True
                param_info['default_value'] = defaults[param]
                if not typ:
                    typ = type_to_str(defaults[param])
            if info.get('optional') is True:
                param_info['optional'] = True
            if typ:
                param_info['type'] = typ
            params.append(param_info)
        order = {name: i for i, name in enumerate(spec.args)}
        params.sort(key=lambda x: (order.get(x['name'], 999), x['name']))
        return params
