# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2017  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 json
import uuid

from django.contrib.postgres.fields import JSONField
from django.db import models
from django.template import Context, Template
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource
from passerelle.utils.api import APIError, endpoint
from passerelle.utils.conversion import simplify


def get_hex_uuid():
    return uuid.uuid4().hex


def clean_json_data(data):
    try:
        payload = json.loads(data)
        if not isinstance(payload, dict):
            raise APIError('payload must be a dict')
        return payload
    except ValueError:
        raise APIError('could not decode body to json')


class JsonData(models.Model):
    datastore = models.ForeignKey('JsonDataStore', null=True, on_delete=models.CASCADE)

    uuid = models.CharField(_('uuid'), max_length=32, default=get_hex_uuid, editable=False, unique=True)
    name_id = models.CharField(max_length=256, blank=True)
    content = JSONField(_('Content'))
    text = models.CharField(max_length=256, blank=True)

    creation_datetime = models.DateTimeField(auto_now_add=True)
    last_update_datetime = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        text_value_template = self.datastore.text_value_template
        if text_value_template:
            template = Template(text_value_template)
            context = Context(self.content)
            self.text = template.render(context).strip()
        return super().save(*args, **kwargs)

    def to_json(self):
        return {
            'id': self.uuid,
            'text': self.text,
            'content': self.content,
            'creation_datetime': self.creation_datetime,
            'last_update_datetime': self.last_update_datetime,
        }


class JsonDataStore(BaseResource):
    category = _('Data Sources')
    documentation_url = (
        'https://doc-publik.entrouvert.com/admin-fonctionnel/parametrage-avance/json-data-store/'
    )
    text_value_template = models.CharField(_('Template for "text" key value'), max_length=256, blank=True)

    class Meta:
        verbose_name = _('JSON Data Store')

    @endpoint(
        perm='can_access',
        name='data',
        pattern=r'$',
        description=_('Listing'),
        long_description=_('More filtering on attributes is possible using "key=val" additionals parameters'),
        parameters={
            'name_id': {'description': _('Object identifier'), 'example_value': '12345'},
            'q': {'description': _('Filter on "text" key value'), 'example_value': 'rue du chateau'},
        },
    )
    def list(self, request, name_id=None, q=None, **kwargs):
        objects = JsonData.objects.filter(datastore=self).order_by('text')
        if name_id is not None:
            objects = objects.filter(name_id=name_id)
        if q:
            q = simplify(q)
            objects = [o for o in objects if q in simplify(o.text)]
        for key, value in kwargs.items():
            objects = [o for o in objects if o.content.get(key) == value]
        return {'data': [x.to_json() for x in objects]}

    @endpoint(
        perm='can_access',
        methods=['post'],
        name='data',
        pattern=r'create$',
        example_pattern='create',
        description=_('Create'),
    )
    def create(self, request, name_id=None, **kwargs):
        content = clean_json_data(request.body)
        attrs = {
            'content': content,
            'datastore': self,
        }
        if name_id is not None:
            attrs['name_id'] = name_id
        data = JsonData(**attrs)
        data.save()
        return {'id': data.uuid, 'text': data.text}

    def get_data_object(self, uuid, name_id=None):
        attrs = {
            'uuid': uuid,
            'datastore': self,
        }
        if name_id is not None:
            attrs['name_id'] = name_id
        return JsonData.objects.get(**attrs)

    @endpoint(
        perm='can_access',
        methods=['get', 'post', 'patch'],
        name='data',
        pattern=r'(?P<uuid>\w+)/$',
        example_pattern='{uuid}/',
        description_get=_('Get'),
        description_post=_('Replace'),
        description_patch=_('Update'),
        parameters={'uuid': {'description': _('Object identifier'), 'example_value': '12345'}},
    )
    def get_or_replace(self, request, uuid, name_id=None):
        data = self.get_data_object(uuid, name_id)
        if request.method == 'POST':
            new_content = clean_json_data(request.body)
            data.content = new_content
            data.save()
        elif request.method == 'PATCH':
            new_content = clean_json_data(request.body)
            data.content.update(new_content)
            data.save()
        return data.to_json()

    @endpoint(
        perm='can_access',
        methods=['post'],
        name='data',
        description=_('Delete'),
        pattern=r'(?P<uuid>\w+)/delete$',
        example_pattern='{uuid}/delete',
        parameters={'uuid': {'description': _('Object identifier'), 'example_value': '12345'}},
    )
    def delete_(self, request, uuid, name_id=None):
        # delete() would collide with Model.delete()
        self.get_data_object(uuid, name_id).delete()
        return {}

    @endpoint(
        perm='can_access',
        name='data',
        pattern=r'by/(?P<attribute>[\w-]+)/$',
        example_pattern='by/{attribute}/',
        description=_('Get a single object by attribute'),
        parameters={
            'attribute': {'description': _('Attribute name'), 'example_value': 'code'},
            'value': {'description': _('Attribute value'), 'example_value': '12345'},
        },
    )
    def get_by_attribute(self, request, attribute, value, name_id=None):
        objects = JsonData.objects.filter(datastore=self)
        if name_id is not None:
            objects = objects.filter(name_id=name_id)
        objects = objects.filter(content__contains={attribute: value})
        data = objects.first()
        if data is not None:
            return data.to_json()
        raise APIError('no such object')
