# passerelle.contrib.agoraplus
# Copyright (C) 2015  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/>.

from requests.auth import HTTPBasicAuth
import json
import logging
from datetime import datetime
from contextlib import contextmanager

from django.db import models
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache as django_cache
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder
from jsonfield import JSONField

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

from .normalize import normalize_reference, normalize_family, \
        normalize_school, normalize_person, denormalize_date, PROVISIONING_DOMAIN, \
        normalize_invoice, normalize_planning, normalize_date, normalize_nursery_enroll_results, \
        normalize_school_enrollment
from .finalize import finalize_family
from .agoraize import agoraize_family, agoraize_child, agoraize_contact, \
    agoraize_address
from .provisioning import provision_attributes, provision_roles


logger = logging.getLogger('passerelle.contrib.agoraplus')


def get_empty_family():
    return {
        'adults': [],
        'children': [],
        'contacts': [],
        'informations': {},
    }


class AgoraAPIError(APIError):
    pass


def parse_json(response):
    try:
        return response.json()
    except Exception, e:
        raise AgoraAPIError('no JSON content returned %s: %r' % (
            e, response.content[:1000]))


@contextmanager
def wrap_agora(response, msg, expected_type=None):
    try:
        if expected_type is not None and not isinstance(response, expected_type):
            raise AgoraAPIError(
                'failure %s: we expected %s and got: %r' % (expected_type.__name__, response))

        yield response
    except AgoraAPIError:
        raise
    except Exception, e:
        raise AgoraAPIError('failure %s %s: %r' % (
            msg, e, str(response)[:1000]))


def wrap_agora_dict(response, msg):
    return wrap_agora(response, msg, expected_type=dict)


def wrap_agora_array(response, msg):
    return wrap_agora(response, msg, expected_type=list)


class AgoraPlus(BaseResource):
    url = models.CharField(max_length=128, blank=False,
            verbose_name=_('Webservices Base URL'))
    frontoffice_url = models.CharField(max_length=128, blank=True,
            verbose_name=_('Frontoffice URL (SSO)'))
    login = models.CharField(max_length=128, blank=False,
            verbose_name=_('Auth login'))
    oauth_consumer_key = models.CharField(max_length=128, blank=False,
            verbose_name=_('Auth oauth_consumer_key'))
    verify_cert = models.BooleanField(default=True,
            verbose_name=_('Check HTTPS Certificate validity'))
    username = models.CharField(max_length=128, blank=True,
            verbose_name=_('HTTP Basic Auth username'))
    password = models.CharField(max_length=128, blank=True,
            verbose_name=_('HTTP Basic Auth password'))
    keystore = models.FileField(upload_to='agoraplus', null=True, blank=True,
            verbose_name=_('Keystore'),
            help_text=_('Certificate and private key in PEM format'))

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('Agora+')

    @classmethod
    def get_icon_class(cls):
        return 'family'

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

    #
    # plumber stuff : request with "oauth token"
    #

    def request_auth(self, endpoint, **kwargs):
        method = 'GET'
        if 'data' in kwargs or 'json' in kwargs:
            method = 'POST'
            if 'json' in kwargs: # requests<2.4.2 don't known json argument
                kwargs['data'] = json.dumps(kwargs.pop('json'), cls=DjangoJSONEncoder)
        if 'method' in kwargs:
            method = kwargs.pop('method')
        if self.keystore:
            kwargs['cert'] = (self.keystore.path, self.keystore.path)
        if not self.verify_cert:
            kwargs['verify'] = False
        if self.username or self.password:
            kwargs['auth'] = HTTPBasicAuth(self.username, self.password)
        if 'data' in kwargs:
            logger.debug('%s %s with payload=%s', method, endpoint, kwargs['data'])
        else:
            logger.debug('%s %s', method, endpoint)
        response = self.requests.request(method, self.url + endpoint, **kwargs)
        logger.debug('response.content: %r', response.content)
        return parse_json(response)

    def get_token_raw(self, login=None):
        if login is None:
            login = self.login
        response = self.request_auth('Auth/',
                json={
                    'login': login,
                    'oauth_consumer_key': self.oauth_consumer_key,
                })
        if 'token' not in response:
            raise AgoraAPIError('no token in response: %r' % response)
        return response['token']

    def get_token(self, cache=True):
        cache_key = 'agoraplus-%d-token' % self.id
        if cache:
            token = django_cache.get(cache_key)
            if token:
                return token
        token = self.get_token_raw()
        django_cache.set(cache_key, token, 60*15)
        return token

    def request(self, endpoint, **kwargs):
        token = self.get_token()
        kwargs['cookies'] = {'portail_session_id': token}
        response = self.request_auth(endpoint, **kwargs)
        if response == {u'erreur': u'Authentication failed.', u'success': u'0'}:
            # token may have expired, get a new one
            token = self.get_token(cache=False)
            kwargs['cookies'] = {'portail_session_id': token}
            response = self.request_auth(endpoint, **kwargs)
        return response

    #
    # agora+ webservices callers
    #

    def get_reference(self, name, reference_id=None):
        endpoint = name + '/'
        if reference_id:
            endpoint += '%s' % reference_id
        with wrap_agora_array(self.request(endpoint), 'to get reference') as results:
            if reference_id:
                return normalize_reference(results[0])
            return sorted([normalize_reference(ref) for ref in results],
                    key=lambda obj: obj.get('id','9999'))

    def get_user(self, login=None):
        endpoint = 'comptes_portail/'
        if login:
            endpoint += '?' + urlencode({'p_login': login})
        return self.request(endpoint)

    def get_school(self, school_id=None, family=None, birthdate=None, year_id=None):
        endpoint = 'etablissements/'
        if school_id:
            endpoint += '%s' % school_id
            with wrap_agora_array(self.request(endpoint), 'to get school') as response:
                return normalize_school(response[0])
        if not birthdate:
            with wrap_agora_array(self.request(endpoint), 'to get schools') as schools:
                return [normalize_school(school) for school in schools]

        date_qs = urlencode({'p_date_naissance': denormalize_date(birthdate)})
        if family:
            # ?p_id_rue={p_id_rue}&p_btq={p_id_btq}&p_numero={p_num}&
            # p_id_ville={p_id_ville}&p_date_naissance={p_date_naissance}
            endpoint += '?p_id_rue={street[id]}&p_btq={btq[id]}' \
                    '&p_numero={number}&p_id_ville={city[id]}'.format(**family['address'])
            endpoint += '&' + date_qs
            if year_id:
                endpoint += '&' + urlencode({'p_id_annee_ref': year_id})
        else:
            # all school, regardless of the address
            endpoint += '?' + date_qs
        with wrap_agora_array(self.request(endpoint), 'to get schools') as schools:
            return [normalize_school(school) for school in schools]

    def get_commune(self, commune_id=None):
        with wrap_agora_array(self.get_reference('communes'), 'to get commune') as communes:
            if not commune_id:
                return communes
            for commune in communes:
                if commune['id'] == commune_id:
                    return commune

    def get_type_of_street(self, type_of_street_id=None):
        with wrap_agora_array(self.get_reference('types_de_voies'), 'to get type of streets') \
                as type_of_streets:
            if not type_of_street_id:
                return type_of_streets
            for type_of_street in type_of_streets:
                if type_of_street['id'] == type_of_street_id:
                    return type_of_street

    def get_street(self, commune_id=None, type_of_street_id=None, street_id=None):
        endpoint = 'voies'
        if not street_id:
            endpoint += '/'
            if commune_id:
                endpoint += '?%s' % urlencode((
                    ('p_id_ville', commune_id),
                    ('p_id_type_voie', type_of_street_id)
                ))
            with wrap_agora_array(self.request(endpoint), 'to get streets') as streets:
                return [normalize_reference(street) for street in streets]
        return self.get_reference(endpoint, reference_id=street_id)

    def get_educational_stage(self, edustage_id=None, birthdate=None, year_id=None):
        endpoint = 'niveaux_scolaires'
        if birthdate:
            endpoint += '/?' + urlencode({'p_date_naissance': denormalize_date(birthdate)})
            if year_id:
                endpoint += '&' + urlencode({'p_id_annee_ref': year_id})
            with wrap_agora_array(self.request(endpoint), 'to get educational stages') as edustages:
                return [normalize_reference(edustage) for edustage in edustages]
        with wrap_agora_array(self.request(endpoint), 'to get educational stages') as edustages:
            edustages = self.get_reference(endpoint)
            if not edustage_id:
                return edustages
            for edustage in edustages:
                if edustage['id'] == edustage_id:
                    return edustage

    def auth(self, login, password):
        if '@' not in login:
            login = u'%s@%s' % (login, PROVISIONING_DOMAIN)
        endpoint = 'comptes_portail/?%s' % urlencode((
            ('p_login', login),
            ('p_password', password)
        ))
        with wrap_agora_dict(self.request(endpoint), 'to auth') as response:
            if int(response.get('success') or '0') != 1:
                return False
            return login

    def get_agoraplus_family(self, login, raise_error=False):
        endpoint = 'familles/?%s' % urlencode({'p_mail': login})
        with wrap_agora_dict(self.request(endpoint), 'to get family') as family:
            try:
                success = int(family.get('success', 0))
            except ValueError:
                if raise_error:
                    raise AgoraAPIError('no family found: invalid success value %r' % family)
                success = 0
            if 'erreur' in family or success < 0:
                if raise_error:
                    raise AgoraAPIError(family.get('erreur') or 'no error given, %r' % family)
                return None
            return normalize_family(family)

    def exist_email(self, email):
        '''
        return True if email is used by a component in Agora+ database
        '''
        endpoint = 'exist_mail/?%s' % urlencode({'p_mail': email})

        with wrap_agora(self.request(endpoint), 'to test email') as out:
            if isinstance(out, list) and len(out) > 0:
                return True
        endpoint = 'comptes_portail/?%s' % urlencode({'p_login': email})
        with wrap_agora(self.request(endpoint), 'to test email') as out:
            if isinstance(out, dict) and 'id' in out:
                return True
            return False

    def exist_child(self, first_name, last_name, birthdate):
        if '/' in birthdate:
            birthdate = normalize_date(birthdate)
        birthdate = denormalize_date(birthdate)
        endpoint = 'exist_enfants/?%s' % urlencode((
            ('p_nom', last_name),
            ('p_prenom', first_name),
            ('p_date_naissance', birthdate)
        ))
        with wrap_agora_dict(self.request(endpoint), 'to test existing child') as out:
            return int(out['code']) != 0

    def exist_representant(self, first_name, last_name, birthdate=None):
        if not birthdate:
            birthdate = "''"
        else:
            if '/' in birthdate:
                birthdate = normalize_date(birthdate)
            birthdate = denormalize_date(birthdate)
        endpoint = 'representants/?%s' % urlencode((
            ('p_nom', last_name),
            ('p_prenom', first_name),
            ('p_date_naiss', birthdate)
        ))
        with wrap_agora(self.request(endpoint), 'to test existing representant') as out:
            return isinstance(out, list) and len(out) > 0

    def exist_person(self, first_name, last_name, birthdate=None):
        if birthdate and self.exist_child(first_name, last_name, birthdate):
            return True
        return self.exist_representant(first_name, last_name, birthdate)

    #
    # Sas system : adults, children and contacts are created in AgoraPlusSas
    #

    def sas_add(self, name_id, kind, data):
        sas_object = AgoraPlusSas(resource=self, name_id=name_id,
                kind=kind, agoraplus_id='', value=data)
        sas_object.save()
        res = sas_object.value or {}
        res['id'] = 'sas_%s' % sas_object.pk
        res['sas'] = True
        return res

    def sas_upsert(self, name_id, kind, data):
        if data.get('sas_id'):
            sas_objects = AgoraPlusSas.objects.filter(resource=self, pk=data['sas_id'][4:],
                                                      name_id=name_id, kind=kind, agoraplus_id='')
            if sas_objects:
                sas_object = sas_objects[0]
                sas_object.value = data
                sas_object.save()
                res = sas_object.value or {}
                res['id'] = 'sas_%s' % sas_object.pk
                res['sas'] = True
                return res
        return self.sas_add(name_id, kind, data)

    def sas_delete(self, name_id, kind, data):
        # remove "data" object if it already exists in sas
        if data.get('sas_id'):
            sas_objects = AgoraPlusSas.objects.filter(resource=self,
                                                      pk=data['sas_id'][4:], name_id=name_id,
                                                      kind=kind, agoraplus_id='')
            count = sas_objects.count()
            sas_objects.delete()
            return count
        return 0

    def sas_purge(self, name_id):
        AgoraPlusSas.objects.filter(resource=self, name_id=name_id).delete()
        return True

    def get_sas_id(self, name_id, kind, agoraplus_id):
        '''
        Returns id of the object (kind, agoraplus_id) if it was pushed from the sas.
        '''
        sas = AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                kind=kind, agoraplus_id=agoraplus_id).values('pk')
        if sas:
            return 'sas_%s' % sas[0]['pk']
        return None

    def get_sas_items(self, name_id, kind):
        '''
        Returns sas items of "kind" that are only in sas (not pushed in Agora+).
        '''
        items = AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                kind=kind, agoraplus_id='')
        for item in items:
            if not item.value:
                item.value = {}
            item.value['id'] = 'sas_%s' % item.pk
            item.value['sas'] = True
        return [item.value for item in items]

    def update_from_sas(self, name_id, objects, kind):
        for obj in objects:
            # If agora+ object was in the sas, use its sas id.
            sas_id = self.get_sas_id(name_id, kind, obj['id'])
            if sas_id:
                obj['original_id'] = obj['id']
                obj['id'] = sas_id
        # Add objects from the sas.
        objects.extend(self.get_sas_items(name_id, kind))
        # Sort objects by 'id' ; or by 'index' if present
        objects.sort(key=lambda obj: obj['id'])
        objects.sort(key=lambda obj: obj.get('index', 9999))

    def get_family(self, login=None, name_id=None):
        '''
        Get family from Agora+ (with login), and mix it with sas informations (with name_id)
        '''
        if not login and not name_id:
            return None
        if login:
            family = self.get_agoraplus_family(login) or get_empty_family()
            # if family was in the sas, use its sas id
            if family.get('id'):
                sas_id = self.get_sas_id(name_id, 'FAMILY', family['id'])
                if sas_id:
                    family['original_id'] = family['id']
                    family['id'] = sas_id
        else:
            family = get_empty_family()
            families = self.get_sas_items(name_id, 'FAMILY')
            if families:
                family.update(families[0])
        if name_id:
            self.update_from_sas(name_id, family['adults'], 'ADULT')
            self.update_from_sas(name_id, family['children'], 'CHILD')
            self.update_from_sas(name_id, family['contacts'], 'CONTACT')
        if not family['adults']:
            return None # Family does not exist in Agora+ or in the sas.
        return finalize_family(family)

    def sas_check_duplicates(self, name_id):
        duplicates = []
        for sas_adult in AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                                                 kind='ADULT', agoraplus_id=''):
            adult = sas_adult.value
            email = adult.get('email')
            if email and self.exist_email(email):
                duplicates.append(email)
            if self.exist_representant(adult['first_name'],
                                       adult['last_name'],
                                       adult['birthdate']):
                duplicates.append('%(first_name)s %(last_name)s - %(birthdate)s' % adult)
        for sas_child in AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                                                     kind='CHILD', agoraplus_id=''):
            child = sas_child.value
            if self.exist_child(child['first_name'],
                                child['last_name'],
                                child['birthdate']):
                duplicates.append('%(first_name)s %(last_name)s - %(birthdate)s' % child)
        return duplicates

    def push_family(self, name_id, validation=False):

        # get family from sas
        sas_families = AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                kind='FAMILY', agoraplus_id='').order_by('pk')
        if not sas_families:
            raise ObjectDoesNotExist('no family in sas')
        if sas_families.count() > 1:
            raise AgoraAPIError('more than one family in sas')
        sas_adults = AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                kind='ADULT', agoraplus_id='')
        if not sas_adults:
            raise ObjectDoesNotExist('no adult in family')
        if sas_adults.count() > 2:
            raise AgoraAPIError('more than 2 adults in sas')
        family = sas_families[0].value
        family['adults'] = [sas_adult.value for sas_adult in sas_adults]

        # store family in agora+
        agora_family = agoraize_family(family)
        if validation:
            agora_family['sas'] = '1'
        else:
            agora_family['sas'] = '0'
        with wrap_agora_dict(self.request('creation_familles/', json=agora_family), 'to create family') \
                as push:
            agora_family_id = push.get('id_famille')
            if push.get('id') != '1' or not agora_family_id:
                raise AgoraAPIError(push.get('message') or 'no message given, %r' % push)

            # get created adults id from agora+, store back
            with wrap_agora_dict(self.request('familles/%s' % agora_family_id), 'to get family') \
                    as agora_family:
                links = {}
                for n, agora_adult in enumerate(agora_family['Representants']):
                    # restore adult with new email : adult_id@PROVISIONING_DOMAIN
                    agora_adult['email'] = agora_adult['id'] + '@' + PROVISIONING_DOMAIN
                    endpoint = 'familles/%s/representants/%s' % (agora_family_id, agora_adult['id'])
                    self.request(endpoint, method='PUT', json=agora_adult)
                    # create compte_portail
                    with wrap_agora_dict(self.request('comptes_portail/',
                                                 json={'idRepresentant': agora_adult['id'], 'mail':
                                                       agora_adult['email']}), 'to get link') \
                            as link:
                                link['id'] = agora_adult['id']
                    links[n] = link
                    # adult is now in agora
                    sas_adults[n].agoraplus_id = agora_adult['id']
                    sas_adults[n].save()
                # family is now in agora+
                sas_families[0].agoraplus_id = agora_family_id
                sas_families[0].save()

                # create link between name_id and first adult
                login = links[0]['identifiant']
                password = links[0]['mdp']
                AgoraPlusLink.objects.update_or_create(resource=self, name_id=name_id,
                                                       defaults={'login': login, 'password': password})
                family = normalize_family(agora_family)
                provision_roles(name_id)
                provision_attributes(family, login, name_id)
                return {'family': family, 'links': links}

    def add_child_insurance(self, agora_child_id, child):
        if not child.get('insurance_name'):
            return
        assurances = {}
        assurances['compagnie'] = child.get('insurance_name') or ''
        assurances['numPolice'] = child.get('insurance_number') or ''
        return self.request('/enfants/%s/assurances/' % agora_child_id, json=assurances)

    def add_child_authorizations(self, agora_child_id, child):
        autorisations = []
        for autorisation in ((1, 'image'), (2, 'trip')):
            if isinstance(child.get('%s_authorization' % autorisation[1]), bool):
                autorisations.append({
                    'ID': autorisation[0],
                    'VALUE': 'Oui' if child['%s_authorization' % autorisation[1]] else 'Non',
                })
        if autorisations:
            return self.request('enfants/%s/autorisations/' % agora_child_id,
                                json=autorisations)

    def add_child_health_information(self, agora_child_id, child):
        # doctor
        if isinstance(child.get('doctor'), dict) and child['doctor'].get('id'):
            self.request('/enfants/%s/medecins/' % agora_child_id,
                         json={'idMedecin': child['doctor']['id']})
        # hospital
        if 'hospital_authorization' in child:
            hospital = {
                'hopital': child.get('hospital_name') or '',
                'telephone': child.get('hospital_phone') or '',
                'autorisationHospitalisation': 1 if child['hospital_authorization'] else 0,
            }
            self.request('/enfants/%s/hospitalisations/' % agora_child_id,
                         json=hospital)
        # food regimes
        if isinstance(child.get('food_regimes'), dict) and child['food_regimes'].get('id'):
            for food_regime_id in child['food_regimes']['id']:
                self.request('/enfants/%s/regimes_alimentaires/' % agora_child_id,
                             json={'idRegime': food_regime_id, 'commentaire': ''})

    def push_child(self, name_id, login, sas_child_id):
        family = self.get_agoraplus_family(login, raise_error=True)
        sas_children = AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                kind='CHILD', agoraplus_id='', pk=sas_child_id)
        if not sas_children:
            raise ObjectDoesNotExist('no child <%s> in sas' % sas_child_id)
        sas_child = sas_children[0]
        child = sas_child.value
        representant_ids = {}
        # if representants were sas objects when the form was created,
        # get their Agora+ id
        for parent in child['parental_autorithy']:
            p_id = parent['id']
            if p_id.startswith('sas_'):
                representant_ids[p_id] = AgoraPlusSas.objects.get(resource=self, pk=p_id[4:]).agoraplus_id
        agora_child = agoraize_child(child, representant_ids)
        endpoint = 'familles/%s/enfants/' % family['id']
        with wrap_agora(self.request(endpoint, json=agora_child), 'to push child') as push:
            agora_child_id = push['id_enfant']
            sas_child.agoraplus_id = agora_child_id
            sas_child.save()
            # save other informations
            self.add_child_authorizations(agora_child_id, child)
            self.add_child_insurance(agora_child_id, child)
            self.add_child_health_information(agora_child_id, child)
            # return child id
            return {
                'child_id': agora_child_id,
            }

    def push_contact(self, name_id, login, sas_contact_id):
        sas_contacts = AgoraPlusSas.objects.filter(resource=self, name_id=name_id,
                kind='CONTACT', agoraplus_id='', pk=sas_contact_id)
        if not sas_contacts:
            raise ObjectDoesNotExist('no contact <%s> in sas' % sas_contact_id)
        sas_contact = sas_contacts[0]
        contact = sas_contact.value
        child_id_map = {}
        # if child were sas objects when the form was created,
        # get their Agora+ id
        for child in contact['children']:
            c_id = child['id']
            if c_id.startswith('sas_'):
                child_id_map[c_id] = AgoraPlusSas.objects.get(resource=self, pk=c_id[4:]).agoraplus_id
        agora_contact = agoraize_contact(contact, child_id_map)
        endpoint = 'contacts/'
        with wrap_agora_dict(self.request(endpoint, json=agora_contact), 'to push contact') as push:
            if 'code' not in push or int(push['code']) != 1:
                raise AgoraAPIError(push.get('message') or 'no message given, %r' % push)
            sas_contact.agoraplus_id = push['id_contact']
            sas_contact.save()
            return {
                'contact_id': sas_contact.agoraplus_id,
            }

    def incomes_declaration(self, login, incomes_data):
        family = self.get_agoraplus_family(login, raise_error=True)
        adults_id = [adult['id'] for adult in family['adults']]

        allowance_adult_id = incomes_data.get('allowance_adult_id')
        if allowance_adult_id.startswith('sas_'):
            allowance_adult_id = AgoraPlusSas.objects.get(resource=self, pk=allowance_adult_id[4:]).agoraplus_id
        if allowance_adult_id not in adults_id: # safety belt
            allowance_adult_id = adults_id[0]

        # update family informations: get original informations, modify and post them
        with wrap_agora_dict(self.request('/familles/%s' % family['id'])
                        .get('Informations complementaires'),
                        'to update extra infos') as infos:
            infos['allocataire'] = allowance_adult_id
            infos['regime'] = {'id': incomes_data.get('allowance_regime_id') or '', 'name': ''}
            infos['numAlloc'] = incomes_data.get('allowance_number') or ''
            self.request('familles/%s/infos_complementaires/' % family['id'], json=infos)

        # build the new declaration
        declarations_dict = {}
        def add_declaration(adult_id, type_id, period_id, amount):
            if adult_id not in declarations_dict:
                declarations_dict[adult_id] = []
            declarations_dict[adult_id].append({
                'type_revenu': {'id': type_id},
                'periode': {'id': period_id},
                'montant': amount,
            })
        if incomes_data.get('incomes_adult1'):
            add_declaration(adults_id[0], '1', '1', incomes_data['incomes_adult1'])
        if incomes_data.get('rental'):
            add_declaration(adults_id[0], '26', '2', incomes_data['rental'])
        if incomes_data.get('reimbursement'):
            add_declaration(adults_id[0], '29', '2', incomes_data['reimbursement'])
        if incomes_data.get('incomes_allowance'):
            add_declaration(allowance_adult_id, '18', '2', incomes_data['incomes_allowance'])
        if incomes_data.get('incomes_adult2') and len(adults_id) > 1:
            add_declaration(adults_id[1], '1', '1', incomes_data['incomes_adult2'])
        declarations = []
        for adult_id in declarations_dict:
            declarations.append({
                'representant': {'id': adult_id},
                'revenus': declarations_dict[adult_id],
            })
        resources = {
            'idFamille': family['id'],
            'dateDebut': denormalize_date(incomes_data['date_start']),
            'dateFin': denormalize_date(incomes_data['date_stop']),
            'nbPart': incomes_data['nb_part'],
            'typeDeclaration': {'id': incomes_data['declaration_type_id'] or '2', 'nom': ''},
            'declarationsRepresentants': declarations,
        }

        # send declaration
        with wrap_agora_dict(self.request('familles/%s/declarations_revenus/' % family['id'],
                                     json=resources), 'to send tax assesment') as res:
            if int(res['code']) != 1:
                raise AgoraAPIError(res.get('message') or 'no message given, %r' % res)
            # get final result from Agora+
            return self.request('familles/%s/declarations_revenus/%s' % (family['id'],
                                                                         res['id_declaration']))

    def school_enrollment(self, enrollment_data):
        enrollment = {'anneeRef': {'id': 'XX', 'name': ''},
                      'commentaire': 'XX',
                      'dateRentree': '', # DD/MM/YYYY -- optionnal
                      'enfant': {'id': 'XX', 'nom': '', 'prenom': ''},
                      'etablissementAffectation': {'id': 'XX', 'name': ''},
                      'etablissementDerog': [],
                      'etablissementSecteur': {'id': 'XX', 'name': ''},
                      'idInscScol': '',
                      'motifDerog': {'id': '', 'name': ''},
                      'niveau': {'id': 'XX', 'name': ''},
                      'observation': '',
                      'statutInscScol': {'id': '1', 'name': ''}}
        child_id = enrollment_data['child_id']
        # if enrollment has been made with a sas child, get her Agora+ id
        if child_id.startswith('sas_'):
            child_id = AgoraPlusSas.objects.get(resource=self, pk=child_id[4:]).agoraplus_id
        enrollment['enfant']['id'] = child_id
        enrollment['anneeRef']['id'] = enrollment_data['year_id']
        enrollment['etablissementAffectation']['id'] = enrollment_data['school_id']
        enrollment['etablissementSecteur']['id'] = enrollment_data['school_id']
        enrollment['niveau']['id'] = enrollment_data['educational_stage_id']
        enrollment['commentaire'] = enrollment_data['comments']
        if enrollment_data['date_start']:
            enrollment['dateRentree'] = denormalize_date(enrollment_data['date_start'])
        if enrollment_data.get('status_id'):
            enrollment['statutInscScol']['id'] = enrollment_data['status_id']
        with wrap_agora_dict(self.request('inscriptions_scols/', json=enrollment),
                        'to enroll in school') as res:
            if int(res['code']) == 1:
                return {'id': res['id_inscription']}
            raise AgoraAPIError(res.get('message') or 'no message given, %r' % res)

    def nursery_enrollment(self, enrollment_data):
        enrollment = {'anneeRef': {'id': 'XX', 'name': ''},
                      'dateDemande': 'DD/MM/YYYY',
                      'dateEntree': '', # DD/MM/YYYY -- optionnal
                      'dateSortie': '', # DD/MM/YYYY -- optionnal
                      'enfant': {'id': 'XX', 'nom': ' ', 'prenom': ''},
                      'frequentation': {'id': 'XX', 'name': ''},
                      'structs': [], # [{'id': 'XX', 'name': ''}, {'id': 'XX', 'name': ''}],
                      'typeStruct': {'id': '', 'name': ''},
                      'id_demande': '',
                      'commentaire': 'XX',
                      'dateAnnulation': '',
                      'motifAnnulation': {'id': '', 'name': ''},
                      'dateConfirmation': '',
                      'naissPrevu': '', # DD/MM/YYYY -- optionnal
                      'priorites': [],
                      'planning': '0',
                      'gare': '0',
                      'vehicule': '0'}
        child_id = enrollment_data['child_id']
        # if enrollment has been made with a sas child, get her Agora+ id
        if child_id.startswith('sas_'):
            child_id = AgoraPlusSas.objects.get(resource=self, pk=child_id[4:]).agoraplus_id
        enrollment['enfant']['id'] = child_id
        if enrollment_data['expected_birthdate']:
            enrollment['naissPrevu'] = denormalize_date(enrollment_data['expected_birthdate'])
        enrollment['anneeRef']['id'] = enrollment_data['year_id']
        enrollment['dateDemande'] = denormalize_date(enrollment_data['date'])
        if enrollment_data['date_start']:
            enrollment['dateEntree'] = denormalize_date(enrollment_data['date_start'])
        if enrollment_data['date_stop']:
            enrollment['dateSortie'] = denormalize_date(enrollment_data['date_stop'])
        enrollment['frequentation']['id'] = enrollment_data['admission_mode_id']
        if enrollment_data['type_structure_id']:
            enrollment['typeStruct']['id'] = enrollment_data['type_structure_id']
        enrollment['commentaire'] = enrollment_data['comments']
        for struct_id in enrollment_data['structures_ids']:
            enrollment['structs'].append({'id': struct_id, 'name': ''})
        with wrap_agora_dict(self.request('demande_pe/', json=enrollment),
                        'to enroll in nursery') as res:
            if int(res['code']) == 1:
                return {'id': res['id_reservation']}
            raise AgoraAPIError(res.get('message') or 'no message given, %r' % res)

    def get_nursery_enrollment_result(self, enroll_id):
        with wrap_agora(self.request('demande_pe/%s/resultats_commission/' % enroll_id),
                        'to get nursery enrollment result') as res:
            if not isinstance(res, list):
                raise AgoraAPIError(res.get('erreur') or 'no message given, %r' % res)
            return normalize_nursery_enroll_results(res)

    def get_plannings(self, child_id):
        if child_id.startswith('sas_'):
            return []

        with wrap_agora_array(self.request('enfants/%s/inscriptions_pe/' % child_id), 
                              'to get plannings') as regs:
            plannings = []
            now = datetime.now()
            for reg in regs:
                reg_title = '%s (%s)' % (reg.get('structure', {}).get('name') or '',
                                         reg.get('section', {}).get('name') or '')
                for planning in reg.get('planning') or []:
                    planning = normalize_planning(planning)
                    planning['title'] = reg_title
                    # retain only last 3 months ~ 92 days
                    if 0 <= (now - datetime.strptime(planning['date'], '%Y-%m-%d')).days <= 92:
                        plannings.append(planning)
            return plannings

    def add_children_plannings(self, family):
        for child in family.get('children') or []:
            child_id = child.get('original_id') or child['id']
            child['plannings'] = self.get_plannings(child_id)

    def get_school_enrollment(self, child):
        url = 'enfants/%s/inscriptions_scols/' % child['id']
        with wrap_agora(self.request(url),
                        'to get school enrollment') as enrollments:
            if not isinstance(enrollments, list):
                return []
            return [normalize_school_enrollment(e) for e in enrollments]

    def get_invoices(self, login):
        if not login: # unlinked account
            return []
        with wrap_agora_dict(self.get_user(login), 'to get user') as user:
            user_id = user.get('id')
        if not user_id:
            return []
        invoices = []
        with wrap_agora(self.request('comptes_portail/%s/factures/' % user_id),
                        'to get invoices') as raw_invoices:
            for invoice in raw_invoices:
                invoice = normalize_invoice(invoice)
                if invoice:  # do not return unpublished invoices
                    invoices.append(invoice)
            return invoices

    def get_payable_invoices(self, login):
        data = [item for item in self.get_invoices(login) if not item['paid']]
        return data

    def get_historical_invoices(self, login):
        data = [item for item in self.get_invoices(login) if item['paid']]
        return data

    def get_invoice(self, login, invoice_id):
        for i in self.get_invoices(login):
            if i['id'] == invoice_id:
                return i
        raise ObjectDoesNotExist

    def pay_invoice(self, invoice_id, transaction_id, transaction_date):
        endpoint = '/factures/%s' % invoice_id
        with wrap_agora_dict(self.request(endpoint), 'to get invoice') as raw_invoice:
            invoice = normalize_invoice(raw_invoice)
            invoice = {
                'idFacture': invoice_id,
                'montant': str(invoice['amount']),
                'numAutorisation': transaction_id,
                'dateReglement': transaction_date
            }
            with wrap_agora(self.request('reglements/', json=invoice), 'to pay invoice') as res:
                return res.get('code') == '0'

    def update_address(self, family, new_address, leaving_date):
        new_address = agoraize_address(new_address)
        address_id = family['address']['id']
        new_address['id'] = address_id
        new_address['demenagement'] = 'true'
        new_address['dateDemenagement'] = denormalize_date(leaving_date)
        if new_address['commune'].get('id'):
            # required by agora but useless
            new_address['commune']['nom'] = ''
        if new_address['voie'].get('id'):
            # required by agora but useless
            new_address['voie']['nom'] = ''
        endpoint = '/familles/%s/adresses/%s' % (family['id'], address_id)
        with wrap_agora_dict(self.request(endpoint, json=new_address, method='PUT'),
                        'to update address') as res:
            if res.get('code') != '1':
                raise AgoraAPIError(res.get('message') or 'no message given, %r' % res)
            return True

    def get_agoraplus_adult_id(self, adult_id, login, name_id):
        if not adult_id.startswith('sas_'):
            return adult_id
        family = self.get_family(login=login, name_id=name_id)
        for adult in family['adults']:
            if adult_id == adult['id']:
                return adult['original_id']
        raise ObjectDoesNotExist(_('adult not in Agora+'))

    def update_phone_numbers(self, login, name_id, adult_id,
                             new_phone_number, new_cellphone_number):
        family = self.get_agoraplus_family(login, raise_error=True)
        # get adult infos
        agoraplus_adult_id = self.get_agoraplus_adult_id(adult_id, login, name_id)
        endpoint = 'familles/%s/representants/%s' % (family['id'], agoraplus_adult_id)
        adult_data = self.request(endpoint)
        adult_data['fixPerso'] = new_phone_number
        adult_data['mobilePerso'] = new_cellphone_number

        # update adult data
        with wrap_agora_dict(self.request(endpoint, json=adult_data, method='PUT'),
                        'to update phone numbers') as res:
            if res.get('code') != '1':
                raise AgoraAPIError(res.get('message') or 'no message given, %r' % res)
            return True

    def update_profession(self, login, name_id, adult_id, new_profession, new_pcs,
                          new_employer_name, new_employer_city, new_employer_phone):
        family = self.get_agoraplus_family(login, raise_error=True)
        # get adult infos
        agoraplus_adult_id = self.get_agoraplus_adult_id(adult_id, login, name_id)
        endpoint = 'familles/%s/representants/%s' % (family['id'], agoraplus_adult_id)
        adult_data = self.request(endpoint)
        adult_data['employeur'] = new_employer_name
        adult_data['telEmployeur'] = new_employer_phone
        adult_data['profession'] = new_profession
        adult_data['pcs']['id'] = new_pcs['id']
        adult_data['adresseEmployeur']['commune'].update({'nom': new_employer_city})

        # update adult data
        with wrap_agora_dict(self.request(endpoint, json=adult_data, method='PUT'),
                        'to update profession') as res:
            if res.get('code') != '1':
                raise AgoraAPIError(res.get('message') or 'no message given, %r' % res)
            return True

    def get_invoice_pdf(self, login, invoice_id):
        invoices = self.get_invoices(login)
        if not invoices:
            raise ObjectDoesNotExist
        if invoice_id not in [i['id'] for i in invoices]:
            logger.warning('failure to retrieve invoice %r for login %r: invoice\'s id unknown'
                           % (invoice_id, login))
            raise PermissionDenied
        endpoint = 'download_factures/?p_id_facture=%s&p_type=pdf' % invoice_id
        cookies = {'portail_session_id': self.get_token()}
        return self.requests.get(self.url + endpoint, cookies=cookies).content

    def get_document(self, uri, content_type):
        cookies = {'portail_session_id': self.get_token()}
        response = self.requests.get(self.url + uri, cookies=cookies)
        if content_type != response.headers['content-type'].split(';')[0].strip().lower():
            return None
        return response.content


class AgoraPlusLink(models.Model):
    '''
    Link between a Publik user (name_id) and a Agora+ user (login/password)
    '''
    resource = models.ForeignKey(AgoraPlus)
    name_id = models.CharField(blank=False, max_length=256)  # Publik user key
    login = models.CharField(blank=False, max_length=128)    # Agora+ user key
    password = models.CharField(blank=False, max_length=128) # Agora+ auth


SAS_KIND_OF_VALUES = (
    ('FAMILY', 'FAMILY'),
    ('ADULT', 'ADULT'),
    ('CHILD', 'CHILD'),
    ('CONTACT', 'CONTACT'),
)


class AgoraPlusSas(models.Model):
    '''
    Stores informations before sending in Agora +
    '''
    resource = models.ForeignKey(AgoraPlus)
    name_id = models.CharField(blank=False, max_length=256)  # Publik user key
    kind = models.CharField(blank=False, max_length=16, choices=SAS_KIND_OF_VALUES)
    agoraplus_id = models.CharField(blank=True, max_length=16)
    value = JSONField(blank=True)
