# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2017  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
import json
import re

from django.db import models
from django.utils.dateparse import parse_date, parse_datetime
from django.utils.timezone import get_fixed_timezone, is_naive, make_aware, utc
from django.utils.translation import gettext_lazy as _

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

# Only for documentation
REFERENTIALS = (
    'service',
    'typology',
    'inputchannel',
    'structure',
    'quartierelu',
    'secteurterritoriale',
    'civility',
    'title',
)


# GDEMA date format is /Date(1510786800000+0100)/ (tz is optionnal)
gdema_datetime_re = re.compile(r'/Date\((?P<timestamp_ms>\d+)(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?\)/$')


def parse_gdema_datetime(value):
    match = gdema_datetime_re.match(value)
    if match:
        kw = match.groupdict()
        timestamp = int(kw['timestamp_ms']) / 1000.0
        tzinfo = kw.get('tzinfo')
        if tzinfo == 'Z':
            tzinfo = utc
        elif tzinfo is not None:
            offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
            offset = 60 * int(tzinfo[1:3]) + offset_mins
            if tzinfo[0] == '-':
                offset = -offset
            tzinfo = get_fixed_timezone(offset)
        return datetime.datetime.fromtimestamp(timestamp, tzinfo)
    return None


def normalize(value):
    '''convert /Date()/ to datetime, integers to strings'''
    if isinstance(value, str):
        datetime = parse_gdema_datetime(value)
        if datetime:
            return datetime
        else:
            return value
    if isinstance(value, bool):
        return value
    if isinstance(value, int):
        return '%d' % value
    if isinstance(value, list):
        return [normalize(item) for item in value]
    if isinstance(value, dict):
        return {k: normalize(v) for k, v in value.items()}
    return value


def gdema_datetime(value):
    if not value:
        return None
    dt = parse_datetime(value)
    if not dt:
        dt = parse_date(value)
        if not dt:
            return None
        dt = datetime.datetime(dt.year, dt.month, dt.day)
    if is_naive(dt):
        dt = make_aware(dt)
    timestamp_ms = (dt - datetime.datetime(1970, 1, 1, tzinfo=utc)).total_seconds() * 1000
    tzinfo = dt.strftime('%z')
    return '/Date(%d%s)/' % (timestamp_ms, tzinfo)


def to_gdema(input_dict):
    """
    nameDate: ... -> nameDate: /Date(...)/
    name: {publik file dict} -> name: {gdema file dict}
    Name_Key: value -> Name: {Key: value, ...}
    Name_<int>: value -> Name: [value, ...]
    """
    gdema_dict = {}
    for key, value in input_dict.items():
        # nameDate: ... -> nameDate: /Date(...)/
        if key.endswith('Date'):
            value = gdema_datetime(value)
        # name: {publik file dict} -> name: {gdema file dict}
        if isinstance(value, dict) and ('filename' in value and 'content' in value):
            value = {
                'Name': value['filename'],
                'Base64Stream': value['content'],
            }
        # Name_Key: value -> Name: {Key: value, ...}
        # Name_1: value -> Name: [value, ...]  (first element index is 1)
        if '_' in key:
            key, index = key.split('_')
            try:
                index = int(index)
            except ValueError:
                pass
            if isinstance(index, int):
                # update/create a list
                if key not in gdema_dict:
                    gdema_dict[key] = []
                if len(gdema_dict[key]) >= index:
                    gdema_dict[key][index - 1] = value
                else:
                    holes = [None for i in range(index - len(gdema_dict[key]) - 1)]
                    gdema_dict[key].extend(holes)
                    gdema_dict[key].append(value)
            else:
                # index is not a integer, update/create a dict
                if key in gdema_dict:
                    gdema_dict[key][index] = value
                else:
                    gdema_dict[key] = {index: value}
        else:
            gdema_dict[key] = value
    return gdema_dict


class Gdema(BaseResource):
    service_url = models.URLField(
        max_length=256, blank=False, verbose_name=_('Service URL'), help_text=_('GDEMA API base URL')
    )
    username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'))
    password = models.CharField(max_length=128, blank=True, verbose_name=_('Password'))

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('GDEMA (Strasbourg requests and interventions system)')

    def request(self, endpoint, payload=None):
        url = self.service_url + endpoint
        if self.username or self.password:
            auth = (self.username, self.password)
        else:
            auth = None
        headers = {}
        headers['Accept'] = 'application/json'
        if payload is None:
            self.log_requests_errors = False
            result = self.requests.get(url, auth=auth, headers=headers)
        else:
            self.log_requests_errors = True
            headers['Content-Type'] = 'application/json'
            data = json.dumps(payload)
            result = self.requests.post(url, data=data, auth=auth, headers=headers)
        if result.status_code < 200 or result.status_code >= 300:
            raise APIError('GDEMA returns HTTP status %s' % result.status_code)
        return result.json()

    def get_services(self):
        services = self.request('referentiel/service')
        for service in services:
            service['id'] = '%s' % service['Id']
            service['text'] = service['Label']
            del service['Id']
            del service['Label']
            del service['Typology']
        return services

    def get_typologies(self, service_id=None):
        services = self.request('referentiel/service')
        typologies = []
        for service in services:
            if not service_id or service['Id'] == service_id:
                for typology in service['Typology']:
                    typologies.append(
                        {
                            'id': '%s' % typology['Value'],
                            'text': typology['Text'],
                            'service_id': '%s' % service['Id'],
                            'service_text': service['Label'],
                        }
                    )
                if service_id:
                    break
        return typologies

    def check_status(self):
        self.get_services()

    @endpoint(
        name='referentiel',
        pattern=r'^(?P<name>\w+)/*$',
        description=_('Get reference items'),
        example_pattern='{name}/',
        parameters={
            'name': {
                'description': _('Referential name: (%s)') % ' | '.join(REFERENTIALS),
                'example_value': 'inputchannel',
            },
            'service_id': {
                'description': _('Filter by service id (for typology referential)'),
                'example_value': '21714',
            },
        },
        methods=['get'],
        perm='can_access',
    )
    def referentiel(self, request, name, service_id=None):
        if name == 'service':
            return {'data': self.get_services()}
        if name == 'typology':
            return {'data': self.get_typologies(service_id)}
        data = []
        items = self.request('referentiel/%s' % name)
        for item in items:
            data.append(
                {
                    'id': '%s' % item['Value'],
                    'text': item['Text'],
                }
            )
        return {'data': data}

    @endpoint(
        name='create-request',
        description=_('Create a new request (POST)'),
        methods=['post'],
        perm='can_access',
    )
    def create_request(self, request):
        try:
            payload = json.loads(request.body)
        except ValueError:
            raise APIError('payload must be a JSON object', http_status=400)
        if not isinstance(payload, dict):
            raise APIError('payload must be a dict', http_status=400)
        payload = to_gdema(payload)
        data = self.request('request/create', payload)
        return {'data': normalize(data)}

    @endpoint(
        name='get-request',
        pattern=r'^(?P<request_id>\d+)/*$',
        description=_('Get request details'),
        example_pattern='{request_id}/',
        parameters={
            'request_id': {
                'description': _('Request Id'),
                'example_value': '10',
            },
        },
        methods=['get'],
        perm='can_access',
    )
    def get_request(self, request, request_id):
        data = self.request('request/%s' % request_id)
        return {'data': normalize(data)}

    @endpoint(
        name='get-request-state',
        pattern=r'^(?P<request_id>\d+)/*$',
        description=_('Get request status'),
        example_pattern='{request_id}/',
        parameters={
            'request_id': {
                'description': _('Request Id'),
                'example_value': '10',
            },
        },
        methods=['get'],
        perm='can_access',
    )
    def get_request_state(self, request, request_id):
        data = self.request('request/%s/state' % request_id)
        return {'data': normalize(data)}
