# 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 json
import re
from urllib import parse as urlparse

from django.core.cache import cache
from django.db import models
from django.utils import dateparse
from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from lxml import etree

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

RESPONSE_CODES = {
    '01': _('Success'),
    '02': _('Remote service error'),
    '10': _('Authentication failed'),
    '11': _('Collectivity not defined'),
    '20': _('Invalid input format'),
    '21': _('Required field not provided'),
    '22': _('Unexpected value (referentials)'),
    '23': _('Demand already exists'),
}


def xml2dict(element):
    data = {}
    for attr in element.keys():
        data[attr] = element.get(attr)
    if element.text:
        return element.text
    for child in element:
        data[child.tag] = xml2dict(child)
    if data:
        return data


def check_value(data, field_name, values):
    value = data[field_name]
    if value not in values:
        raise ValueError('%s must be one of %s' % (field_name, values))
    return value


def strip_emoji(value):
    emoji_pattern = re.compile(
        "["
        "\U00002700-\U000027BF"  # Dingbats
        "\U0001F300-\U0001F5FF"  # symbols & pictographs
        "\U0001F600-\U0001F64F"  # emoticons
        "\U0001F680-\U0001F6FF"  # transport & map symbols
        "]+",
    )
    return emoji_pattern.sub(r'', value)


class GrenobleGRU(BaseResource):
    base_url = models.URLField(
        max_length=256, blank=False, verbose_name=_('Base URL'), help_text=_('Grenoble GRU API base URL')
    )
    username = models.CharField(max_length=128, verbose_name=_('Username'))
    password = models.CharField(max_length=128, verbose_name=_('Password'))

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = "Grenoble - Gestion des signalements"

    @classmethod
    def get_verbose_name(cls):
        return cls._meta.verbose_name

    def request(self, endpoint, payload=None):
        payload = payload or {}
        # do not alter original payload
        payload = payload.copy()
        payload['uti_identifiant'] = self.username
        payload['uti_motdepasse'] = self.password
        url = urlparse.urljoin(self.base_url, endpoint)
        return self.requests.post(url, data=payload)

    def check_status(self):
        response = self.request('ws_typologie_demande.php')
        response.raise_for_status()

    def build_gru_params(self, data):
        types_params = {}
        payload = {'id': data['application_id']}
        payload['dem_comp'] = types_params['dem_comp'] = data.get('dem_comp', 'Voirie')

        payload.update(
            {
                # applicant informations
                'dem_nom': data['applicant_lastname'],
                'dem_prenom': data['applicant_firstname'],
                'dem_tel': data['applicant_phone'],
                'dem_mail': data['applicant_email'],
                'dem_reponse': 1 if data.get('applicant_requires_reply') is True else 0,
                'dem_moyen_contact': check_value(
                    data, 'applicant_contact_mode', self.types('//modeContact', True, types_params)
                ),
                'dem_nature': check_value(
                    data, 'applicant_status', self.types('//natureContact', True, types_params)
                ),
                # intervention informations
                'int_type_adresse': check_value(
                    data, 'intervention_address_type', self.types('//typeAdresse', True, types_params)
                ),
                'int_numerovoie': data['intervention_street_number'],
                'int_libellevoie': data['intervention_street_name'],
                'int_insee': data['intervention_address_insee'],
                'int_secteur': check_value(
                    data, 'intervention_sector', self.types('//secteur', True, types_params)
                ),
                'int_type_numero': check_value(
                    data, 'intervention_number_type', self.types('//typeNumero', True, types_params)
                ),
                'int_date_demande': dateparse.parse_datetime(data['intervention_datetime']).strftime(
                    '%d%m%Y %H:%M'
                ),
                # comments
                'obs_demande_urgente': 1 if data.get('urgent_demand') in (True, 'True', 1, '1') else 0,
                'obs_type_dysfonctionnement': check_value(
                    data, 'dysfonction_type', self.types('//typeDysfonctionnement', True, types_params)
                ),
                'obs_motif': check_value(
                    data, 'intervention_reason', self.types('//motif', True, types_params)
                ),
                'obs_description_probleme': strip_emoji(data.get('comment_description', '')),
            }
        )
        if data['intervention_reason'] == '24':
            # code for reason 'Autre' in which case it should be specified
            payload['obs_motifautre'] = data.get('intervention_custom_reason', '')

        if 'intervention_free_address' in data:
            payload['int_adresse_manuelle'] = data['intervention_free_address']

        if 'applicant_free_address' in data:
            payload['dem_adresse_manuelle'] = data['applicant_free_address']

        if 'dem_pav' in data and data['dem_pav']:
            payload['dem_pav'] = data['dem_pav']

        return payload

    def types(self, path, as_list=False, params=None):
        params = params or {}
        cache_key = 'grenoble-gru-%s' % self.id
        if params:
            # compute new cache key based on params
            params_slug = '-'.join(sorted(slugify('%s-%s' % (k, v)) for k, v in params.items()))
            cache_key = '%s-%s' % (cache_key, params_slug)
        xml_content = cache.get(cache_key)
        if not xml_content:
            xml_content = self.request('ws_typologie_demande.php', params).content
        try:
            root = etree.fromstring(xml_content)
        except etree.XMLSyntaxError as e:
            raise APIError('Invalid XML returned: %s' % e)
        cache.set(cache_key, xml_content, 3600)
        if as_list:
            return [el.find('identifiant').text for el in root.xpath(path)]
        return {
            'data': [
                {'id': el.find('identifiant').text, 'text': el.find('libelle').text}
                for el in root.xpath(path)
            ]
        }

    @endpoint(name='contact-modes', perm='can_access', description=_('Lists contact modes'))
    def contact_modes(self, request, *args, **kwargs):
        return self.types('//modeContact', params=kwargs)

    @endpoint(name='contact-types', perm='can_access', description=_('Lists contact types'))
    def contact_types(self, request, *args, **kwargs):
        return self.types('//natureContact', params=kwargs)

    @endpoint(name='sectors', perm='can_access', description=_('Lists sectors'))
    def sectors(self, request, *args, **kwargs):
        return self.types('//secteur', params=kwargs)

    @endpoint(name='address-types', perm='can_access', description=_('Lists address types'))
    def address_types(self, request, *args, **kwargs):
        return self.types('//typeAdresse', params=kwargs)

    @endpoint(name='number-types', perm='can_access', description=_('Lists number types'))
    def number_types(self, request, *args, **kwargs):
        return self.types('//typeNumero', params=kwargs)

    @endpoint(name='dysfunction-types', perm='can_access', description=_('Lists dysfunction types'))
    def dysfunction_types(self, request, *args, **kwargs):
        return self.types('//typeDysfonctionnement', params=kwargs)

    @endpoint(
        name='intervention-descriptions', perm='can_access', description=_('Lists intervention descriptions')
    )
    def intervention_descriptions(self, request, *args, **kwargs):
        return self.types('//descIntervention', params=kwargs)

    @endpoint(name='intervention-reasons', perm='can_access', description=_('Lists intervention reasons'))
    def intervention_reasons(self, request, *args, **kwargs):
        return self.types('//motif', params=kwargs)

    @endpoint(perm='can_access', description=_('Lists PAVs'))
    def pavs(self, request, *args, **kwargs):
        response = self.request('ws_recuperation_pav.php')
        data = []
        for item in response.json():
            if not isinstance(item['adresse'], dict):
                continue
            item['id'] = str(item['id'])
            item['text'] = '{numero_voie}, {nom_voie}, {commune}'.format(**item['adresse'])
            data.append(item)
        return {'data': data}

    @endpoint(name='create-demand', perm='can_access', methods=['post'], description=_('Create a demand'))
    def create_demand(self, request, *args, **kwargs):
        try:
            payload = self.build_gru_params(json.loads(request.body))
        except (KeyError, ValueError) as e:
            raise APIError(e)
        response = self.request('ws_creation_demande.php', payload)
        if response.text != '01':
            raise APIError(RESPONSE_CODES.get(response.text, _('Unknown error code (%s)') % response.text))
        return {'data': 'Demand successfully created'}

    @endpoint(
        name='demand',
        perm='can_access',
        methods=['post'],
        description=_('Add attachment to a demand'),
        pattern=r'(?P<demand_id>[\w-]+)/add-attachment/$',
    )
    def add_attachment_to_demand(self, request, demand_id, **kwargs):
        data = json.loads(request.body)
        if 'file' not in data:
            raise WrongParameter(['file'], [])
        file_data = data['file']
        if not isinstance(file_data, dict):
            raise APIError('file should be a dict')
        if 'filename' not in file_data:
            raise WrongParameter(['file[filename]'], [])
        if 'content_type' not in file_data:
            raise WrongParameter(['file[content_type]'], [])
        if 'content' not in file_data:
            raise WrongParameter(['file[content]'], [])
        # file data should be ordered
        file_data = (
            ('filetype', file_data['content_type']),
            ('filename', file_data['filename']),
            ('filecontent', file_data['content']),
        )
        # file parameters should be urlencoded and sent as 'piece_jointe' param
        payload = {'dem_tiers_id': demand_id, 'piece_jointe': urlencode(file_data)}
        response = self.request('ws_update_demandePJ.php', payload)
        if response.text == '01':
            return True
        return False

    @endpoint(
        name='demand', perm='can_access', description=_('Get demand'), pattern=r'(?P<demand_id>[\w-]+)/$'
    )
    def get_demand(self, request, demand_id, **kwargs):
        payload = {'dem_tiers_id': demand_id}
        response = self.request('ws_get_demande.php', payload)
        try:
            demand = etree.fromstring(response.content)
        except etree.XMLSyntaxError as e:
            raise APIError('Invalid XML returned: %s' % e)
        return {'data': xml2dict(demand)}
