# 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 base64
from collections import OrderedDict
from urllib import parse as urlparse

from django.core.cache import cache
from django.db import models
from django.utils.translation import gettext_lazy as _
from requests.exceptions import ConnectionError

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

DEMAND_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-04/schema#',
    'title': 'Intervention demand',
    'description': '',
    'type': 'object',
    'required': ['company', 'service', 'subject', 'name'],
    'properties': OrderedDict(
        {
            'company': {
                'description': 'Company code (sgesdemSoc)',
                'type': 'string',
            },
            'service': {
                'description': 'Service code (sgesdemSserv)',
                'type': 'string',
            },
            'label': {
                'description': 'Defined label code, required if LIBELDEMDEF=O (sgesdemLibdef)',
                'type': 'string',
            },
            'subject': {
                'description': 'Subject. Use defined label text if LIBELDEMDEF=O (sgesdemNdt)',
                'type': 'string',
            },
            'description': {
                'description': 'Description, up to 4000 chars (sgesdemLibelle)',
                'type': 'string',
            },
            'name': {
                'description': 'Applicant first name and last name (sgesdemCplnom)',
                'type': 'string',
            },
            'email': {
                'description': 'Email address (sgesdemCplemail)',
                'type': 'string',
            },
            'phone1': {
                'description': 'Phone 1 (sgesdemCpltel1)',
                'type': 'string',
            },
            'phone2': {
                'description': 'Phone 2 (sgesdemCpltel2)',
                'type': 'string',
            },
            'address1': {
                'description': 'Address, first line (sgesdemCpladr1)',
                'type': 'string',
            },
            'address2': {
                'description': 'Address, second line (sgesdemCpladr2)',
                'type': 'string',
            },
            'address3': {
                'description': 'Address, third line (sgesdemCpladr2)',
                'type': 'string',
            },
        }
    ),
}

ADD_DOCUMENT_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-04/schema#',
    'title': 'Intervention demand',
    'description': '',
    'type': 'object',
    'required': ['demand_id', 'document', 'title'],
    'properties': OrderedDict(
        {
            'demand_id': {
                'description': 'Document title (docTitre)',
                'type': 'string',
            },
            'title': {
                'description': 'Document title (docTitre)',
                'type': 'string',
            },
            'filename': {
                'description': 'Document filename (docFile)',
                'type': 'string',
            },
            'document': {
                'type': 'object',
                'description': 'File object (file0)',
                'required': ['filename', 'content_type', 'content'],
                'properties': {
                    'filename': {
                        'type': 'string',
                    },
                    'content_type': {
                        'type': 'string',
                        'description': 'MIME content-type',
                    },
                    'content': {
                        'type': 'string',
                        'description': 'Content, base64 encoded',
                    },
                },
            },
        }
    ),
}


class ASTech(BaseResource, HTTPResource):
    category = _('Business Process Connectors')

    base_url = models.URLField(
        verbose_name=_('Webservice Base URL'),
        help_text=_('Base API URL (example: https://astech-symphonie.com/app.php/)'),
    )
    connection = models.CharField(
        verbose_name=_('Connection code'),
        help_text=_('See possibilities with "connections" endpoint. If empty, default code is used'),
        max_length=64,
        blank=True,
        null=True,
    )

    _category_ordering = [_('Parameters'), _('Rules'), _('Demand'), 'Tech & Debug']

    class Meta:
        verbose_name = _('AS-TECH')

    def call_json(self, method, url, **kwargs):
        try:
            response = self.requests.request(method, url, timeout=25, **kwargs)
        except ConnectionError as e:
            raise APIError('connection error: %s' % e)
        if response.status_code not in (200, 201):
            try:
                content = response.json()
            except ValueError:
                content = response.content[:1024]
            raise APIError(
                'AS-TECH response: %s %s' % (response.status_code, response.reason),
                data={
                    'error': {
                        'status': response.status_code,
                        'reason': response.reason,
                        'content': content,
                    }
                },
            )
        try:
            json_data = response.json()
        except ValueError:
            raise APIError('invalid JSON in response: %r' % response.content[:1024])
        return json_data

    def get_connections(self):
        url = urlparse.urljoin(self.base_url, 'connection/all')
        connections = self.call_json('get', url)
        if self.connection:
            if self.connection not in connections['connections']:
                raise APIError(
                    'connection %s not found in available connections: %r' % (self.connection, connections)
                )
            connections['default'] = self.connection
        connections['connection'] = connections['connections'][connections['default']]
        return connections

    def get_authorization(self):
        cache_key = 'astech-%s-%s-authorization' % (self.connection or 'default', self.id)
        authorization = cache.get(cache_key)
        if authorization:
            return authorization
        connection = self.get_connections()['connection']
        params = {
            'client_id': connection['clientId'],
            'connection_id': connection['code'],
            'format': 'json',
            'response_type': 'token',
        }
        url = urlparse.urljoin(self.base_url, 'oauth/v2/auth')
        authorization = self.call_json('post', url, params=params, json={'application': 'interfaceCitoyenne'})
        timeout = min(120, authorization['expires_in'])  # do not trust expires_in delay (1 day in tests)
        cache.set(cache_key, authorization, timeout)
        return authorization

    def call(self, endpoint, **kwargs):
        url = urlparse.urljoin(self.base_url, endpoint)
        params = kwargs.pop('params', {})
        json = kwargs.pop('json', None)
        method = kwargs.pop('method', 'get')
        authorization = self.get_authorization()
        params['access_token'] = authorization['access_token']
        params['connection_id'] = authorization['connection_id']
        if json is not None:
            json_response = self.call_json('post', url, params=params, json=json, **kwargs)
        else:
            json_response = self.call_json(method, url, params=params, **kwargs)
        return json_response

    @endpoint(
        name='connections',
        description=_('See all possible connections codes (see configuration)'),
        perm='can_access',
        display_category='Tech & Debug',
        display_order=1,
    )
    def connections(self, request):
        return {'data': self.get_connections()}

    @endpoint(
        name='authorization',
        description=_('See authorization tokens (testing only)'),
        perm='can_access',
        display_category='Tech & Debug',
        display_order=2,
    )
    def authorization(self, request):
        return {'data': self.get_authorization()}

    @endpoint(
        name='services',
        description=_("List authorized services for connected user"),
        perm='can_access',
        display_category=_('Rules'),
        display_order=1,
    )
    def services(self, request):
        services = self.call('apicli/rule-call-by-alias/sousservices/invoke', method='post')
        services = [{'id': key, 'text': value} for key, value in services.items()]
        services.sort(key=lambda item: item['id'])  # "same as output" sort
        return {'data': services}

    @endpoint(
        name='company',
        description=_('Company code of the applicant'),
        perm='can_access',
        parameters={
            'applicant': {
                'description': _(
                    'Applicant code (codeDemandeur). If absent, use HTTP basic authentication username'
                )
            },
        },
        display_category=_('Rules'),
        display_order=2,
    )
    def company(self, request, applicant=None):
        if applicant is None:
            applicant = self.basic_auth_username
        company = self.call(
            'apicli/rule-call-by-alias/code_societe_demandeur/invoke', json={'codeDemandeur': applicant}
        )
        return {'data': {'company': company}}

    @endpoint(
        name='companies',
        description=_('List of authorized companies for an applicant'),
        perm='can_access',
        parameters={
            'applicant': {
                'description': _(
                    'Applicant code (codeDemandeur). If absent, use HTTP basic authentication username'
                )
            },
            'signatory': {'description': _('Signatory code (codeSignataire), supersede applicant')},
            'kind': {
                'description': _('Kind(s) of request: 1=parts, 2=interventions, 3=loans'),
                'example_value': '1,3',
            },
        },
        display_category=_('Rules'),
        display_order=3,
    )
    def companies(self, request, applicant=None, signatory=None, kind='1,2,3'):
        if applicant is None and signatory is None:
            applicant = self.basic_auth_username
        kind = ','.join([item.strip() for item in kind.split(',') if item.strip()])
        payload = {
            'codeDemandeur': applicant or '',
            'codeSignataire': signatory or '',
            'typeDemande': kind,
            'designation': True,
        }
        companies = self.call('apicli/rule-call-by-alias/societes_demandeur/invoke', json=payload)
        companies = [{'id': str(key), 'text': value} for key, value in companies.items()]
        companies.sort(key=lambda item: item['id'])  # "same as output" sort
        return {'data': companies}

    @endpoint(
        name='labels',
        description=_('List of predefined labels for a company'),
        perm='can_access',
        parameters={
            'company': {
                'description': _('Company code (societeDemandeur). If absent, use "company" endpoint result')
            },
        },
        display_category=_('Rules'),
        display_order=4,
    )
    def labels(self, request, company=None):
        if company is None:
            company = self.company(request)['data']['company']
        labels = self.call(
            'apicli/rule-call-by-alias/libelles_predefinis/invoke', json={'societeDemandeur': company}
        )
        labels = [{'id': str(key), 'text': value} for key, value in labels.items()]
        labels.sort(key=lambda item: item['id'])  # "same as output" sort
        return {'data': labels}

    @endpoint(
        name='parameter',
        description=_("Value of a parameter"),
        perm='can_access',
        parameters={
            'name': {'description': _('Name of the parameter'), 'example_value': 'LIBELDEMDEF'},
            'company': {'description': _('Company code. If absent, use "company" endpoint result')},
        },
        display_category=_('Parameters'),
        display_order=5,
    )
    def parameter(self, request, name, company=None):
        if company is None:
            company = self.company(request)['data']['company']
        endpoint = 'apicli/common/getparam/' + name
        if company:
            endpoint += '/' + company
        value = self.call(endpoint)
        return {'data': value}

    @endpoint(
        name='create-demand',
        description=_('Create a demand'),
        perm='can_access',
        methods=['post'],
        post={'request_body': {'schema': {'application/json': DEMAND_SCHEMA}}},
        display_category=_('Demand'),
        display_order=1,
    )
    def create_demand(self, request, post_data):
        payload = {
            'interface_citoyenne_demande': {
                'sgesdemSoc': post_data['company'],
                'sgesdemSserv': post_data['service'],
                'sgesdemLibdef': post_data.get('label') or '',
                'sgesdemNdt': post_data['subject'],
                'sgesdemLibelle': post_data.get('description') or '',
                'sgesdemCplnom': post_data['name'] or '',
                'sgesdemCplemail': post_data.get('email') or '',
                'sgesdemCpltel1': post_data.get('phone1') or '',
                'sgesdemCpltel2': post_data.get('phone2') or '',
                'sgesdemCpladr1': post_data.get('address1') or '',
                'sgesdemCpladr2': post_data.get('address2') or '',
                'sgesdemCpladr3': post_data.get('address3') or '',
            }
        }

        # forward payload parameters matching APIs params name pattern
        for key, value in post_data.items():
            if key.startswith('sgesdem'):
                payload['interface_citoyenne_demande'][key] = value

        create = self.call('apicli/interface-citoyenne/demande-intervention', json=payload)
        if not isinstance(create, dict) or not create.get('sgesdemNum'):
            raise APIError('no sgesdemNum in response: %s' % create)
        create['demand_id'] = create['sgesdemNum']
        return {'data': create}

    @endpoint(
        name='add-document',
        description=_('Add a document in a demand'),
        perm='can_access',
        methods=['post'],
        post={'request_body': {'schema': {'application/json': ADD_DOCUMENT_SCHEMA}}},
        display_category=_('Demand'),
        display_order=2,
    )
    def add_document(self, request, post_data):
        endpoint = 'apicli/interface-citoyenne/document/sgesdemNum/' + post_data['demand_id']
        filename = post_data.get('filename') or post_data['document']['filename']
        params = {
            'docTitre': post_data['title'],
            'docFile': filename,
        }
        content = base64.b64decode(post_data['document']['content'])
        content_type = post_data['document']['content_type']
        files = {'file0': (filename, content, content_type)}
        added = self.call(endpoint, method='post', params=params, files=files)
        return {'data': added}

    @endpoint(
        name='demand-position',
        description=_('Get demand position'),
        perm='can_access',
        parameters={
            'demand_id': {
                'description': _('Demand id'),
                'example_value': '000000000001234',
            },
        },
        display_category=_('Demand'),
        display_order=3,
    )
    def demand_position(self, request, demand_id):
        endpoint = 'apicli/demande/position/' + demand_id
        position = self.call(endpoint)
        if not isinstance(position, dict) or not position.get('position'):
            raise APIError('no position in response: %s' % position)
        position['id'] = position['position']
        position['text'] = position.get('positionLib')
        return {'data': position}

    @endpoint(
        name='demand-all-positions',
        description=_('List all demand possible positions'),
        perm='can_access',
        display_category=_('Demand'),
        display_order=4,
    )
    def demand_all_positions(self, request):
        endpoint = 'apicli/demande/positions'
        positions = self.call(endpoint)
        for position in positions:
            position['id'] = position['position']
            position['text'] = position['positionLib']
        return {'data': positions}
