# 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 json

from django.db import models
from django.http import HttpResponse
from django.utils.http import urlencode
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


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

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('Okina')

    def request(self, endpoint, payload=None, result_is_json=True, result_is_list=True):
        url = self.service_url + endpoint
        auth = (self.username, self.password)
        headers = {}
        if result_is_json:
            headers['Accept'] = 'application/json'
        if payload is None:
            response = self.requests.get(url, auth=auth, headers=headers)
        else:
            headers['Content-Type'] = 'application/json'
            data = json.dumps(payload)
            response = self.requests.post(url, data=data, auth=auth, headers=headers)
        if not result_is_json:
            if response.status_code // 100 != 2:
                try:
                    data = response.json()
                except ValueError:
                    data = {'message': 'HTTP request failed'}
                raise APIError('%s (HTTP error %s)' % (data.get('message'), response.status_code), data=data)
            return response
        try:
            data = response.json()
        except ValueError:
            raise APIError('bad JSON response: %r' % response.content)
        if response.status_code in (401, 403):
            raise APIError('%s (%s)' % (data.get('message'), response.status_code), data=data)
        if result_is_list and not isinstance(data, list):
            raise APIError('response is not a list', data=data)
        if response.status_code // 100 != 2:
            data = {
                'status_code': response.status_code,
                'payload': data,
            }
            raise APIError('HTTP request failed, error %s' % response.status_code, data=data)
        return data

    @endpoint()
    def cities(self, request):
        okina_cities = self.request('cities')
        cities = []
        for city in okina_cities:
            if 'name' in city:
                city['text'] = '%(name)s (%(zipCode)s)' % city
            else:  # old API
                city = city['cityObject']
                city['text'] = '%(nameCity)s (%(zipCode)s)' % city
            city['id'] = '%s' % city['id']
            city['lat'] = city.get('latitude')
            city['lon'] = city.get('longitude')
            cities.append(city)
        cities.sort(key=lambda x: x['text'])
        return {'data': cities}

    @endpoint()
    def classes(self, request):
        return {
            'data': [{'id': '%s' % item['id'], 'text': item['label']} for item in self.request('classes')]
        }

    def get_institutions(self, query=''):
        okina_institutions = self.request('institutions' + query)
        institutions = []
        for institution in okina_institutions:
            institution.update(institution['schoolEstablishment'])
            del institution['schoolEstablishment']
            institution['id'] = '%s' % institution['id']
            institution['text'] = '%(uaiLabel)s' % institution
            institution['lat'] = institution.get('customLocationY') or institution.get('locationY')
            institution['lon'] = institution.get('customLocationX') or institution.get('locationX')
            institutions.append(institution)
        institutions.sort(key=lambda x: x['text'])
        return institutions

    @endpoint(
        parameters={
            'insee': {'description': _('INSEE City code'), 'example_value': '36005'},
        }
    )
    def institutions(self, request, insee=None):
        if insee:
            query = '?' + urlencode({'inseeCode': insee})
        else:
            query = ''
        return {'data': self.get_institutions(query)}

    @endpoint(name='institutions', pattern=r'^from-city/(?P<city_insee_code>\d+)/*$')
    def institutions_from_city(self, request, city_insee_code):
        return {'data': self.get_institutions('/subscriberCity/%s' % city_insee_code)}

    @endpoint(
        name='search',
        perm='can_access',
        description=_('Get stop points based on a starting position and an arrival institution (API 2020)'),
        parameters={
            'lat': {'description': _('Latitude (departure)'), 'example_value': '46.828652'},
            'lon': {'description': _('Longitude (departure)'), 'example_value': '1.701463'},
            'address': {'description': _('Address (departure)')},
            'institution': {'description': _('Institution ID (arrival)'), 'example_value': '277'},
            'mode': {'description': _('Search mode: CLOSE_SCHOLAR (default) = 3km, FAR_ALL = 15km')},
        },
    )
    def search(self, request, lat, lon, institution, address='', mode='CLOSE_SCHOLAR'):
        payload = {
            'from-lat': lat.replace(',', '.'),
            'from-long': lon.replace(',', '.'),
            'from-address': address,
            'institution-id': institution,
            'type': mode,
        }
        stops = self.request('wishes/search', payload)
        for stop in stops:
            stop['id'] = '%s' % stop['id']
            stop['text'] = stop['commercial_name']
            stop['lat'] = stop.get('latitude')
            stop['lon'] = stop.get('longitude')
        return {'data': stops}

    @endpoint(
        name='stop-areas',
        pattern=r'^from-city/(?P<city_insee_code>\d+)/to-institution/(?P<institution_id>\d+)/*$',
    )
    def stop_areas(self, request, city_insee_code, institution_id):
        stops = self.request(
            'stop-areas/subscriberCity/%s/institution/%s' % (city_insee_code, institution_id)
        )
        for stop in stops:
            stop['id'] = '%s' % stop['id']
            stop['text'] = stop['commercial_name']
        return {'data': stops}

    def get_ods(self, endpoint=''):
        # ods = origin/destinations
        okina_ods = self.request('ods' + endpoint)
        ods = []
        for okina_od in okina_ods:
            base_id = 'inst:%(institutionId)s-seq:%(sequenceNumber)s' % okina_od['id']
            for journey in okina_od['originDestinationSequenceElements']:
                identifier = journey['okinaVehicleJourney']['publishedJourneyIdentifier']
                if journey['okinaVehicleJourney']['okinaJourneyPattern']:
                    text = journey['okinaVehicleJourney']['okinaJourneyPattern']['publishedName']
                else:
                    text = identifier
                ods.append(
                    {
                        'id': '%s-%s-%s' % (base_id, journey['id'], journey['okinaVehicleJourney']['id']),
                        'text': text,
                        'vehicle_journey_id': '%s' % journey['okinaVehicleJourney']['id'],
                        'object_id': journey['okinaVehicleJourney']['objectId'],
                        'identifier': identifier,
                    }
                )
        return {'data': ods}

    @endpoint(name='origin-destinations')
    def origin_destinations(self, request):
        return self.get_ods()

    @endpoint(name='origin-destinations', pattern=r'^to-institution/(?P<institution_id>\d+)/*$')
    def origin_destinations_to_institution(self, request, institution_id):
        return self.get_ods('/institution/%s' % institution_id)

    @endpoint(
        name='origin-destinations',
        pattern=r'^from-stop-area/(?P<stop_area_id>\d+)/to-institution/(?P<institution_id>\d+)/*$',
    )
    def origin_destinations_from_stop_to_institution(self, request, stop_area_id, institution_id):
        endpoint = 'ods/institution/%s/stop-area/%s' % (institution_id, stop_area_id)
        okina_journeys = self.request(endpoint)
        journeys = []
        for n, okina_journey in enumerate(okina_journeys, 1):
            journey = {
                'id': str(n),
                'text': ' + '.join(line['name'] for line in okina_journey),
                'lines': [
                    {
                        'id': str(line['id']),
                        'text': line['name'],
                    }
                    for line in okina_journey
                ],
            }
            journeys.append(journey)
        return {'data': journeys}

    @endpoint(
        name='origin-destinations',
        pattern=r'^from-city/(?P<city_insee_code>\d+)/to-institution/(?P<institution_id>\d+)/*$',
    )
    def origin_destinations_from_city_to_institution(self, request, city_insee_code, institution_id):
        return self.get_ods('/institution/%s/subscriberCity/%s' % (institution_id, city_insee_code))

    @endpoint(name='origin-destinations', pattern=r'^from-city/(?P<city_insee_code>\d+)/*$')
    def origin_destinations_from_city(self, request, city_insee_code):
        return self.get_ods('/subscriberCity/%s' % city_insee_code)

    @endpoint(name='topology', pattern='^(?P<kind>(lines|networks|vehicle-journeys))/*$')
    def topology(self, request, kind):
        return {
            'data': [
                {'id': '%s' % item['id'], 'text': item['name']} for item in self.request('topology/%s' % kind)
            ]
        }

    @endpoint(name='subscriber', methods=['post'], perm='can_access')
    def create_subscriber(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)
        return {'data': self.request('subscribers', payload, result_is_list=False)}

    @endpoint(name='subscriber', pattern=r'^(?P<subscriber_id>\d+)/*$', methods=['get'], perm='can_access')
    def get_subscriber(self, request, subscriber_id):
        return {'data': self.request('subscribers/%s' % subscriber_id, result_is_list=False)}

    @endpoint(name='subscriber', pattern=r'^(?P<subscriber_id>\d+)/qrcode/*$', perm='can_access')
    def get_subscriber_qrcode(self, request, subscriber_id):
        qrcode = self.request('subscribers/%s/qrcode' % subscriber_id, result_is_json=False)
        content_type = qrcode.headers.get('Content-Type')
        if not (content_type and content_type.startswith('image/')):
            response = json.loads(qrcode.content)
            raise APIError(response['message'], http_status=response['status'], err=response['code'])
        return HttpResponse(qrcode.content, content_type=content_type)

    @endpoint(name='subscription', methods=['post'], perm='can_access')
    def create_subscription(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)
        if 'lineIds' in payload:
            lineids = payload.pop('lineIds')
            lineids = [lid.strip() for lid in lineids.split(',')]
            subscriptions = []
            for lineid in lineids:
                payload['lineId'] = lineid
                subscriptions.append(self.request('subscriptions', payload, result_is_list=False))
            return {'data': subscriptions}
        return {'data': self.request('subscriptions', payload, result_is_list=False)}

    @endpoint(
        name='subscription', pattern=r'^(?P<subscription_id>\d+)/*$', methods=['get'], perm='can_access'
    )
    def get_subscription(self, request, subscription_id):
        return {'data': self.request('subscriptions/%s' % subscription_id, result_is_list=False)}
