# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2018 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 xml.etree.ElementTree as ET
from urllib import parse as urlparse

import requests
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource, HTTPResource
from passerelle.utils import xml as xmlutils
from passerelle.utils.api import endpoint
from passerelle.utils.conversion import to_ascii
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.validation import is_number

from . import utils


class Resource(BaseResource, HTTPResource):
    category = _('Business Process Connectors')

    webservice_base_url = models.URLField(_('Webservice Base URL'))
    cod_rgp = models.CharField(_('Code RGP'), max_length=64, default='RGP_PUB')
    http_timeout = models.PositiveIntegerField(_('HTTP timeout'), default=60)

    class Meta:
        verbose_name = _('ATOS Genesys')

    @property
    def base_url(self):
        if 'WSUsagerPublik' in self.webservice_base_url:
            return self.webservice_base_url.split('WSUsagerPublik')[0]
        return self.webservice_base_url

    @property
    def select_codifications_url(self):
        return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectCodifications')

    def xml_request(self, url, *args, **kwargs):
        root, response = self._xml_request_base(url, *args, **kwargs)
        row = root.find('ROWSET/ROW')
        if row is None:
            raise APIError('no ROWSET/ROW node', data={'content': response.text[:1024]})
        return row

    def xml_request_multiple(self, url, *args, **kwargs):
        root, response = self._xml_request_base(url, *args, **kwargs)
        rowset = root.find('ROWSET')
        if rowset is None:
            raise APIError('no ROWSET node', data={'content': response.text[:1024]})
        rows = rowset.findall('ROW')
        return rows

    def _xml_request_base(self, url, *args, **kwargs):
        kwargs.setdefault('timeout', self.http_timeout)
        try:
            response = self.requests.get(url, *args, **kwargs)
            response.raise_for_status()
        except (requests.ConnectionError, requests.Timeout) as e:
            raise APIError('genesys is down', data={'exception': str(e)})
        except requests.RequestException as e:
            raise APIError('HTTP request failed', data={'exception': str(e)})
        try:
            root = ET.fromstring(response.content)
        except ET.ParseError as e:
            raise APIError('XML parsing failed', data={'exception': str(e)})
        if root.tag != 'return':
            raise APIError('root XML node is not return', data={'content': response.text[:1024]})
        return root, response

    def call_select_codifications(self):
        root = self.xml_request(self.select_codifications_url)
        categories = {}
        for category in root.findall('CATEGORIES/CATEGORIES_ROW'):
            code = category.find('CD_CAT_CODIF')
            label = category.find('LB_CAT_CODIF')
            if None in (code, label):
                self.logger.warning('invalid category: %s', ET.tostring(category))
                continue
            categories[xmlutils.text_content(code)] = {
                'label': xmlutils.text_content(label),
                'codifications': [],
            }
        for codification in root.findall('CODIFICATIONS/CODIFICATIONS_ROW'):
            code = codification.find('CD_CODIF')
            label = codification.find('LB_CODIF')
            in_val = codification.find('IN_VAL_CODIF')
            category_cod = codification.find('CD_CAT_CODIF')
            if None in (code, label, category_cod):
                self.logger.warning('invalid codification: %s', ET.tostring(codification))
                continue
            category_cod = xmlutils.text_content(category_cod)
            if category_cod not in categories:
                self.logger.warning('unknown category: %s', category_cod)
                continue
            categories[category_cod]['codifications'].append(
                {
                    'code': xmlutils.text_content(code),
                    'label': xmlutils.text_content(label),
                    'enabled': xmlutils.text_content(in_val).strip().lower() == 'o'
                    if in_val is not None
                    else True,
                }
            )
        return categories

    def get_codifications(self):
        cache = utils.RowLockedCache(
            function=self.call_select_codifications,
            row=self,
            key_prefix='atos-genesys-codifications',
            logger=self.logger,
        )
        return cache()

    @endpoint(name='codifications', description=_('List of codifications categories'))
    def codifications(self, request):
        codifications = self.get_codifications()

        items = []
        for code, category in codifications.items():
            items.append(
                {
                    'id': code,
                    'label': category['label'],
                }
            )
        items.sort(key=lambda c: c['label'])
        return {'data': items}

    @endpoint(
        name='codifications',
        pattern=r'^(?P<category>[\w-]+)/$',
        example_pattern='{category}/',
        description=_('List of codifications'),
        parameters={
            'category': {
                'description': _('Category of codification'),
                'example_value': 'MOT_APA',
            }
        },
    )
    def codifications_list(self, request, category):
        codifications = self.get_codifications().get(category, {}).get('codifications', [])

        items = [
            {'id': codification['code'], 'text': codification['label']} for codification in codifications
        ]
        return {'data': items}

    def check_status(self):
        return bool(self.call_select_codifications())

    @property
    def select_appairage_url(self):
        return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectAppairage')

    def call_select_appairage(self, login, password, email):
        row = self.xml_request(
            self.select_appairage_url,
            params={
                'login': login,
                'pwd': password,
                'email': email,
            },
        )
        row_d = xmlutils.to_json(row)
        id_per = row_d.get('ID_PER', '').strip()
        code = row_d.get('CD_RET', '').strip()
        label = row_d.get('LB_RET', '').strip()

        error = None
        if code not in ['1', '2', '3', '4', '5', '6']:
            error = ('invalid CD_RET: %s' % code,)
        if code in ['2', '3', '5'] and not id_per:
            error = 'missing ID_PER'
        if error:
            raise APIError(error, data={'response': repr(ET.tostring(row))})
        return code, label, id_per

    @endpoint(
        name='link',
        methods=['post'],
        description=_('Create link with an extranet account'),
        perm='can_access',
        parameters={
            'NameID': {
                'description': _('Publik NameID'),
                'example_value': 'xyz24d934',
            },
            'email': {
                'description': _('Publik known email'),
                'example_value': 'john.doe@example.com',
            },
            'login': {
                'description': _('ATOS Genesys extranet login'),
                'example_value': '1234',
            },
            'password': {
                'description': _('ATOS Genesys extranet password'),
                'example_value': 'password',
            },
        },
    )
    def link(self, request, NameID, email, login, password):
        code, label, id_per = self.call_select_appairage(login, password, email)
        if code in ['2', '3', '5']:
            link, created = Link.objects.get_or_create(resource=self, name_id=NameID, id_per=id_per)
            return {'link_id': link.pk, 'new': created, 'code': code, 'label': label}
        elif code == '6':
            raise APIError('unknown-login', data={'code': code, 'label': label})
        elif code in ['4', '1']:
            raise APIError('invalid-password', data={'code': code, 'label': label})

    @endpoint(
        name='unlink',
        methods=['post'],
        description=_('Delete link with an extranet account'),
        perm='can_access',
        parameters={
            'NameID': {
                'description': _('Publik NameID'),
                'example_value': 'xyz24d934',
            },
            'link_id': {
                'description': _('Identifier of the link'),
                'example_value': '1',
            },
        },
    )
    def unlink(self, request, NameID, link_id):
        try:
            link_id = int(link_id.strip())
        except ValueError:
            raise APIError('invalid link_id')

        qs = Link.objects.filter(resource=self, name_id=NameID, pk=link_id)
        count = qs.count()
        qs.delete()
        return {'deleted': count}

    @property
    def select_usager_url(self):
        return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectUsager')

    def call_select_usager(self, id_per):
        row = self.xml_request(
            self.select_usager_url,
            params={
                'idPer': id_per,
                'codRgp': self.cod_rgp,
            },
        )
        return self._select_usager_row_to_json(row)

    def _select_usager_row_to_json(self, row):
        d = xmlutils.to_json(row)
        # sort demandes and droits using COD_APPLI
        demandes = {}
        for demande in d.get('DEMANDES', []):
            cod_appli = demande.get('COD_APPLI', '')
            demandes.setdefault(cod_appli, []).append(demande)
        d['DEMANDES'] = demandes
        droits = {}
        for droit in d.get('DROITS', []):
            cod_appli = droit.get('COD_APPLI', '')
            droits.setdefault(cod_appli, []).append(droit)
        d['DROITS'] = droits
        # create CIVILITE
        for identification in d.get('IDENTIFICATION', []):
            sexe = identification.get('SEXE', '')
            identification['CIVILITE'] = {'M': 'Monsieur', 'F': 'Madame'}.get(sexe, '')
        return d

    @endpoint(
        name='dossiers',
        description=_('Get datas for all links'),
        perm='can_access',
        parameters={
            'NameID': {
                'description': _('Publik NameID'),
                'example_value': 'xyz24d934',
            },
        },
    )
    def dossiers(self, request, NameID, link_id=None):
        qs = Link.objects.filter(resource=self, name_id=NameID)
        if link_id:
            try:
                link_id = int(link_id)
            except ValueError:
                raise APIError('invalid-link-id')
            qs = qs.filter(id=link_id)
        data = []
        for link in qs:
            cache = utils.RowLockedCache(
                function=self.call_select_usager,
                row=link,
                key_prefix='atos-genesys-usager',
                logger=self.logger,
            )
            dossier = cache(link.id_per)
            # build text as "id_per - prenom - no
            text_parts = [str(link.id_per), '-']
            identifications = dossier.get('IDENTIFICATION') or [{}]
            identification = identifications[0]
            prenom = identification.get('PRENOM')
            nom = identification.get('NOM')
            if prenom:
                text_parts.append(prenom.title())
            if nom:
                text_parts.append(nom.upper())
            data.append(
                {
                    'id': str(link.id),
                    'text': ' '.join(text_parts),
                    'id_per': link.id_per,
                    'dossier': dossier,
                }
            )
        if link_id:
            return {'data': data[0] if data else None}
        return {'data': data}

    @property
    def select_usager_by_ref_url(self):
        return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectUsagerByRef')

    def call_select_usager_by_ref(self, ref_per):
        row = self.xml_request(
            self.select_usager_by_ref_url,
            params={
                'refPer': ref_per,
                'codRgp': self.cod_rgp,
            },
        )
        return self._select_usager_row_to_json(row)

    @property
    def cherche_beneficiaire_url(self):
        return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/chercheBeneficiaire')

    def call_cherche_beneficiaire(self, prenom, nom, dob):
        rows = self.xml_request_multiple(
            self.cherche_beneficiaire_url,
            params={
                'nmPer': nom,
                'prPer': prenom,
                'dtNaissance': dob.strftime('%d/%m/%Y'),
            },
        )
        beneficiaires = [xmlutils.to_json(row) for row in rows]
        return beneficiaires

    @endpoint(
        name='search',
        description=_('Search for beneficiaries'),
        perm='can_access',
        parameters={
            'first_name': {
                'description': _('Beneficiary first name'),
                'example_value': 'John',
            },
            'last_name': {
                'description': _('Beneficiary last name'),
                'example_value': 'Doe',
            },
            'date_of_birth': {
                'description': _('Beneficiary date of birth'),
                'type': 'date',
                'example_value': '1987-10-23',
            },
        },
    )
    def search(self, request, first_name, last_name, date_of_birth, NameID=None, commune_naissance=None):
        if date_of_birth.year < 1900:
            raise APIError('date_of_birth must be >= 1900')
        if commune_naissance:
            # convert commune_naissance to ASCII
            commune_naissance = to_ascii(commune_naissance).lower()
        beneficiaires = self.call_cherche_beneficiaire(prenom=first_name, nom=last_name, dob=date_of_birth)
        data = []
        dossiers = []
        # get dossiers of found beneficiaries
        for beneficiaire in beneficiaires:
            id_per = beneficiaire.get('ID_PER')
            if not id_per:
                self.logger.warning('no ID_PER')
                continue
            try:
                dob = beneficiaire['DATE_NAISSANCE']
            except KeyError:
                self.logger.warning('id_per %s: no DATE_NAISSANCE', id_per)
                continue
            try:
                dob = datetime.datetime.strptime(dob, '%d/%m/%Y').date()
            except (ValueError, TypeError):
                self.logger.warning('id_per %s: invalid DATE_NAISSANCE', id_per)
                continue
            if dob != date_of_birth:
                self.logger.debug('ignoring id_per %s different dob %s != %s', id_per, dob, date_of_birth)
                continue
            dossier = self.call_select_usager(id_per)
            try:
                identification = dossier['IDENTIFICATION'][0]
            except KeyError:
                self.logger.debug('id_per %s: dossier is empty', id_per)
                continue
            if not identification['ID_PER'] == id_per:
                self.logger.warning('id_per %s: ID_PER differs', id_per)
                continue
            if commune_naissance:
                cmu_nais = to_ascii(identification.get('CMU_NAIS', '')).lower()
                if cmu_nais and commune_naissance != cmu_nais:
                    self.logger.debug(
                        'id_per %s: CMU_NAIS(%s) does not match commune_naissance(%s)',
                        id_per,
                        cmu_nais,
                        commune_naissance,
                    )
                    continue
            dossiers.append(dossier)

        # there must be only one
        if len(dossiers) == 0:
            raise APIError('not-found')
        if len(dossiers) > 1:
            raise APIError('too-many')

        # get contact informations
        identification = dossiers[0]['IDENTIFICATION'][0]
        id_per = identification['ID_PER']
        nom = identification.get('NOM', '')
        prenom = identification.get('PRENOM', '')
        nom_naissance = identification.get('NOM_NAISSANCE', '')
        tel1 = ''.join(c for c in identification.get('TEL_MOBILE', '') if is_number(c))
        tel2 = ''.join(c for c in identification.get('TEL_FIXE', '') if is_number(c))
        email = identification.get('MAIL', '').strip()
        if tel1 and tel1[:2] in ('06', '07'):
            data.append(
                {
                    'id': 'tel1',
                    'text': 'par SMS vers ' + tel1[:2] + '*****' + tel1[-3:],
                    'phone': tel1,
                    'id_per': id_per,
                    'nom': nom,
                    'prenom': prenom,
                    'nom_naissance': nom_naissance,
                }
            )
        if tel2 and tel2[:2] in ('06', '07'):
            data.append(
                {
                    'id': 'tel2',
                    'text': 'par SMS vers ' + tel2[:2] + '*****' + tel2[-3:],
                    'phone': tel2,
                    'id_per': id_per,
                    'nom': nom,
                    'prenom': prenom,
                    'nom_naissance': nom_naissance,
                }
            )
        if email:
            data.append(
                {
                    'id': 'email1',
                    'text': 'par courriel vers ' + email[:2] + '***@***' + email[-3:],
                    'email': email,
                    'id_per': id_per,
                    'nom': nom,
                    'prenom': prenom,
                    'nom_naissance': nom_naissance,
                }
            )
        if len(data) == 0:
            self.logger.debug('id_per %s: no contact information, ignored', id_per)
            raise APIError('no-contacts')
        try:
            link = NameID and Link.objects.get(resource=self, name_id=NameID, id_per=id_per)
        except Link.DoesNotExist:
            link = None
        return {
            'data': data,
            'already_paired': link is not None,
            'link_id': link and link.id,
        }

    @endpoint(
        name='link-by-id-per',
        methods=['post'],
        description=_('Create link with an extranet account'),
        perm='can_access',
        parameters={
            'NameID': {
                'description': _('Publik NameID'),
                'example_value': 'xyz24d934',
            },
            'id_per': {
                'description': _('ATOS Genesys ID_PER'),
                'example_value': '767676',
            },
        },
    )
    def link_by_id_per(self, request, NameID, id_per):
        self.call_select_usager(id_per)
        link, created = Link.objects.get_or_create(resource=self, name_id=NameID, id_per=id_per)
        return {'link_id': link.pk, 'new': created}

    @endpoint(
        name='dossier-by-pair',
        description=_('Get dossier data with two integers'),
        perm='can_access',
        parameters={
            'p1': {
                'description': _('First integer'),
                'example_value': '1234',
            },
            'p2': {
                'description': _('Second integer'),
                'example_value': '6789',
            },
        },
    )
    def dossier_by_pair(self, request, p1, p2):
        try:
            p1 = int(p1)
            p2 = int(p2)
            if p1 < 0:
                raise ValueError
            if p2 < 0:
                raise ValueError
            if p1 <= p2:
                raise ValueError
        except ValueError:
            raise APIError('p1 and p2 MUST be positive integers')
        id_per = p1 // p2
        dossier = self.call_select_usager(id_per)
        return {
            'data': {
                'id_per': id_per,
                'dossier': dossier,
            }
        }


class Link(models.Model):
    resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
    name_id = models.CharField(verbose_name=_('NameID'), blank=False, max_length=256)
    id_per = models.CharField(verbose_name=_('ID Per'), blank=False, max_length=64)
    created = models.DateTimeField(verbose_name=_('Creation date'), auto_now_add=True)
    extra = JSONField(verbose_name=_('Anything'), null=True)

    class Meta:
        unique_together = (
            'resource',
            'name_id',
            'id_per',
        )
        ordering = ['created']
