# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 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 datetime
from collections import OrderedDict
from urllib import parse as urlparse

from django.core.cache import cache
from django.db import models
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource, HTTPResource
from passerelle.utils.api import endpoint
from passerelle.utils.conversion import simplify
from passerelle.utils.jsonresponse import APIError

API_VERSION = [
    ('1.0.0', '1.0.0'),
    ('2.1.0', '2.1.0'),
]
API_VERSION_DEFAULT = '1.0.0'

SITE_BOOKING_SCHOOL_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "ENS site/booking/school",
    "description": "",
    "type": "object",
    "required": [
        "site",
        "date",
        "pmr",
        "morning",
        "lunch",
        "afternoon",
        "participants",
        "grade_levels",
        "beneficiary_first_name",
        "beneficiary_last_name",
        "beneficiary_email",
        "beneficiary_phone",
    ],
    "properties": OrderedDict(
        {
            "external_id": {
                "description": "external id",
                "type": "string",
            },
            "site": {
                "description": "site id (code)",
                "type": "string",
            },
            "project": {
                "description": "project code",
                "type": "string",
            },
            "date": {
                "description": "booking date (format: YYYY-MM-DD)",
                "type": "string",
            },
            "pmr": {
                "description": "PMR",
                "type": "boolean",
            },
            "morning": {
                "description": "morning booking",
                "type": "boolean",
            },
            "lunch": {
                "description": "lunch booking",
                "type": "boolean",
            },
            "afternoon": {
                "description": "afternoon booking",
                "type": "boolean",
            },
            "participants": {
                "description": "number of participants",
                "type": "string",
                "pattern": "^[0-9]+$",
            },
            "animator": {
                "description": "animator id",
                "type": "string",
                "pattern": "^[0-9]*$",
            },
            "group": {
                "description": "school group id (API v2.1.0, use applicant if empty)",
                "type": "string",
                "pattern": "^[0-9]*$",
            },
            "applicant": {
                "description": "applicant",
                "type": "string",
            },
            "grade_levels": {
                "description": "grade levels",
                "type": "array",
                "items": {
                    "type": "string",
                    "description": "level",
                },
            },
            "beneficiary_first_name": {
                "description": "beneficiary first name",
                "type": "string",
            },
            "beneficiary_last_name": {
                "description": "beneficiary last name",
                "type": "string",
            },
            "beneficiary_email": {
                "description": "beneficiary email",
                "type": "string",
            },
            "beneficiary_phone": {
                "description": "beneficiary phone number",
                "type": "string",
            },
            "beneficiary_cellphone": {
                "description": "beneficiary cell phone number",
                "type": "string",
            },
            # v1.0.0 only
            "code": {
                "description": "booking code (API v1.0.0)",
                "type": "string",
            },
            "status": {
                "description": "booking status (API v1.0.0)",
                "type": "string",
            },
            "beneficiary_id": {
                "description": "beneficiary id (API v1.0.0)",
                "type": "string",
            },
            "public": {
                "description": "public (API v1.0.0)",
                "type": "string",
            },
            "entity_id": {
                "description": "entity/school id (UAI/RNE) (API v1.0.0)",
                "type": "string",
            },
            "entity_name": {
                "description": "entity/school name (API v1.0.0)",
                "type": "string",
            },
            "entity_type": {
                "description": "entity/school type (API v1.0.0)",
                "type": "string",
            },
        }
    ),
}


class IsereENS(BaseResource, HTTPResource):
    category = _("Business Process Connectors")

    base_url = models.URLField(
        verbose_name=_("Webservice Base URL"),
        help_text=_("Base API URL (before /api/...)"),
    )
    token = models.CharField(verbose_name=_("Access token"), max_length=128)
    api_version = models.CharField(
        verbose_name=_("API version"), max_length=10, choices=API_VERSION, default=API_VERSION_DEFAULT
    )

    class Meta:
        verbose_name = _("Espaces naturels sensibles de l'Isère")

    def request(self, endpoint, params=None, json=None, method='get'):
        url = urlparse.urljoin(self.base_url, endpoint)
        headers = {"token": self.token}
        if method == 'post' or json is not None:
            response = self.requests.post(url, params=params, json=json, headers=headers)
        else:
            response = self.requests.get(url, params=params, headers=headers)

        if response.status_code // 100 != 2:
            try:
                json_content = response.json()
            except ValueError:
                json_content = None
            raise APIError(
                "error status:%s %r, content:%r"
                % (response.status_code, response.reason, response.content[:1024]),
                data={
                    "status_code": response.status_code,
                    "json_content": json_content,
                },
            )

        if response.status_code == 204:  # 204 No Content
            raise APIError("abnormal empty response")

        try:
            return response.json()
        except ValueError:
            raise APIError("invalid JSON in response: %r" % response.content[:1024])

    @endpoint(
        name="sites",
        description=_("Sites"),
        display_order=1,
        perm="can_access",
        parameters={
            "q": {"description": _("Search text in name field")},
            "id": {
                "description": _("Returns site with code=id"),
            },
            "kind": {
                "description": _("Returns only sites of this kind (school_group or social)"),
            },
        },
    )
    def sites(self, request, q=None, id=None, kind=None):
        if id is not None:
            site = self.request("api/%s/site/%s" % (self.api_version, id))
            site["id"] = site["code"]
            site["text"] = "%(name)s (%(city)s)" % site
            sites = [site]
        else:
            cache_key = "isere-ens-sites-%d" % self.id
            sites = cache.get(cache_key)
            if not sites:
                sites = self.request("api/%s/site" % self.api_version)
                for site in sites:
                    site["id"] = site["code"]
                    site["text"] = "%(name)s (%(city)s)" % site
                cache.set(cache_key, sites, 300)
        if kind is not None:
            sites = [site for site in sites if site.get(kind)]
        if q is not None:
            q = simplify(q)
            sites = [site for site in sites if q in simplify(site["text"])]
        return {"data": sites}

    @endpoint(
        name="animators",
        description=_("Animators"),
        display_order=2,
        perm="can_access",
        parameters={
            "q": {"description": _("Search text in name field")},
            "id": {
                "description": _("Returns animator number id"),
            },
        },
    )
    def animators(self, request, q=None, id=None):
        cache_key = "isere-ens-animators-%d" % self.id
        animators = cache.get(cache_key)
        if not animators:
            animators = self.request("api/%s/schoolAnimator" % self.api_version)
            for animator in animators:
                animator["id"] = str(animator["id"])
                animator["text"] = "%(first_name)s %(last_name)s <%(email)s> (%(entity)s)" % animator
            cache.set(cache_key, animators, 300)
        if id is not None:
            animators = [animator for animator in animators if animator["id"] == id]
        if q is not None:
            q = simplify(q)
            animators = [animator for animator in animators if q in simplify(animator["text"])]
        return {"data": animators}

    @endpoint(
        name="site-calendar",
        description=_("Available bookings for a site"),
        display_order=3,
        perm="can_access",
        parameters={
            "site": {"description": _("Site code (aka id)")},
            "participants": {
                "description": _("Number of participants"),
            },
            "start_date": {
                "description": _("First date of the calendar (format: YYYY-MM-DD, default: today)"),
            },
            "end_date": {
                "description": _(
                    "Last date of the calendar (format: YYYY-MM-DD, default: start_date + 92 days)"
                ),
            },
        },
    )
    def site_calendar(self, request, site, participants="1", start_date=None, end_date=None):
        if start_date:
            try:
                start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
            except ValueError:
                raise APIError(
                    "bad start_date format (%s), should be YYYY-MM-DD" % start_date,
                    http_status=400,
                )
        else:
            start_date = datetime.date.today()
        if end_date:
            try:
                end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
            except ValueError:
                raise APIError(
                    "bad end_date format (%s), should be YYYY-MM-DD" % end_date,
                    http_status=400,
                )
        else:
            end_date = start_date + datetime.timedelta(days=92)

        params = {
            "participants": participants,
            "start_date": start_date.strftime("%Y-%m-%d"),
            "end_date": end_date.strftime("%Y-%m-%d"),
        }
        dates = self.request("api/%s/site/%s/calendar" % (self.api_version, site), params=params)

        def status_name(status):
            return {
                "AVAILABLE": _("available"),
                "COMPLETE": _("complete"),
                "OVERBOOKING": _("overbooking"),
                "OPEN": _("open"),
                "CLOSE": _("closed"),
            }.get(status) or _("unknown")

        for date in dates:
            date["id"] = site + ":" + date["date"]
            date["site"] = site
            date_ = datetime.datetime.strptime(date["date"], "%Y-%m-%d").date()
            date["date_format"] = date_format(date_, format="DATE_FORMAT")
            date["date_number"] = date_format(date_, format="d")
            date["date_weekday"] = date_format(date_, format="l")
            date["date_weekdayindex"] = date_format(date_, format="w")
            date["date_weeknumber"] = date_format(date_, format="W")
            date["date_month"] = date_format(date_, format="F Y")
            date["disabled"] = False
            date["status"] = "open"
            for period in ("morning", "lunch", "afternoon"):
                date["%s_status" % period] = status_name(date[period])
            for period in ("morning", "afternoon"):
                if date[period] in ("COMPLETE", "CLOSE"):
                    if date["status"] == "partially-open":
                        date["disabled"] = True
                        date["status"] = "closed"
                    else:
                        date["status"] = "partially-open"
            date["details"] = (
                _("Morning (%(morning_status)s), Lunch (%(lunch_status)s), Afternoon (%(afternoon_status)s)")
                % date
            )
            date["text"] = "%(date_format)s - %(details)s" % date
        return {"data": dates}

    def site_booking_v1(self, request, post_data):
        for key in (
            'code',
            'status',
            'beneficiary_id',
            'entity_id',
            'entity_name',
            'entity_type',
            'project',
            'applicant',
            'public',
        ):
            if key not in post_data:
                raise APIError('%s is mandatory (API v1.0.0)' % key, err_code='bad-request', http_status=400)
        payload = {
            "code": post_data["code"],
            "status": post_data["status"],
            "beneficiary": {
                "id": post_data["beneficiary_id"],
                "firstName": post_data["beneficiary_first_name"],
                "lastName": post_data["beneficiary_last_name"],
                "email": post_data["beneficiary_email"],
                "phone": post_data["beneficiary_phone"],
                "cellphone": post_data.get("beneficiary_cellphone", ""),
            },
            "entity": {
                "id": post_data["entity_id"],
                "name": post_data["entity_name"],
                "type": post_data["entity_type"],
            },
            "booking": {
                "projectCode": post_data.get("project"),
                "siteCode": post_data["site"],
                "applicant": post_data["applicant"],
                "public": post_data["public"],
                "bookingDate": post_data["date"],
                "participants": int(post_data["participants"]),
                "morning": post_data["morning"],
                "lunch": post_data["lunch"],
                "afternoon": post_data["afternoon"],
                "pmr": post_data["pmr"],
                "gradeLevels": post_data["grade_levels"],
            },
        }
        if post_data.get("animator"):
            payload["booking"]["schoolAnimator"] = int(post_data["animator"])

        booking = self.request("api/1.0.0/booking", json=payload)
        if not isinstance(booking, dict):
            raise APIError("response is not a dict", data=booking)
        if "status" not in booking:
            raise APIError("no status in response", data=booking)
        if booking["status"] not in ("BOOKING", "OVERBOOKING"):
            raise APIError("booking status is %s" % booking["status"], data=booking)
        return {"data": booking}

    @endpoint(
        name="site-booking",
        description=_("Book a site for a school"),
        display_order=4,
        perm="can_access",
        methods=["post"],
        post={
            "request_body": {
                "schema": {
                    "application/json": SITE_BOOKING_SCHOOL_SCHEMA,
                }
            }
        },
    )
    def site_booking(self, request, post_data):
        if self.api_version == '1.0.0':
            return self.site_booking_v1(request, post_data)
        payload = {
            "siteCode": post_data["site"],
            "bookingDate": post_data["date"],
            "pmr": post_data["pmr"],
            "morning": post_data["morning"],
            "lunch": post_data["lunch"],
            "afternoon": post_data["afternoon"],
            "participants": int(post_data["participants"]),
            "gradeLevels": post_data["grade_levels"],
            "beneficiary": {
                "firstName": post_data["beneficiary_first_name"],
                "lastName": post_data["beneficiary_last_name"],
                "email": post_data["beneficiary_email"],
                "phone": post_data["beneficiary_phone"],
                "cellphone": post_data.get("beneficiary_cellphone", ""),
            },
        }
        if post_data.get('group'):
            payload['schoolGroup'] = int(post_data['group'])
        elif post_data.get('applicant'):
            payload['schoolGroup'] = None
            payload['applicant'] = post_data['applicant']
        else:
            raise APIError(
                'group or applicant are mandatory (API v2.1.0)',
                err_code='bad-request',
                http_status=400,
            )
        if post_data.get('animator'):
            payload['schoolAnimator'] = int(post_data['animator'])
        if 'external_id' in post_data:
            payload['idExternal'] = post_data['external_id']
        if 'project' in post_data:
            payload['projectCode'] = post_data['project']

        booking = self.request('api/' + self.api_version + '/site/booking/school', json=payload)
        if not isinstance(booking, dict):
            raise APIError("response is not a dict", data=booking)
        if "status" not in booking:
            raise APIError("no status in response", data=booking)
        if booking["status"] not in ("BOOKING", "OVERBOOKING"):
            raise APIError("booking status is %s" % booking["status"], data=booking)
        return {"data": booking}

    @endpoint(
        name="get-site-booking",
        description=_("Booking status"),
        display_order=5,
        perm="can_access",
        parameters={
            "code": {"description": _('Booking Code (API v1.0.0) or External ID (API v2.1.0)')},
        },
    )
    def get_site_booking(self, request, code):
        if self.api_version == '1.0.0':
            status = self.request('api/1.0.0/booking/' + code + '/status')
        else:
            status = self.request('api/' + self.api_version + '/site/booking/school/' + code + '/status/')
        if not isinstance(status, dict):
            raise APIError('response is not a dict', data=status)
        if 'status' not in status:
            raise APIError('no status in response', data=status)
        return {'data': status}

    @endpoint(
        name="cancel-site-booking",
        description=_("Cancel a booking"),
        methods=["post"],
        display_order=6,
        perm="can_access",
        parameters={
            "code": {"description": _('External ID')},
        },
    )
    def cancel_booking(self, request, code):
        if self.api_version == '1.0.0':
            raise APIError('not available on API v1.0.0', data=None)
        status = self.request(
            'api/' + self.api_version + '/site/booking/school/cancel/' + code, method='post'
        )
        if not isinstance(status, dict):
            raise APIError('response is not a dict', data=status)
        if 'status' not in status:
            raise APIError('no status in response', data=status)
        return {'data': status}
