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

from django.contrib.postgres.fields import JSONField
from django.core.exceptions import PermissionDenied
from django.db import models
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.template import Context, Template
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource
from passerelle.utils import get_trusted_services
from passerelle.utils.api import endpoint
from passerelle.utils.origin import get_url_origin, is_same_origin

from . import fc

# from passerelle.utils.jsonresponse import APIError


class Resource(BaseResource):
    category = _('Data Sources')

    fc_platform_slug = models.CharField(
        _('FranceConnect platform'),
        max_length=4,
        choices=[(platform.slug, platform.name) for platform in fc.PLATFORMS],
    )

    fc_client_id = models.CharField(_('FranceConnect client_id'), max_length=64)

    fc_client_secret = models.CharField(_('FranceConnect client_secret'), max_length=64)

    fc_scopes = models.TextField(_('FranceConnect scopes'), default='identite_pivot')

    text_template = models.TextField(
        _('Text template'),
        default=(
            '''{{ fc.given_name }} {{ fc.family_name }} '''
            '''{% if fc.gender == 'male' %}né{% else %}née{% endif %} le {{ fc.birthdate }}'''
        ),
    )

    dgfip_api_base_url = models.URLField(
        _('DGFIP API base URL'), max_length=256, default='https://gwfc.dgfip.finances.gouv.fr/'
    )

    dgfip_username = models.CharField(_('DGFIP API Username'), max_length=64, blank=True, null=True)

    dgfip_password = models.CharField(_('DGFIP API Password'), max_length=64, blank=True, null=True)

    dgfip_scopes = models.TextField(_('DGFIP API Scopes'), blank=True, null=True)

    dgfip_id_teleservice = models.TextField(_('DGFIP API ID_Teleservice'), blank=True, null=True)

    log_requests_errors = False

    class Meta:
        verbose_name = _('Data sources through FranceConnect')

    @property
    def fc_platform(self):
        return fc.PLATFORMS_BY_SLUG[self.fc_platform_slug]

    def build_callback_url(self, request, **kwargs):
        redirect_uri = request.build_absolute_uri(
            reverse(
                'generic-endpoint',
                kwargs={'slug': self.slug, 'connector': self.get_connector_slug(), 'endpoint': 'callback'},
            )
        )
        if kwargs:
            redirect_uri += '?' + urlencode(
                {key: value for key, value in kwargs.items() if value is not None}
            )
        return redirect_uri

    def is_trusted_origin(self, request, origin):
        for service in get_trusted_services():
            if is_same_origin(origin, service['url']):
                return True

        if is_same_origin(request.build_absolute_uri(), origin):
            return True

        return False

    @endpoint(
        description=_('Init request'),
        parameters={
            'mode': {
                'description': _('What to retrieve, default to FranceConnect identity, can be "dgfip"'),
            },
            'origin': {
                'description': _('Origin for returning results through window.postMessage'),
            },
            'test': {
                'description': _('If set to one, activate the test callback view.'),
            },
        },
    )
    def init_request(self, request, origin, mode=None, test=None):
        if not request.user.is_superuser and not self.is_trusted_origin(request, origin):
            return HttpResponseBadRequest('Missing or invalid origin')

        redirect_uri = self.build_callback_url(request, origin=origin, mode=mode, test=test)
        franceconnect = fc.FranceConnect(
            session=self.requests, logger=self.logger, dgfip_api_base_url=self.dgfip_api_base_url
        )
        return HttpResponseRedirect(
            franceconnect.authorization_request(
                platform=self.fc_platform,
                client_id=self.fc_client_id,
                scopes=self.fc_scopes,
                redirect_uri=redirect_uri,
            )
        )

    @endpoint(
        description=_('FranceConnect callback (internal use)'),
        parameters={
            'origin': {
                'description': _('HTTP Origin, needed to secure window.postMessage'),
            },
            'mode': {
                'description': _('Mode'),
            },
            'test': {
                'description': _('Use test mode (to see exchanges)'),
            },
        },
    )
    def callback(self, request, origin, mode=None, test=None, **kwargs):
        if not request.user.is_superuser and not self.is_trusted_origin(request, origin):
            return HttpResponseBadRequest('Missing or invalid origin.')

        if test and not request.user.is_superuser:
            return HttpResponseBadRequest('Only admin can use test mode.')

        franceconnect = fc.FranceConnect(
            session=self.requests, logger=self.logger, dgfip_api_base_url=self.dgfip_api_base_url
        )
        redirect_uri = self.build_callback_url(request, origin=origin, mode=mode, test=test)
        context = {
            'origin': origin,
            'franceconnect': franceconnect,
            'redirect_uri': redirect_uri,
            'test': test,
        }
        try:
            franceconnect.handle_authorization_response(
                platform=self.fc_platform,
                client_id=self.fc_client_id,
                client_secret=self.fc_client_secret,
                redirect_uri=redirect_uri,
                code=request.GET.get('code'),
                error=request.GET.get('error'),
                error_description=request.GET.get('error_description'),
            )
            token = {'franceconnect': franceconnect.fc_user_info}
            if mode == 'dgfip':
                franceconnect.request_dgfip_access_token(
                    self.dgfip_username, self.dgfip_password, scope=self.dgfip_scopes
                )
                current_year = now().year
                for year in range(current_year - 3, current_year):
                    franceconnect.request_dgfip_ir(str(year), id_teleservice=self.dgfip_id_teleservice)
                franceconnect.request_dgfip_last_known_ir()
                token['dgfip_ir'] = franceconnect.dgfip_ressource_ir_response
                token['dgfip_ir']['last_known'] = franceconnect.dgfip_ressource_last_known_ir_response
            try:
                template = Template(self.text_template)
                text_template_context = {
                    'fc': franceconnect.fc_user_info.copy(),
                    'dgfip': token.get('dgfip_ir'),
                }
                if franceconnect.fc_user_info.get('birthdate'):
                    birthdate = franceconnect.fc_user_info['birthdate']
                    try:
                        text_template_context['fc']['birthdate'] = datetime.datetime.strptime(
                            birthdate, '%Y-%m-%d'
                        ).date()
                    except ValueError:
                        pass
                token['text'] = template.render(Context(text_template_context))
            except Exception:
                token['text'] = '<failed to render>'
            context['data'] = {'id': self.store(token), 'text': token['text']}
            context['data_json'] = json.dumps({'id': self.store(token), 'text': token['text']})
        except fc.FranceConnectError as e:
            self.logger.warning('callback: error %s', e)
            context['error'] = e
            context['error_json'] = json.dumps(repr(e))
            context['error_data_json'] = json.dumps(e.data)
        return TemplateResponse(request, 'franceconnect_data/callback.html', context=context)

    @endpoint(
        description=_('Demo page (to check your configuration)'),
    )
    def demo(self, request, **kwargs):
        if not request.user.is_superuser:
            raise PermissionDenied
        return TemplateResponse(
            request,
            'franceconnect_data/demo.html',
            context={'origin': request.build_absolute_uri('/'), 'resource': self},
        )

    @endpoint(
        description=_('Data source'),
    )
    def data_source(self, request, id=None, test=None, mode=None, **kwargs):
        if id:
            token = self.retrieve(id)
            if token:
                return {
                    'data': [
                        dict(token, id=id),
                    ]
                }
        url = request.build_absolute_uri(
            reverse(
                'generic-endpoint',
                kwargs={
                    'slug': self.slug,
                    'connector': self.get_connector_slug(),
                    'endpoint': 'init_request',
                },
            )
        )
        params = {}
        if mode == 'dgfip':
            params['mode'] = 'dgfip'
        if test:
            params['test'] = '1'
        if params:
            url += '?' + urlencode(params)
        return {
            'data': [
                {
                    'id': 'fc',
                    'text': 'fc',
                    'init_request_url': url,
                    'service_origin': get_url_origin(request.build_absolute_uri()),
                }
            ]
        }

    def store(self, data):
        # automatic cleaning of the cache
        Token.objects.filter(timestamp__lt=now() - datetime.timedelta(days=60)).delete()
        token = Token.objects.create(content=data)
        return token.guid.hex

    def retrieve(self, ref):
        try:
            ref = uuid.UUID(str(ref))
        except ValueError:
            return None
        token = Token.objects.filter(guid=ref).first()
        return token and token.content


class Token(models.Model):
    guid = models.UUIDField(verbose_name=_('UUID'), primary_key=True, default=uuid.uuid4)
    timestamp = models.DateTimeField(verbose_name=_('Timestamp'), auto_now_add=True)
    content = JSONField(verbose_name=_('Content'))

    class Meta:
        verbose_name = _('FranceConnect data token')
        verbose_name_plural = _('FranceConnect data tokens')
