# 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/>.

'''Gateway to API-Particulier web-service from SGMAP:
   https://particulier.api.gouv.fr/
'''

import re
from collections import OrderedDict
from urllib import parse

import requests

try:
    from json.decoder import JSONDecodeError
except ImportError:
    JSONDecodeError = ValueError

from django.contrib.postgres.fields import ArrayField
from django.core.cache import cache
from django.db import models
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint
from passerelle.utils.conversion import exception_to_text
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.validation import is_number

from .known_errors import KNOWN_ERRORS

# regular expression for numero_fiscal and reference_avis
FISCAL_RE = re.compile(r'^[0-9a-zA-Z]{13}$')


class APIParticulier(BaseResource):
    PLATFORMS = [
        {'name': 'prod', 'label': _('Production'), 'url': 'https://particulier.api.gouv.fr/api/'},
        {
            'name': 'test',
            'label': _('Test'),
            'url': 'https://particulier-test.api.gouv.fr/api/',
        },
    ]
    PLATFORMS = OrderedDict([(platform['name'], platform) for platform in PLATFORMS])

    platform = models.CharField(
        verbose_name=_('Platform'),
        max_length=8,
        choices=[(key, platform['label']) for key, platform in PLATFORMS.items()],
    )

    api_key = models.CharField(max_length=1024, default='', blank=True, verbose_name=_('API key'))

    log_requests_errors = False

    accessible_scopes = ArrayField(
        models.CharField(max_length=32),
        null=True,
        blank=True,
        editable=False,
    )

    hide_description_fields = ['accessible_scopes']

    @property
    def url(self):
        return self.PLATFORMS[self.platform]['url']

    def get(self, path, **kwargs):
        user = kwargs.pop('user', None)
        url = parse.urljoin(self.url, path)
        headers = {'X-API-KEY': self.api_key}
        if user:
            headers['X-User'] = user
        try:
            response = self.requests.get(url, headers=headers, timeout=5, **kwargs)
        except requests.RequestException as e:
            raise APIError(
                'API-particulier platform "%s" connection error: %s' % (self.platform, exception_to_text(e)),
                log_error=True,
                data={
                    'code': 'connection-error',
                    'platform': self.platform,
                    'error': str(e),
                },
            )
        try:
            data = response.json()
        except JSONDecodeError as e:
            content = repr(response.content[:1000])
            raise APIError(
                'API-particulier platform "%s" returned non-JSON content with status %s: %s'
                % (self.platform, response.status_code, content),
                log_error=True,
                data={
                    'code': 'non-json',
                    'status_code': response.status_code,
                    'exception': str(e),
                    'platform': self.platform,
                    'content': content,
                },
            )
        if response.status_code != 200:
            # avoid logging http errors about non-transport failure
            message = data.get('message', '')
            if message in KNOWN_ERRORS.get(response.status_code, []):
                error_data = {
                    'code': data.get('error', 'api-error').replace('_', '-'),
                    'status_code': response.status_code,
                    'platform': self.platform,
                    'content': data,
                }
                if response.status_code // 100 == 4:
                    # for 40x errors, allow caching of the response, as it should not change with future requests
                    return {
                        'err': 1,
                        'err_desc': message,
                        'data': error_data,
                    }
                raise APIError(message, data=error_data)
            raise APIError(
                'API-particulier platform "%s" returned a non 200 status %s: %s'
                % (self.platform, response.status_code, data),
                log_error=True,
                data={
                    'code': 'non-200',
                    'status_code': response.status_code,
                    'platform': self.platform,
                    'content': data,
                },
            )
        return {
            'err': 0,
            'data': data,
        }

    def get_scopes(self):
        response = self.get('introspect')
        result = sorted(response['data'].get('scopes'))
        return result

    def clean(self):
        try:
            scopes = self.get_scopes()
        except APIError:
            scopes = []
        self.accessible_scopes = [x[:32] for x in scopes]

    def daily(self):
        super().daily()
        try:
            scopes = self.get_scopes()
        except APIError:
            scopes = []
        self.accessible_scopes = [x[:32] for x in scopes]
        self.save()

    @endpoint(
        perm='can_access',
        description=_('Get scopes available'),
        display_order=1,
    )
    def scopes(self, request):
        scopes = self.get_scopes()
        self.accessible_scopes = [x[:32] for x in scopes]
        self.save()
        return {
            'err': 0,
            'data': scopes,
        }

    @endpoint(
        perm='can_access',
        show=False,
        description=_('Get citizen\'s fiscal informations'),
        parameters={
            'numero_fiscal': {
                'description': _('fiscal identifier'),
                'example_value': '1562456789521',
            },
            'reference_avis': {
                'description': _('tax notice number'),
                'example_value': '1512456789521',
            },
            'user': {
                'description': _('requesting user'),
                'example_value': 'John Doe (agent)',
            },
        },
    )
    def impots_svair(self, request, numero_fiscal, reference_avis, user=None):
        # deprecated endpoint
        return self.v2_avis_imposition(request, numero_fiscal, reference_avis, user=user)

    @endpoint(
        name='avis-imposition',
        perm='can_access',
        description=_('Get citizen\'s fiscal informations'),
        parameters={
            'numero_fiscal': {
                'description': _('fiscal identifier'),
                'example_value': '1562456789521',
            },
            'reference_avis': {
                'description': _('tax notice number'),
                'example_value': '1512456789521',
            },
            'user': {
                'description': _('requesting user'),
                'example_value': 'John Doe (agent)',
            },
        },
        json_schema_response={
            'type': 'object',
            'required': ['err'],
            'properties': {
                'err': {'enum': [0, 1]},
                'declarant1': {
                    'type': 'object',
                    'properties': {
                        'nom': {'type': 'string'},
                        'nomNaissance': {'type': 'string'},
                        'prenoms': {'type': 'string'},
                        'dateNaissance': {'type': 'string'},
                    },
                },
                'declarant2': {
                    'type': 'object',
                    'properties': {
                        'nom': {'type': 'string'},
                        'nomNaissance': {'type': 'string'},
                        'prenoms': {'type': 'string'},
                        'dateNaissance': {'type': 'string'},
                    },
                },
                'foyerFiscal': {
                    'type': 'object',
                    'properties': {
                        'annee': {'type': 'integer'},
                        'adresse': {'type': 'string'},
                    },
                },
                'dateRecouvrement': {'type': 'string', 'pattern': r'^\d{1,2}/\d{1,2}/\d{4}$'},
                'dateEtablissement': {'type': 'string', 'pattern': r'^\d{1,2}/\d{1,2}/\d{4}$'},
                'nombreParts': {'type': 'integer'},
                'situationFamille': {'type': 'string'},
                'nombrePersonnesCharge': {'type': 'integer'},
                'revenuBrutGlobal': {'type': 'integer'},
                'revenuImposable': {'type': 'integer'},
                'impotRevenuNetAvantCorrections': {'type': 'integer'},
                'montantImpot': {'type': 'integer'},
                'revenuFiscalReference': {'type': 'integer'},
                'anneeImpots': {'type': 'string', 'pattern': r'^[0-9]{4}$'},
                'anneeRevenus': {'type': 'string', 'pattern': r'^[0-9]{4}$'},
                'erreurCorrectif': {'type': 'string'},
                'situationPartielle': {'type': 'string'},
            },
        },
    )
    def v2_avis_imposition(self, request, numero_fiscal, reference_avis, user=None):
        # clean the references
        numero_fiscal = re.sub(r'\s', '', numero_fiscal)[:13]
        reference_avis = re.sub(r'\s', '', reference_avis)[:13]
        # check they are both 13 digits numbers
        if not FISCAL_RE.match(numero_fiscal):
            raise APIError(
                'bad numero_fiscal, it MUST be composed of 13 alphanumeric characters', status_code=400
            )
        if not FISCAL_RE.match(reference_avis):
            raise APIError(
                'bad reference_avis, it MUST be composed of 13 alphanumeric characters', status_code=400
            )

        cache_key = 'svai-%s-%s-%s' % (self.pk, numero_fiscal, reference_avis)
        data = cache.get(cache_key)
        if data is None:
            data = self.get(
                'v2/avis-imposition',
                params={
                    'numeroFiscal': numero_fiscal,
                    'referenceAvis': reference_avis,
                },
                user=user,
            )
            # put in cache for two hours
            cache.set(cache_key, data, 3600 * 2)
        else:
            self.logger.info('found response in cache, "%s"', data)
        return data

    @endpoint(
        perm='can_access',
        show=False,
        description=_('Get family allowances recipient informations'),
        parameters={
            'code_postal': {
                'description': _('postal code'),
                'example_value': '99148',
            },
            'numero_allocataire': {
                'description': _('recipient identifier'),
                'example_value': '0000354',
            },
            'user': {
                'description': _('requesting user'),
                'example_value': 'John Doe (agent)',
            },
        },
    )
    def caf_famille(self, request, code_postal, numero_allocataire, user=None):
        # deprecated endpoint
        return self.v2_situation_familiale(request, code_postal, numero_allocataire, user=user)

    @endpoint(
        name='situation-familiale',
        perm='can_access',
        description=_('Get family allowances recipient informations'),
        parameters={
            'code_postal': {
                'description': _('postal code'),
                'example_value': '99148',
            },
            'numero_allocataire': {
                'description': _('recipient identifier'),
                'example_value': '0000354',
            },
            'user': {
                'description': _('requesting user'),
                'example_value': 'John Doe (agent)',
            },
        },
    )
    def v2_situation_familiale(self, request, code_postal, numero_allocataire, user=None):
        numero_allocataire = numero_allocataire.strip()[:7]
        code_postal = code_postal.strip()

        if not code_postal or not numero_allocataire:
            raise APIError('missing code_postal or numero_allocataire')

        if len(numero_allocataire) != 7 or not is_number(numero_allocataire):
            raise APIError('numero_allocataire should be a 7 digits number')
        data = self.get(
            'v2/composition-familiale',
            params={
                'codePostal': code_postal,
                'numeroAllocataire': numero_allocataire,
            },
            user=user,
        )
        data['data']['numero_allocataire'] = numero_allocataire
        data['data']['code_postal'] = code_postal
        return data

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('API Particulier')
