# Passerelle - uniform access to data and services
# Copyright (C) 2016  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 os

from django.db import models
from django.utils.translation import gettext_lazy as _

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

from . import mdel
from .utils import get_file_content_from_zip, parse_date, zipdir

DEMAND_TYPES = ['ILE-LA', 'RCO-LA', 'AEC-LA']

STATUS_MAPPING = {
    '100': 'closed',
    '20': 'rejected',
    '19': 'accepted',
    '17': 'information needed',
    '16': 'in progress',
    '15': 'invalid',
    '14': 'imported',
}

APPLICANTS = [
    {"id": "PersonneConcernee", "text": "La personne concernée par l'acte"},
    {"id": "PereMere", "text": "Son père ou sa mère"},
    {"id": "Conjoint", "text": "Son conjoint ou sa conjointe"},
    {"id": "Fils", "text": "Son fils ou sa fille"},
    {"id": "GrandPere", "text": "Son grand-père ou sa grand-mère"},
    {"id": "PetitFils", "text": "Son petit-fils ou sa petite-fille"},
    {"id": "Representant", "text": "Son représentant légal"},
    {"id": "Heritier", "text": "Son héritier"},
    {"id": "Autre", "text": "Autre"},
]

CERTIFICATES = [
    {'id': 'NAISSANCE', 'text': 'Acte de naissance'},
    {'id': 'MARIAGE', 'text': 'Acte de mariage'},
    {'id': 'DECES', 'text': 'Acte de décès'},
]

CERTIFICATE_TYPES = [
    {'id': 'COPIE-INTEGRALE', 'text': 'Copie intégrale'},
    {'id': 'EXTRAIT-AVEC-FILIATION', 'text': 'Extrait avec filiation'},
    {'id': 'EXTRAIT-SANS-FILIATION', 'text': 'Extrait sans filiation'},
    {'id': 'EXTRAIT-PLURILINGUE', 'text': 'Extrait plurilingue'},
]


class MDEL(BaseResource):
    """Connector converting Publik's Demand into
    MDEL (XML based format) Demand.
    It's able to manage demand such as :
    - Inscription sur Liste Eletoral (ILE-LA)
    - Acte d'Etat Civil (AEC-LA)
    - Recensement Citoyen Obligatoire (RCO-LA)
    """

    outcoming_sftp = SFTPField(verbose_name=_('SFTP server for outgoing files'), blank=True)
    incoming_sftp = SFTPField(verbose_name=_('SFTP server for incoming files'), blank=True)

    category = _('Civil Status Connectors')

    class Meta:
        verbose_name = 'Mes Demarches En Ligne'

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

    @endpoint(perm='can_access', methods=['post'])
    def create(self, request, *args, **kwargs):
        """Create a demand"""
        formdata = json.loads(request.body)
        extra = formdata.pop('extra', {})
        fields = formdata.pop('fields', {})

        def flatten_payload(*subs):
            result = {}
            for sub in subs:
                result.update(sub)  # priority on last sub dict
            return result

        # flatten the payload
        formdata.update(flatten_payload(fields, extra))

        demand_type = formdata.pop('demand_type', '').upper()
        if demand_type not in DEMAND_TYPES:
            raise APIError('demand_type must be : %r' % DEMAND_TYPES)

        if 'display_id' not in formdata:
            raise APIError('display_id is required')

        if 'code_insee' not in formdata:
            raise APIError('code_insee is required')

        demand_id = formdata.pop('display_id')

        demand, created = Demand.objects.get_or_create(num=demand_id, flow_type=demand_type, resource=self)
        if not created:
            demand.step += 1

        demand.create_zip(formdata)

        demand.save()

        if self.outcoming_sftp:
            self.add_job('send_demand', demand_id=demand.demand_id)

        return {'data': {'demand_id': demand.demand_id}}

    @endpoint(perm='can_access')
    def status(self, request, *args, **kwargs):
        """Return demand's statutes"""
        demand_id = request.GET.get('demand_id', None)
        if not demand_id:
            raise APIError('demand_id is required')

        try:
            demand = Demand.objects.get(demand_id=demand_id, resource=self)
        except Demand.DoesNotExist:
            raise APIError('demand %s does not exist' % demand_id)

        status = demand.get_status()

        demand.save()
        return {'data': status}

    @endpoint(perm='can_access')
    def applicants(self, request, without=''):
        return {'data': [item for item in APPLICANTS if item.get('id') not in without.split(',')]}

    @endpoint(perm='can_access')
    def certificates(self, request):
        return {'data': CERTIFICATES}

    @endpoint(name='certificate-types', perm='can_access')
    def certificate_types(self, request, without=''):
        return {'data': [item for item in CERTIFICATE_TYPES if item.get('id') not in without.split(',')]}

    @property
    def input_dir(self):
        return os.path.join(mdel.get_resource_base_dir(), self.slug, 'inputs')

    @property
    def output_dir(self):
        return os.path.join(mdel.get_resource_base_dir(), self.slug, 'outputs')

    def send_demand(self, demand_id):
        try:
            demand = Demand.objects.get(demand_id=demand_id)
        except Demand.DoesNotExist:
            return
        with self.outcoming_sftp.client() as client:
            client.put(demand.filepath, os.path.join(client.getcwd(), demand.filename))

    def get_response_files(self):
        if not os.path.exists(self.output_dir):
            os.makedirs(self.output_dir)

        with self.incoming_sftp.client() as client:
            for filename in client.listdir():
                if filename in os.listdir(self.output_dir):
                    continue
                with atomic_write(os.path.join(self.output_dir, filename)) as fd:
                    client.getfo(filename, fd)

    def hourly(self):
        super().hourly()
        if self.incoming_sftp:
            self.get_response_files()


class Demand(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    resource = models.ForeignKey(MDEL, on_delete=models.CASCADE)
    num = models.CharField(max_length=64, null=False, primary_key=True, unique=True)
    flow_type = models.CharField(max_length=32, null=False)
    status = models.CharField(max_length=32, null=True)
    step = models.IntegerField(default=0)
    demand_id = models.CharField(max_length=128, null=True)

    def __str__(self):
        return '%s - %s - %s' % (self.resource.slug, self.demand_id, self.status)

    class Meta:
        unique_together = ('num', 'flow_type')

    def save(self, *args, **kwargs):
        self.demand_id = '%s-%s' % (self.num, self.flow_type)
        super().save(*args, **kwargs)

    @property
    def name(self):
        if self.flow_type == 'AEC-LA':
            return '%s-%s-%d' % (self.num, 'EtatCivil', self.step)
        return '%s-%s--%d' % (self.num, self.flow_type, self.step)

    @property
    def filename(self):
        return '%s.zip' % self.name

    @property
    def filepath(self):
        return os.path.join(self.resource.input_dir, self.filename)

    def create_zip(self, formdata):
        """
        Creates demand as zip folder
        """

        code_insee = formdata['code_insee']
        flow_type = self.flow_type
        demand_num = self.num

        inputs_dir = os.path.join(self.resource.input_dir, self.name)
        input_files = {}
        attached_files = []

        if flow_type in ('ILE-LA', 'AEC-LA'):

            proofs = [('JI', 'justificatif_identite'), ('JD', 'justificatif_domicile')]

            for proof_code, proof_attribute in proofs:

                documents = [
                    value
                    for key, value in formdata.items()
                    if key.startswith(proof_attribute)
                    and isinstance(value, dict)
                    and 'filename' in value
                    and 'content' in value
                ]
                for document in documents:
                    filename, b64_content = document.get('filename'), document.get('content')
                    attached_files.append(mdel.AttachedFile(proof_code, filename, b64_content))
                if flow_type == 'ILE-LA' and not documents:
                    raise APIError('%s and all its attributes are required' % proof_attribute)

        if flow_type == 'ILE-LA':
            # process address additional information
            adresse_complement = []

            complement_keys = sorted(key for key in formdata if key.startswith('adresse_complement'))

            for key in complement_keys:
                adresse_complement.append(formdata[key])

            if adresse_complement:
                formdata['adresse_complement'] = ', '.join(adresse_complement)

            # get contact info
            contacts = [('TEL', 'contact_telephone'), ('EMAIL', 'contact_email')]

            for contact_code, contact_uri in contacts:
                uri = formdata.get(contact_uri)
                if uri:
                    formdata['contact_uri'] = uri
                    formdata['contact_code'] = contact_code

            # mdel protocol only supports two values here (prem & cci)
            # but we may want the form to have more specific values;
            # we name them (prem|cci)_suffix and squeeze the suffix here.
            if not formdata.get('anterieur_situation_raw', None):
                raise APIError('anterieur_situation_raw is required')

            formdata['anterieur_situation_raw'] = formdata.get('anterieur_situation_raw').split('_')[0]

            doc = mdel.ILEData(self.demand_id, formdata)
            doc.save(inputs_dir)
            input_files['demande'] = doc.filename

        elif flow_type == 'RCO-LA':
            raise APIError('RCO-LA processing not implemented')

        else:
            # Set date format
            if formdata.get('aec_type_raw') != 'NAISSANCE' and not formdata.get('date_acte'):
                raise APIError('<date_acte> is required')
            if formdata.get('date_acte'):
                formdata['date_acte'] = parse_date(formdata['date_acte'])
            if formdata.get('titulaire_naiss_date'):
                formdata['titulaire_naiss_date'] = parse_date(formdata['titulaire_naiss_date'])
            if formdata.get('titulaire2_naiss_date'):
                formdata['titulaire2_naiss_date'] = parse_date(formdata['titulaire2_naiss_date'])

            # Ensuring that all titles are uppercase
            for key in formdata.keys():
                if 'civilite' in key:
                    formdata[key] = formdata[key].upper()

            # Merging street number and street name
            demandeur_adresse_voie = formdata.get('demandeur_adresse_voie')
            demandeur_adresse_num = formdata.get('demandeur_adresse_num')
            if demandeur_adresse_voie and demandeur_adresse_num:
                formdata['demandeur_adresse_voie'] = '%s %s' % (demandeur_adresse_num, demandeur_adresse_voie)

            # Set foreign address if country is not France
            adresse_keys = ['etage', 'batiment', 'voie', 'code_postal', 'ville']
            adresse_keys = ['demandeur_adresse_%s' % key for key in adresse_keys]
            demandeur_adresse_pays_raw = formdata.get('demandeur_adresse_pays_raw')
            demandeur_adresse_etrangere = formdata.get('demandeur_adresse_etrangere')
            demandeur_adresse_etrangere_pays_raw = formdata.get('demandeur_adresse_etrangere_pays_raw')
            if demandeur_adresse_etrangere_pays_raw or (
                demandeur_adresse_pays_raw and demandeur_adresse_pays_raw != 'FRA'
            ):
                formdata.pop('demandeur_adresse_pays_raw', None)
                if not demandeur_adresse_etrangere_pays_raw:
                    formdata['demandeur_adresse_etrangere_pays_raw'] = demandeur_adresse_pays_raw
                if demandeur_adresse_etrangere:
                    # dismiss french address if the foreign one is filled
                    for key in adresse_keys:
                        if key in formdata:
                            del formdata[key]
                else:
                    # build foreign address from french address fields
                    adresse_etrangere = []
                    for key in adresse_keys:
                        value = formdata.pop(key, '')
                        if value:
                            if key != 'demandeur_adresse_ville':
                                adresse_etrangere.append(value)
                            else:
                                adresse_etrangere[-1] += ' %s' % value
                    formdata['demandeur_adresse_etrangere'] = ', '.join(adresse_etrangere)

            # Set aec_nature if aec_type_raw == DECES
            if formdata.get('aec_type_raw') == 'DECES' and not formdata.get('aec_nature_raw'):
                formdata['aec_nature'] = 'Copie integrale'
                formdata['aec_nature_raw'] = 'COPIE-INTEGRALE'

            # Set motif_demand if none
            if not formdata.get('motif_demande'):
                formdata['motif_demande'] = 'Autre'
                formdata['motif_demande_raw'] = 'Autre'

            # handling requester when 'Autre'
            if formdata.get('qualite_demandeur_autre') and not formdata.get('qualite_demandeur_raw'):
                formdata['qualite_demandeur'] = formdata['qualite_demandeur_autre']
                formdata['qualite_demandeur_raw'] = 'Autre'

            doc = mdel.AECData(self.demand_id, formdata, self.step)
            doc.save(inputs_dir)
            input_files['demande'] = doc.filename

        for attached_file in attached_files:
            attached_file.save(inputs_dir)

        submission_date = formdata.get('receipt_time', None)

        message = mdel.Message(flow_type, demand_num, code_insee, date=submission_date, doc=doc)
        message.save(inputs_dir)
        input_files['message'] = message.filename

        description = mdel.Description(
            flow_type,
            demand_num,
            code_insee,
            date=submission_date,
            attached_files=attached_files,
            step=self.step,
            doc=doc,
            user_comment=formdata.get('logitud_commentaire_usager'),
        )
        description.save(inputs_dir)
        input_files['enveloppe'] = description.filename

        # create zip file
        return zipdir(inputs_dir, input_files)

    def get_status(self):
        """Read demand' statuses from file"""

        result = {'closed': False, 'status': None, 'comment': ''}

        namespace = {'ns2': 'http://finances.gouv.fr/dgme/pec/message/v1'}

        if not os.path.exists(self.resource.output_dir):
            os.makedirs(self.resource.output_dir)

        # list all demand response zip files
        zip_files = {}
        for zfile in os.listdir(self.resource.output_dir):
            if zfile.lower().startswith(self.demand_id.lower()) and zfile.lower().endswith('.zip'):
                fname = os.path.splitext(zfile)[0]
                try:
                    step = int(fname.split('--')[-1])
                except (TypeError, ValueError):
                    continue
                zip_files[step] = zfile

        # get the file with the highest number
        if zip_files:
            self.step = max(zip_files)
            zip_file = zip_files[self.step]
        else:
            return result

        path = os.path.join(self.resource.output_dir, zip_file)

        content = get_file_content_from_zip(path, 'message.xml')
        element = mdel.etree.fromstring(content)
        majs = element.findall('ns2:Body/*/*/*/ns2:Maj', namespace)

        statuses = []

        for maj in majs:
            etat = maj.findtext('ns2:Etat', namespaces=namespace)
            commentaire = maj.findtext('ns2:Commentaire', namespaces=namespace)

            if etat:
                statuses.append({'etat': etat, 'commentaire': commentaire})

        statuses = sorted(statuses, key=lambda x: int(x['etat']))[-2:]

        for status in statuses:
            if status['etat'] == '100':
                result['closed'] = True
            else:
                result['status'] = STATUS_MAPPING[status['etat']]
                result['comment'] = status.get('commentaire', '')

        self.status = result['status']
        self.save()

        return result
