# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2019  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 base64
import binascii
import urllib

import lxml.etree
from django.db import models
from django.utils import dateformat, dateparse
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from zeep import helpers
from zeep.exceptions import Fault

from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError

from . import schemas

DATE_FORMAT = 'l d F Y, G:i'


def process_response(demande_number):
    if demande_number.startswith('DIT') or demande_number.startswith('DPR'):
        return {'data': {'demande_number': demande_number}}
    raise APIError(demande_number)


class ATALConnector(BaseResource):
    base_soap_url = models.URLField(
        max_length=400, verbose_name=_('Base SOAP endpoint'), help_text=_('URL of the base SOAP endpoint')
    )
    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('ATAL connector')

    DEMANDE_NUMBER_PARAM = {'description': _('Demande number'), 'example_value': 'DIT18050001'}

    def _soap_client(self, wsdl):
        wsdl_url = urllib.parse.urljoin(self.base_soap_url, '%s?wsdl' % wsdl)
        return self.soap_client(wsdl_url=wsdl_url)

    def _soap_call(self, wsdl, method, **kwargs):
        client = self._soap_client(wsdl=wsdl)
        try:
            return getattr(client.service, method)(**kwargs)
        except Fault as e:
            raise APIError(force_str(e))

    def _basic_ref(self, wsdl, method):
        soap_res = self._soap_call(wsdl=wsdl, method=method)
        res = []
        for elem in soap_res:
            res.append({'id': elem.code, 'text': elem.libelle})
        return {'data': res}

    def _xml_ref(self, wsdl, method, root_elem):
        soap_res = self._soap_call(wsdl=wsdl, method=method)
        tree = lxml.etree.fromstring(soap_res.encode('utf-8')).getroottree()
        types = tree.xpath('//%s' % root_elem)[0]
        res = []
        for type_elem in types.getchildren():
            res.append({'id': type_elem.get('id'), 'text': type_elem.get('label')})
        return {'data': res}

    def check_status(self):
        """
        Raise an exception if something goes wrong.
        """
        self._soap_client(wsdl='DemandeService')

    @endpoint(methods=['get'], perm='can_access', name='get-thematique')
    def get_thematique(self, request):
        return self._xml_ref('DemandeService', 'getThematiqueATAL', 'thematiques')

    @endpoint(methods=['get'], perm='can_access', name='get-type-activite')
    def get_type_activite(self, request):
        return self._basic_ref('VilleAgileService', 'getTypeActivite')

    @endpoint(methods=['get'], perm='can_access', name='get-type-de-voie')
    def get_type_de_voie(self, request):
        return self._basic_ref('VilleAgileService', 'getTypeDeVoie')

    @endpoint(methods=['get'], perm='can_access', name='get-types-equipement')
    def get_types_equipement(self, request):
        return self._xml_ref('VilleAgileService', 'getTypesEquipement', 'types')

    @endpoint(
        perm='can_access',
        name='insert-action-comment',
        post={
            'description': _('Insert action comment'),
            'request_body': {'schema': {'application/json': schemas.INSERT_ACTION_COMMENT}},
        },
    )
    def insert_action_comment(self, request, post_data):
        demande_number = self._soap_call(
            wsdl='DemandeService',
            method='insertActionComment',
            numeroDemande=post_data['numero_demande'],
            commentaire=post_data['commentaire'],
        )
        return process_response(demande_number)

    @endpoint(
        perm='can_access',
        name='insert-demande-complet-by-type',
        post={
            'description': _('Insert demande complet by type'),
            'request_body': {'schema': {'application/json': schemas.INSERT_DEMANDE_COMPLET_BY_TYPE}},
        },
    )
    def insert_demande_complet_by_type(self, request, post_data):
        data = {}
        for recv, send in [
            ('type_demande', 'typeDemande'),
            ('code_service_demandeur', 'codeServiceDemandeur'),
            ('date_saisie', 'dateSaisie'),
            ('date_demande', 'dateDemande'),
            ('date_souhaite', 'dateSouhaitee'),
            ('date_butoir', 'dateButoir'),
            ('contact_civilite', 'contactCivilite'),
            ('contact_nom', 'contactNom'),
            ('contact_prenom', 'contactPrenom'),
            ('contact_tel', 'contactTelephone'),
            ('contact_mobile', 'contactMobile'),
            ('contact_email', 'contactCourriel'),
            ('contact_info_compl', 'contactInfoCompl'),
            ('demande_type_support', 'demandeTypeDeSupport'),
            ('contact_adresse', 'contactAdresse'),
            ('contact_adresse_compl', 'contactAdresseCompl'),
            ('contact_code_postal', 'contactCodePostal'),
            ('contact_ville', 'contactVille'),
            ('contact_organisme', 'contactOrganisme'),
            ('contact_titre', 'contactTitre'),
            ('code_equipement', 'codeEquipement'),
            ('code_mairie_equipement', 'codeMairieEquipement'),
            ('code_sig_equipement', 'codeSIGEquipement'),
            ('code_collectivite_equipement', 'codeCollectiviteEquipement'),
            ('code_quartier_equipement', 'codeQuartierEquipement'),
            ('code_type_equipement', 'codeTypeEquipement'),
            ('demande_lieu', 'demandeLieu'),
            ('coord_x', 'coordX'),
            ('coord_y', 'coordY'),
            ('demande_priorite', 'demandePriorite'),
            ('demande_objet', 'demandeObjet'),
            ('demande_description', 'demandeDescription'),
            ('demande_commentaire', 'demandeCommentaire'),
            ('demande_mots_cles', 'demandeMotsCles'),
            ('code_thematique', 'codeThematiqueATAL'),
            ('code_priorite', 'codePrioriteATAL'),
            ('demande_thematique', 'demandeThematique'),
            ('code_projet', 'codeProjetATAL'),
        ]:
            if recv in post_data:
                data[send] = post_data[recv]

        demande_number = self._soap_call(wsdl='DemandeService', method='insertDemandeCompletByType', **data)
        return process_response(demande_number)

    @endpoint(
        methods=['get'],
        perm='can_access',
        example_pattern='{demande_number}/',
        pattern=r'^(?P<demande_number>\w+)/$',
        name='retrieve-details-demande',
        parameters={'demande_number': DEMANDE_NUMBER_PARAM},
    )
    def retrieve_details_demande(self, request, demande_number):
        soap_res = self._soap_call(
            wsdl='DemandeService', method='retrieveDetailsDemande', demandeNumberParam=demande_number
        )
        return {'data': helpers.serialize_object(soap_res)}

    @endpoint(
        methods=['get'],
        perm='can_access',
        example_pattern='{demande_number}/',
        pattern=r'^(?P<demande_number>\w+)/$',
        name='retrieve-etat-travaux',
        parameters={'demande_number': DEMANDE_NUMBER_PARAM},
    )
    def retrieve_etat_travaux(self, request, demande_number):
        soap_res = self._soap_call(wsdl='DemandeService', method='retrieveEtatTravaux', numero=demande_number)
        return {'data': helpers.serialize_object(soap_res)}

    @endpoint(
        methods=['get'],
        perm='can_access',
        example_pattern='{demande_number}/',
        pattern=r'^(?P<demande_number>\w+)/$',
        parameters={
            'demande_number': DEMANDE_NUMBER_PARAM,
            'full': {
                'description': _('Full'),
                'example_value': 'true',
                'type': 'bool',
            },
        },
    )
    def infos(self, request, demande_number, full=False):
        demand_details = helpers.serialize_object(
            self._soap_call(
                wsdl='DemandeService', method='retrieveDetailsDemande', demandeNumberParam=demande_number
            )
        )
        if not demand_details:
            raise APIError('Could not get a status')

        status = (demand_details.get('etatDemande') or {}).get('description')
        if not status:
            raise APIError('Could not get a status')

        responses = (demand_details.get('reponses') or {}).get('Reponse') or []
        works_comments = []
        if responses:
            for response in responses:
                comment = {'text': response.get('commentaires'), 'date': None}
                if 'dateReponse' in response:
                    comment['date'] = dateformat.format(response['dateReponse'], DATE_FORMAT)
                works_comments.append(comment)

        works_comment = {'text': None, 'date': None}
        if works_comments:
            works_comment = works_comments[-1]

        data = {
            'status': status,
            'works_comment': works_comment,
            'demand_details': None,
            'works_comments': [],
        }
        if full:
            data['demand_details'] = demand_details
            data['works_comments'] = works_comments

        if status not in ('PRISE EN COMPTE', 'ARCHIVEE'):
            return {'data': data}

        works_status = helpers.serialize_object(
            self._soap_call(wsdl='DemandeService', method='retrieveEtatTravaux', numero=demande_number)
        )
        status = works_status.get('libelle')
        if not status:
            raise APIError('Could not get a status')

        data['status'] = status
        data['demand_comment'] = works_status.get('commentaires', '')
        data['works_status'] = None

        if full:
            data['works_status'] = works_status

        return {'data': data}

    @endpoint(
        perm='can_access',
        post={
            'description': _('Upload a file'),
            'request_body': {'schema': {'application/json': schemas.UPLOAD}},
        },
    )
    def upload(self, request, post_data):
        try:
            content = base64.b64decode(post_data['file']['content'])
        except (TypeError, binascii.Error):
            raise APIError('Invalid base64 string')

        filename = post_data.get('nom_fichier', '')
        if not filename:
            filename = post_data['file'].get('filename', '')

        data = {
            'donneesFichier': content,
            'numeroDemande': post_data['numero_demande'],
            'nomFichier': filename,
        }
        self._soap_call(wsdl='ChargementPiecesJointesService', method='upload', **data)
        return {}

    @endpoint(
        methods=['get'],
        perm='can_access',
        example_pattern='{demande_number}/',
        pattern=r'^(?P<demande_number>\w+)/$',
        name='new-comments',
        parameters={
            'demande_number': DEMANDE_NUMBER_PARAM,
        },
    )
    def new_comments(self, request, demande_number, last_datetime=None):
        def issup(datetime1, datetime2):
            if datetime1.tzinfo is None or datetime2.tzinfo is None:
                datetime1 = datetime1.replace(tzinfo=None)
                datetime2 = datetime2.replace(tzinfo=None)
            return datetime1 > datetime2

        if last_datetime is not None:
            last_datetime = dateparse.parse_datetime(last_datetime)
            if last_datetime is None:
                raise APIError('Wrong datetime format', http_status=400)

        demand_details = helpers.serialize_object(
            self._soap_call(
                wsdl='DemandeService', method='retrieveDetailsDemande', demandeNumberParam=demande_number
            )
        )
        if not demand_details:
            raise APIError('Could not get comments')

        new_comments, all_comments, last_date = [], [], None
        responses = (demand_details.get('reponses') or {}).get('Reponse') or []
        for response in responses:
            if not response.get('commentaires'):
                continue
            comment = {'text': response.get('commentaires'), 'date': None, 'date_raw': None}
            dateobj = None
            if 'dateReponse' in response:
                dateobj = response['dateReponse']
                comment['date'] = dateformat.format(dateobj, DATE_FORMAT)
                comment['date_raw'] = dateformat.format(dateobj, 'c')
                last_date = comment['date_raw']

            all_comments.append(comment)
            if dateobj and issup(dateobj, last_datetime) or last_datetime is None:
                if comment not in new_comments:
                    new_comments.append(comment)
        return {'data': {'new_comments': new_comments, 'all_comments': all_comments, 'last_date': last_date}}
