# Copyright (C) 2020 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 collections import defaultdict
from urllib.parse import urljoin

import zeep
from django.db import models
from django.utils import timezone
from django.utils.dateparse import parse_date
from django.utils.translation import gettext_lazy as _
from zeep.helpers import serialize_object
from zeep.wsse.username import UsernameToken

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

from . import utils

LINK_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "Maelis",
    "description": "",
    "type": "object",
    "required": ["family_id", "password"],
    "properties": {
        "family_id": {
            "description": "family_id",
            "type": "string",
        },
        "password": {
            "description": "family password",
            "type": "string",
        },
        "school_year": {
            "description": "school year",
            "type": "string",
        },
    },
}

COORDINATES_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "Maelis",
    "description": "Person Coordinates",
    "type": "object",
    "properties": {
        "num": {"description": "number", "type": "string", "pattern": "^[0-9]*$"},
        "street": {
            "description": "street",
            "type": "string",
        },
        "zipcode": {
            "description": "zipcode",
            "type": "string",
        },
        "town": {
            "description": "town",
            "type": "string",
        },
        "phone": {
            "description": "phone",
            "type": "string",
        },
        "mobile": {
            "description": "mobile",
            "type": "string",
        },
        "mail": {
            "description": "mail",
            "type": "string",
        },
    },
}


class Maelis(BaseResource):
    base_url = models.URLField(_('Base API URL'), default='http://www3.sigec.fr/entrouvertws/services/')
    login = models.CharField(_('API Login'), max_length=256)
    password = models.CharField(_('API Password'), max_length=256)

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = 'Maélis'

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

    def check_status(self):
        response = self.requests.get(self.base_url)
        response.raise_for_status()

    def get_client(self, wsdl_name):
        wsse = UsernameToken(self.login, self.password)
        wsdl_url = urljoin(self.base_url, wsdl_name)
        return self.soap_client(wsdl_url=wsdl_url, wsse=wsse)

    def call(self, wsdl_name, service, **kwargs):
        client = self.get_client(wsdl_name)
        method = getattr(client.service, service)
        try:
            return method(**kwargs)
        except zeep.exceptions.Fault as e:
            raise APIError(e)

    def get_link(self, name_id):
        try:
            return self.link_set.get(name_id=name_id)
        except Link.DoesNotExist:
            raise APIError('User not linked to family', err_code='not-found')

    def get_family_data(self, family_id, school_year=None):
        if not school_year:
            # fallback to current year if not provided
            school_year = utils.get_school_year()
        family_data = serialize_object(
            self.call('FamilyService?wsdl', 'readFamily', dossierNumber=family_id, schoolYear=school_year)
        )
        for child in family_data['childInfoList']:
            utils.normalize_person(child)
        return family_data

    def get_child_info(self, NameID, childID):
        link = self.get_link(NameID)
        family_data = self.get_family_data(link.family_id)

        for child in family_data.get('childInfoList', []):
            if child['num'] == childID:
                return child

        raise APIError('Child not found', err_code='not-found')

    def get_invoices(self, regie_id, name_id):
        family_id = self.get_link(name_id).family_id
        return [
            utils.normalize_invoice(i)
            for i in self.call(
                'InvoiceService?wsdl', 'readInvoices', numDossier=family_id, codeRegie=regie_id
            )
        ]

    @endpoint(
        display_category=_('Family'),
        display_order=1,
        description=_('Create link between user and family'),
        perm='can_access',
        parameters={
            'NameID': {'description': _('Publik ID')},
        },
        post={'request_body': {'schema': {'application/json': LINK_SCHEMA}}},
    )
    def link(self, request, NameID, post_data):
        if 'school_year' not in post_data:
            # fallback to default year if not provided
            post_data['school_year'] = utils.get_school_year()
        r = self.call(
            'FamilyService?wsdl',
            'readFamilyByPassword',
            dossierNumber=post_data['family_id'],
            password=post_data['password'],
            schoolYear=post_data['school_year'],
        )
        if not r.number:
            raise APIError('Family not found', err_code='not-found')
        Link.objects.update_or_create(resource=self, name_id=NameID, defaults={'family_id': r.number})
        return {'data': serialize_object(r)}

    @endpoint(
        display_category=_('Family'),
        display_order=2,
        description=_('Delete link between user and family'),
        methods=['post'],
        perm='can_access',
        parameters={
            'NameID': {'description': _('Publik ID')},
        },
    )
    def unlink(self, request, NameID):
        link = self.get_link(NameID)
        link_id = link.pk
        link.delete()
        return {'link': link_id, 'deleted': True, 'family_id': link.family_id}

    @endpoint(
        display_category=_('Family'),
        display_order=4,
        description=_("Get information about user's family"),
        name='family-info',
        perm='can_access',
        parameters={
            'NameID': {'description': _('Publik ID')},
        },
    )
    def family_info(self, request, NameID):
        link = self.get_link(NameID)
        family_data = self.get_family_data(link.family_id)
        return {'data': family_data}

    @endpoint(
        display_category=_('Family'),
        display_order=6,
        description=_("Get information about children"),
        perm='can_access',
        name='children-info',
        parameters={
            'NameID': {'description': _('Publik ID')},
        },
    )
    def children_info(self, request, NameID):
        link = self.get_link(NameID)
        family_data = self.get_family_data(link.family_id)
        return {'data': family_data['childInfoList']}

    @endpoint(
        display_category=_('Family'),
        display_order=7,
        description=_("Get information about adults"),
        perm='can_access',
        name='adults-info',
        parameters={
            'NameID': {'description': _('Publik ID')},
        },
    )
    def adults_info(self, request, NameID):
        link = self.get_link(NameID)
        family_data = self.get_family_data(link.family_id)
        adults = []
        if family_data.get('rl1InfoBean'):
            adults.append(utils.normalize_person(family_data['rl1InfoBean']))
        if family_data.get('rl2InfoBean'):
            adults.append(utils.normalize_person(family_data['rl2InfoBean']))
        return {'data': adults}

    @endpoint(
        display_category=_('Family'),
        display_order=7,
        description=_("Get information about a child"),
        perm='can_access',
        name='child-info',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
        },
    )
    def child_info(self, request, NameID, childID):
        return {'data': self.get_child_info(NameID, childID)}

    @endpoint(
        display_category=_('Family'),
        display_order=7,
        description=_('Update coordinates'),
        perm='can_access',
        name='update-coordinates',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'personID': {'description': _('Person ID')},
        },
        post={'request_body': {'schema': {'application/json': COORDINATES_SCHEMA}}},
    )
    def update_coordinates(self, request, NameID, personID, post_data):
        link = self.get_link(NameID)
        params = defaultdict(dict)
        for address_param in ('num', 'zipcode', 'town'):
            if address_param in post_data:
                params['adresse'][address_param] = post_data[address_param]
        if 'street' in post_data:
            params['adresse']['street1'] = post_data['street']

        for contact_param in ('phone', 'mobile', 'mail'):
            if contact_param in post_data:
                params['contact'][contact_param] = post_data[contact_param]

        r = self.call(
            'FamilyService?wsdl', 'updateCoordinate', numDossier=link.family_id, numPerson=personID, **params
        )
        return serialize_object(r)

    @endpoint(
        display_category=_('Invoices'),
        display_order=1,
        name='regie',
        perm='can_access',
        pattern=r'^(?P<regie_id>[\w-]+)/invoices/?$',
        example_pattern='{regie_id}/invoices',
        description=_("Get invoices to pay"),
        parameters={
            'NameID': {'description': _('Publik ID')},
            'regie_id': {'description': _('Regie identifier'), 'example_value': '42-42'},
        },
    )
    def invoices(self, request, regie_id, NameID):
        invoices = [i for i in self.get_invoices(regie_id=regie_id, name_id=NameID) if not i['paid']]
        return {'data': invoices}

    @endpoint(
        display_category=_('Invoices'),
        display_order=2,
        name='regie',
        perm='can_access',
        pattern=r'^(?P<regie_id>[\w-]+)/invoices/history/?$',
        example_pattern='{regie_id}/invoices/history',
        description=_("Get invoices already paid"),
        parameters={
            'NameID': {'description': _('Publik ID')},
            'regie_id': {'description': _('Regie identifier'), 'example_value': '42-42'},
        },
    )
    def invoices_history(self, request, regie_id, NameID):
        invoices = [i for i in self.get_invoices(regie_id=regie_id, name_id=NameID) if i['paid']]
        return {'data': invoices}

    @endpoint(
        display_category=_('Invoices'),
        display_order=3,
        name='regie',
        perm='can_access',
        pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/?$',
        example_pattern='{regie_id}/invoice/{invoice_id}',
        description=_('Get invoice details'),
        parameters={
            'NameID': {'description': _('Publik ID')},
            'regie_id': {'description': _('Regie identifier'), 'example_value': '1'},
            'invoice_id': {'description': _('Invoice identifier'), 'example_value': '42-42'},
        },
    )
    def invoice(self, request, regie_id, invoice_id, NameID):
        for invoice in self.get_invoices(regie_id=regie_id, name_id=NameID):
            if invoice['id'] == invoice_id:
                return {'data': invoice}

    @endpoint(
        display_category=_('Invoices'),
        display_order=4,
        name='regie',
        perm='can_access',
        pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/pdf/?$',
        example_pattern='{regie_id}/invoice/{invoice_id}/pdf',
        description=_('Get invoice as a PDF file'),
        parameters={
            'NameID': {'description': _('Publik ID')},
            'regie_id': {'description': _('Regie identifier'), 'example_value': '1'},
            'invoice_id': {'description': _('Invoice identifier'), 'example_value': '42-42'},
        },
    )
    def invoice_pdf(self, request, regie_id, invoice_id, **kwargs):
        # TODO to implement
        pass

    @endpoint(
        perm='can_access',
        description=_('Get activity list'),
        name='activity-list',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'personID': {'description': _('Person ID')},
            'school_year': {'description': _('School year')},
        },
    )
    def activity_list(
        self, request, NameID, personID, school_year=None, start_datetime=None, end_datetime=None
    ):
        link = self.get_link(NameID)
        family_data = self.get_family_data(link.family_id)
        if personID not in [c['id'] for c in family_data['childInfoList']]:
            raise APIError('Child not found', err_code='not-found')
        if not school_year:
            school_year = utils.get_school_year()
        if not start_datetime:
            start_datetime = timezone.now()
        if not end_datetime:
            end_datetime = start_datetime + timezone.timedelta(days=62)
        r = self.call(
            'ActivityService?wsdl',
            'readActivityList',
            schoolyear=school_year,
            numPerson=personID,
            dateStartCalend=start_datetime,
            dateEndCalend=end_datetime,
        )
        activities = serialize_object(r)
        return {'data': [utils.normalize_activity(a) for a in activities]}

    def get_activities_dates(self, query_date):
        if query_date:
            try:
                start_date = parse_date(query_date)
            except ValueError as exc:
                raise APIError('input is well formatted but not a valid date: %s' % exc)
            if not start_date:
                raise APIError("input isn't well formatted, YYYY-MM-DD expected: %s" % query_date)
        else:
            start_date = timezone.now().date()
        if start_date.strftime('%m-%d') >= '05-01':
            # start displaying next year activities on may
            school_year = start_date.year
        else:
            school_year = start_date.year - 1
        end_date = utils.get_datetime('%s-07-31' % (school_year + 1)).date()
        return school_year, start_date, end_date

    def get_child_activities(self, childID, school_year, start_date, end_date):
        r = self.call(
            'ActivityService?wsdl',
            'readActivityList',
            schoolyear=school_year,
            numPerson=childID,
            dateStartCalend=start_date,
            dateEndCalend=end_date,
        )
        return serialize_object(r)

    @endpoint(
        display_category=_('Activities'),
        perm='can_access',
        display_order=2,
        description=_('Get child activities'),
        name='child-activities',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
            'subscribePublication': {'description': _('string including E, N or L (default to "E")')},
            'subscribingStatus': {'description': _('subscribed, not-subscribed or None')},
            'queryDate': {'description': _('Optional querying date (YYYY-MM-DD)')},
        },
    )
    def child_activities(
        self, request, NameID, childID, subscribePublication='E', subscribingStatus=None, queryDate=None
    ):
        if subscribingStatus and subscribingStatus not in ('subscribed', 'not-subscribed'):
            raise APIError('wrong value for subscribingStatus: %s' % subscribingStatus)
        school_year, start_date, end_date = self.get_activities_dates(queryDate)
        child_info = self.get_child_info(NameID, childID)
        activities = self.get_child_activities(childID, school_year, start_date, end_date)
        flatted_activities = utils.flatten_activities(activities, start_date, end_date)
        utils.mark_subscribed_flatted_activities(flatted_activities, child_info)
        data = utils.flatted_activities_as_list(
            flatted_activities, subscribePublication, subscribingStatus, start_date
        )
        return {'data': data}

    @endpoint(
        display_category=_('Activities'),
        perm='can_access',
        display_order=3,
        description=_('Get bus lines (units)'),
        name='bus-lines',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
            'activityID': {'description': _('Activity ID')},
            'unitID': {'description': _('Unit ID')},
            'queryDate': {'description': _('Optional querying date (YYYY-MM-DD)')},
            'direction': {'description': _('aller, retour or None')},
        },
    )
    def bus_lines(self, request, NameID, childID, activityID, unitID, queryDate=None, direction=None):
        if direction and direction.lower() not in ('aller', 'retour'):
            raise APIError('wrong value for direction: %s' % direction)
        school_year, start_date, end_date = self.get_activities_dates(queryDate)
        self.get_child_info(NameID, childID)
        activities = self.get_child_activities(childID, school_year, start_date, end_date)
        flatted_activities = utils.flatten_activities(activities, start_date, end_date)
        legacy_activity_info = flatted_activities[activityID]['info']
        legacy_unit_info = flatted_activities[activityID]['units'][unitID]['info']

        bus_lines = []
        bus_activity_id = legacy_activity_info['bus_activity_id']
        for bus_unit_id in legacy_activity_info['bus_unit_ids']:
            bus_unit_info = flatted_activities[bus_activity_id]['units'][bus_unit_id]['info']
            if direction and direction.lower() not in bus_unit_info['unit_text']:
                continue
            unit_calendar_letter = bus_unit_info['unit_calendar_letter']
            unit_weekly_planning = ""
            for letter in legacy_activity_info['activity_weekly_planning_mask']:
                if letter == '0':
                    unit_weekly_planning += unit_calendar_letter
                else:
                    unit_weekly_planning += '1'
            bus_lines.append(
                {
                    'id': bus_unit_info['unit_id'],
                    'text': bus_unit_info['unit_text'],
                    'unit_id': bus_unit_info['unit_id'],
                    'activity_id': bus_activity_id,
                    'unit_calendar_letter': unit_calendar_letter,
                    'unit_weekly_planning': unit_weekly_planning,
                    'subscribe_start_date': legacy_unit_info['unit_start_date'],
                    'subscribe_end_date': legacy_unit_info['unit_end_date'],
                }
            )
        return {'data': bus_lines}

    @endpoint(
        display_category=_('Activities'),
        perm='can_access',
        display_order=4,
        description=_('Get bus stops (places)'),
        name='bus-stops',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
            'busActivityID': {'description': _('Activity ID')},
            'busUnitID': {'description': _('Bus Unit ID')},
            'queryDate': {'description': _('Optional querying date (YYYY-MM-DD)')},
        },
    )
    def bus_stops(self, request, NameID, childID, busActivityID, busUnitID, queryDate=None):
        school_year, start_date, end_date = self.get_activities_dates(queryDate)
        self.get_child_info(NameID, childID)
        activities = self.get_child_activities(childID, school_year, start_date, end_date)

        for activity in activities:
            if activity['activityPortail']['idAct'] != busActivityID:
                continue
            break
        else:
            raise APIError('Bus activity not found: %s' % busActivityID, err_code='not-found')

        for unit in activity['unitPortailList']:
            if unit['idUnit'] != busUnitID:
                continue
            break
        else:
            raise APIError('Bus unit not found: %s' % busUnitID, err_code='not-found')

        bus_stops = []
        for place in unit['placeList']:
            bus_stops.append(
                {
                    'id': place['id'],
                    'text': ' '.join([w.capitalize() for w in place['lib'].split(' ')]),
                }
            )
        if bus_stops:
            bus_stops[0]['disabled'] = True  # hide terminus
        return {'data': bus_stops}

    @endpoint(
        display_category=_('Activities'),
        perm='can_access',
        display_order=3,
        description=_('Read child planning'),
        name='child-planning',
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
            'start_date': {'description': _('Start date (YYYY-MM-DD format)')},
            'end_date': {'description': _('End date (YYYY-MM-DD format)')},
            'legacy': {'description': _('Decompose events related to parts of the day if set')},
        },
    )
    def child_planning(self, request, NameID, childID, start_date=None, end_date=None, legacy=None):
        """Return an events list sorted by id"""
        link = self.get_link(NameID)
        family_data = self.get_family_data(link.family_id)
        if childID not in [c['id'] for c in family_data['childInfoList']]:
            raise APIError('Child not found', err_code='not-found')
        if start_date and end_date:
            start = utils.get_datetime(start_date)
            end = utils.get_datetime(end_date)
        else:
            start, end = utils.week_boundaries_datetimes(start_date)
        school_year = utils.get_school_year(start.date())

        r = self.call(
            'ActivityService?wsdl',
            'readActivityList',
            schoolyear=school_year,
            numPerson=childID,
            dateStartCalend=start,
            dateEndCalend=end,
        )
        activities = serialize_object(r)
        events = {key: value for a in activities for (key, value) in utils.get_events(a, start, end)}

        for date in utils.month_range(start, end):
            r = self.call(
                'ActivityService?wsdl',
                'readChildMonthPlanning',
                year=date.year,
                numMonth=date.month,
                numPerson=childID,
            )
            planning = serialize_object(r['calendList'])
            for schedule in planning:
                utils.book_event(events, schedule, start, end)

        if not legacy:
            events = {
                x['id']: x  # dictionary is used de remove dupplicated events
                for e in events.values()
                for x in utils.decompose_event(e)
            }
        return {'data': [s[1] for s in sorted(events.items())]}

    @endpoint(
        display_category=_('Family'),
        perm='can_access',
        display_order=10,
        description=_('Subscribe'),
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
            'activityID': {'description': _('Activity ID')},
            'unitID': {'description': _('Unit ID')},
            'placeID': {'description': _('Place ID')},
            'weeklyPlanning': {'description': _('Week planning (7 chars)')},
            'start_date': {'description': _('Start date of the unit (YYYY-MM-DD)')},
            'end_date': {'description': _('End date of the unit (YYYY-MM-DD)')},
        },
    )
    def subscribe(
        self, request, NameID, childID, activityID, unitID, placeID, weeklyPlanning, start_date, end_date
    ):
        self.get_child_info(NameID, childID)
        client = self.get_client('FamilyService?wsdl')
        trigram_type = client.get_type('ns1:activityUnitPlaceBean')
        trigram = trigram_type(idActivity=activityID, idUnit=unitID, idPlace=placeID)
        subscription_type = client.get_type('ns1:subscribeActivityRequestBean')
        subpscription = subscription_type(
            personNumber=childID,
            activityUnitPlace=trigram,
            weeklyPlanning=weeklyPlanning,
            dateStart=start_date,
            dateEnd=end_date,
        )
        r = self.call('FamilyService?wsdl', 'subscribeActivity', subscribeActivityRequestBean=subpscription)
        return {'data': serialize_object(r)}

    @endpoint(
        display_category=_('Family'),
        perm='can_access',
        display_order=11,
        description=_('Unsubscribe'),
        parameters={
            'NameID': {'description': _('Publik ID')},
            'childID': {'description': _('Child ID')},
            'activityID': {'description': _('Activity ID')},
            'start_date': {'description': _('Start date of the unit (YYYY-MM-DD)')},
        },
    )
    def unsubscribe(self, request, NameID, childID, activityID, start_date):
        self.get_child_info(NameID, childID)
        r = self.call(
            'FamilyService?wsdl',
            'deletesubscribe',
            numPerson=childID,
            idActivite=activityID,
            dateRefDelete=start_date,
        )
        return {'data': serialize_object(r)}


class Link(models.Model):
    resource = models.ForeignKey(Maelis, on_delete=models.CASCADE)
    name_id = models.CharField(blank=False, max_length=256)
    family_id = models.CharField(blank=False, max_length=128)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ('resource', 'name_id')
