# Copyright (C) 2022  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
import json
import urllib.parse

import requests
from django.db import models
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

BASE_ENPOINT_PARAMS = {
    'organism': {'description': _('Organisme'), 'example_value': 'NOMDEVILLE'},
    'budget': {'description': _('Budget'), 'example_value': '01'},
    'exercice': {'description': _('Exercice'), 'example_value': '2022'},
}

ENTITY_TYPE_PARAM = {'optionnal': False, 'description': _('Entity type'), 'example_value': 'FACTURE'}

ENTITY_REF_PARAM = {'optionnal': False, 'description': _('Entity ref'), 'example_value': ''}

GF_DOCUMENTS_DOCUMENT_READ_PARAMS = dict(BASE_ENPOINT_PARAMS)
GF_DOCUMENTS_DOCUMENT_READ_PARAMS.update(
    {
        'entity_type': ENTITY_TYPE_PARAM,
        'entity_ref': ENTITY_REF_PARAM,
        'document_ref': {'optionnal': False, 'description': _('Document ref'), 'example_value': ''},
    }
)

GF_DOCUMENTS_ENTITIES_GETFREF_PARAMS = dict(BASE_ENPOINT_PARAMS)
GF_DOCUMENTS_ENTITIES_GETFREF_PARAMS.update(
    {
        'entity_type': ENTITY_TYPE_PARAM,
        'entity_code': {'optionnal': False, 'description': _('Entity type'), 'example_value': 'RMA01'},
    }
)

GF_DOCUMENTS_ENTITIES_READ_PARAMS = dict(BASE_ENPOINT_PARAMS)
GF_DOCUMENTS_ENTITIES_READ_PARAMS.update(
    {
        'entity_type': ENTITY_TYPE_PARAM,
        'entity_ref': ENTITY_REF_PARAM,
    }
)

GF_DOCUMENTS_ENTITIES_SEARCH_PARAMS = dict(BASE_ENPOINT_PARAMS)
GF_DOCUMENTS_ENTITIES_SEARCH_PARAMS.update(
    {
        'entity_type': ENTITY_TYPE_PARAM,
        's': {'optionnal': False, 'description': _('Search criteria'), 'example_value': ''},
    }
)

GF_DOCUMENTS_ENTITIES_LIST_PARAMS = dict(BASE_ENPOINT_PARAMS)
GF_DOCUMENTS_ENTITIES_LIST_PARAMS.update(
    {
        'entity_type': ENTITY_TYPE_PARAM,
        'entity_ref': ENTITY_REF_PARAM,
        'code_list': {'optionnal': False, 'description': _('Code list'), 'example_value': ''},
    }
)

DOCUMENT_CREATE_OR_UPDATE_SCHEMA = {
    'type': 'object',
    'title': _('Document creation or update'),
    'properties': {
        'file': {
            'title': _('File object'),
            'type': 'object',
            'properties': {
                'filename': {
                    'type': 'string',
                    'description': _('Filename'),
                },
                'content': {
                    'type': 'string',
                    'description': _('Content'),
                },
                'content_type': {
                    'type': 'string',
                    'description': _('Content type'),
                },
            },
            'required': ['content'],
        },
        'entity_type': {
            'type': 'string',
        },
        'entity_ref': {
            'type': 'string',
        },
        'astre_ref': {
            'type': 'string',
        },
        'ged_ref': {
            'type': 'string',
        },
        'doc_type': {
            'type': 'string',
        },
        'description': {
            'type': 'string',
        },
        'filename': {
            'type': 'string',
            'description': _('Filename (takes precendence over filename in "file" object)'),
        },
        'indpj': {
            'type': 'string',
        },
        'domain_code': {
            'type': 'string',
        },
    },
    'required': ['entity_type', 'entity_ref', 'doc_type', 'file'],
    'unflatten': False,
}

DOCUMENT_DELETE_SCHEMA = {
    'type': 'object',
    'title': _('Document delete'),
    'properties': {
        'entity_type': {
            'type': 'string',
        },
        'entity_ref': {
            'type': 'string',
        },
        'document_ref': {
            'type': 'string',
        },
    },
    'required': ['entity_type', 'entity_ref', 'document_ref'],
    'unflatten': False,
}

ASTRE_ERRORS = (
    'UndefinedError',
    'VersionError',
    'ConfigError',
    'AuthError',
    'ConfError',
    'DataError',
    'NotFoundError',
    'AstreError',
    'AstreException',
)

AUTH_CHOICES = [
    ('AD', 'AD'),
    ('EXTERNE', 'EXTERNE'),
    ('LDAP', 'LDAP'),
    ('NORMAL', 'NORMAL'),
]


class AstreREST(BaseResource):

    base_url = models.URLField(_('API URL'))
    username = models.CharField(_('Username'), max_length=32)
    password = models.CharField(_('Password'), max_length=32)
    database = models.CharField('Database', max_length=32)
    auth = models.CharField(_('Authentication mode'), max_length=32, choices=AUTH_CHOICES, blank=True)
    organism = models.CharField('Organisme', max_length=32)
    budget = models.CharField('Budget', max_length=32)
    exercice = models.CharField('Exercice', max_length=32)
    verify_cert = models.BooleanField(default=True, verbose_name=_('Check HTTPS Certificate validity'))

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = 'Astre REST'

    def check_status(self):
        self._astre_call('astre/webservices/gf/documents/referentiel/typedocument/list/json')

    @property
    def encrypted_password(self):
        key = 48
        password = ''
        for char in self.password:
            password += chr(ord(char) ^ key)
        return urllib.parse.quote(password, 'iso-8859-1')

    def _astre_call(self, path, method='get', params=None, data=None, files=None, expect_json=True):
        params = params or {}
        url = urllib.parse.urljoin(self.base_url, path)
        authentication_params = {
            'login': self.username,
            'password': self.encrypted_password,
            'database': self.database,
        }
        if self.auth:
            authentication_params['auth'] = self.auth

        kwargs = {}
        if method == 'get':
            kwargs['headers'] = authentication_params

        elif method == 'post':
            if not data:
                data = {}
            data.update(authentication_params)
            kwargs['data'] = data
            if files:
                kwargs['files'] = files

        resp = getattr(self.requests, method)(url, params=params, **kwargs)

        try:
            resp.raise_for_status()
        except requests.RequestException as e:
            content = getattr(getattr(e, 'response', None), 'content', None)
            raise APIError('request failed', url, e, content)
        try:
            json_resp = resp.json()
            if hasattr(json_resp, 'get') and json_resp.get('code') in ASTRE_ERRORS:
                msg = json_resp.get('code')
                if json_resp.get('message'):
                    msg += ': %s' % json_resp.get('message')
                raise APIError(msg)
            return json_resp
        except json.JSONDecodeError:
            if expect_json:
                raise
        return resp.text

    def _get_context_param(self, params, concat=True):
        res = []
        for param_name in ('organism', 'budget', 'exercice'):
            res.append(params[param_name] if param_name in params else getattr(self, param_name))
        if concat:
            return '%s-%s-%s' % tuple(res)
        return res

    def _get_document_create_update_data(self, post_data):
        organism, budget, exercice = self._get_context_param(post_data, concat=False)
        return {
            'codeOrganisme': organism,
            'codeBudget': budget,
            'codeExercice': exercice,
            'typeEntite': post_data['entity_type'],
            'refEntite': post_data['entity_ref'],
            'referenceAstre': post_data.get('astre_ref', ''),
            'referenceGed': post_data.get('ged_ref', ''),
            'typeDoc': post_data['doc_type'],
            'description': post_data.get('description', ''),
            'nomFichier': post_data.get('filename', '') or post_data['file'].get('filename', ''),
            'typeMime': post_data['file'].get('mime_type', ''),
            'indPJ': post_data.get('indpj', ''),
            'codeDomaine': post_data.get('domain_code', ''),
        }

    def _get_data_source(self, data):
        res = []
        for record in data:
            res.append(
                {
                    'id': record['code'],
                    'text': record['libelle'],
                    'code': record['code'],
                    'libelle': record['libelle'],
                }
            )
        return res

    @endpoint(
        methods=['get'],
        perm='can_access',
        name='gf-documents-entites-getref',
        parameters=GF_DOCUMENTS_ENTITIES_GETFREF_PARAMS,
    )
    def gf_documents_entites_getref(self, request, **kwargs):
        contexte = self._get_context_param(kwargs)
        fonctionnal_key = '%s-%s' % (kwargs['entity_type'], kwargs['entity_code'])
        path = 'astre/webservices/gf/documents/entites/getRefEntite/%s/%s' % (contexte, fonctionnal_key)
        return {'data': self._astre_call(path, expect_json=False)}

    @endpoint(
        methods=['get'],
        perm='can_access',
        name='gf-documents-entites-list',
        parameters=GF_DOCUMENTS_ENTITIES_LIST_PARAMS,
    )
    def gf_documents_entites_list(self, request, **kwargs):
        contexte = self._get_context_param(kwargs)
        entity_key = '%s-%s' % (kwargs['entity_type'], kwargs['entity_ref'])
        path = 'astre/webservices/gf/documents/entites/%s/%s/list/%s/json' % (
            contexte,
            entity_key,
            kwargs['code_list'],
        )
        data = []
        for record in self._astre_call(path):
            data.append(
                {
                    'id': record['codeEntite'],
                    'text': record['libelleEntite'],
                    'codeEntite': record['codeEntite'],
                    'libelleEntite': record['libelleEntite'],
                    'typeEntite': record['typeEntite'],
                    'refEntite': record['refEntite'],
                    'codeOrganisme': record['codeOrganisme'],
                }
            )
        return {'data': data}

    @endpoint(
        methods=['get'],
        perm='can_access',
        name='gf-documents-entites-read',
        parameters=GF_DOCUMENTS_ENTITIES_READ_PARAMS,
    )
    def gf_documents_entites_read(self, request, **kwargs):
        contexte = self._get_context_param(kwargs)
        entity_key = '%s-%s' % (kwargs['entity_type'], kwargs['entity_ref'])
        path = 'astre/webservices/gf/documents/entites/read/%s/%s/json' % (contexte, entity_key)
        return {'data': self._astre_call(path)}

    @endpoint(
        methods=['get'],
        perm='can_access',
        name='gf-documents-entites-search',
        parameters=GF_DOCUMENTS_ENTITIES_SEARCH_PARAMS,
    )
    def gf_documents_entites_search(self, request, **kwargs):
        contexte = self._get_context_param(kwargs)
        path = 'astre/webservices/gf/documents/entites/search/%s/%s/json' % (
            contexte,
            kwargs['entity_type'],
        )
        data = []
        for record in self._astre_call(path, params={'s': kwargs['s']}):
            data.append(
                {
                    'id': record['codeEntite'],
                    'text': record['libEntite'],
                    'refEntite': record['refEntite'],
                    'libEntite': record['libEntite'],
                    'typeEntite': record['typeEntite'],
                    'codeEntite': record['codeEntite'],
                    'codeOrganisme': record['codeOrganisme'],
                }
            )

        return {'data': data}

    @endpoint(
        name='gf-documents-gedmanager-document-create',
        description=_('Create document'),
        perm='can_access',
        post={
            'request_body': {
                'schema': {
                    'application/json': DOCUMENT_CREATE_OR_UPDATE_SCHEMA,
                }
            }
        },
    )
    def gf_documents_gedmanager_document_create(self, request, post_data):
        send_data = self._get_document_create_update_data(post_data)
        return {
            'data': self._astre_call(
                'astre/webservices/gf/documents/gedmanager/document/create/json',
                method='post',
                data=send_data,
                files={'fichier': base64.b64decode(post_data['file']['content'])},
            )
        }

    @endpoint(
        name='gf-documents-gedmanager-document-delete',
        description=_('Delete document'),
        perm='can_access',
        post={
            'request_body': {
                'schema': {
                    'application/json': DOCUMENT_DELETE_SCHEMA,
                }
            }
        },
    )
    def gf_documents_gedmanager_document_delete(self, request, post_data):
        organism, budget, exercice = self._get_context_param(post_data, concat=False)
        send_data = {
            'codeOrganisme': organism,
            'codeBudget': budget,
            'codeExercice': exercice,
            'typeEntite': post_data['entity_type'],
            'refEntite': post_data['entity_ref'],
            'referenceGed': post_data['document_ref'],
        }
        return {
            'data': self._astre_call(
                'astre/webservices/gf/documents/gedmanager/document/delete/json',
                method='post',
                data=send_data,
            )
        }

    @endpoint(
        methods=['get'],
        perm='can_access',
        name='gf-documents-gedmanager-document-read',
        parameters=GF_DOCUMENTS_DOCUMENT_READ_PARAMS,
    )
    def gf_documents_gedmanager_document_read(self, request, **kwargs):
        context = self._get_context_param(kwargs)
        entity_key = '%s-%s' % (kwargs['entity_type'], kwargs['entity_ref'])
        path = '/astre/webservices/gf/documents/gedmanager/document/read/%s/%s/%s/json?metadonnees=all' % (
            context,
            entity_key,
            kwargs['document_ref'],
        )
        return {'data': self._astre_call(path)}

    @endpoint(
        name='gf-documents-gedmanager-document-update',
        description=_('Update document'),
        perm='can_access',
        post={
            'request_body': {
                'schema': {
                    'application/json': DOCUMENT_CREATE_OR_UPDATE_SCHEMA,
                }
            }
        },
    )
    def gf_documents_gedmanager_document_update(self, request, post_data):
        send_data = self._get_document_create_update_data(post_data)
        return {
            'data': self._astre_call(
                'astre/webservices/gf/documents/gedmanager/document/update/json',
                method='post',
                data=send_data,
                files={'fichier': base64.b64decode(post_data['file']['content'])},
            )
        }

    @endpoint(methods=['get'], perm='can_access', name='gf-documents-referentiel-domainepj')
    def gf_documents_referentiel_domainepj(self, request):
        return {
            'data': self._get_data_source(
                self._astre_call('astre/webservices/gf/documents/referentiel/domainepj/list/json')
            )
        }

    @endpoint(methods=['get'], perm='can_access', name='gf-documents-referentiel-typedocument')
    def gf_documents_referentiel_typedocument(self, request):
        return {
            'data': self._get_data_source(
                self._astre_call('astre/webservices/gf/documents/referentiel/typedocument/list/json')
            )
        }
