import json
import urllib.parse

import requests
from django.db import models
from django.utils.text import slugify
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 exception_to_text
from passerelle.utils.jsonresponse import APIError

MULT_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-04/schema#',
    'title': 'Multi-criterion search',
    'unflatten': True,
    'description': '',
    'type': 'object',
    'required': ['search_name', 'criterions'],
    'properties': {
        'search_name': {
            'description': _('Search name, as specified in the Esabora webservice'),
            'type': 'string',
            'examples': ['WS_ETAT_DOSSIER_SAS'],
        },
        'criterions': {
            'description': _('A mapping of criterions'),
            'type': 'object',
            'examples': [{'SAS_Référence': 'HISTO0001'}],
        },
    },
}

DO_TREATMENT_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-04/schema#',
    'unflatten': True,
    'title': 'Treatment creation',
    'description': 'Additional fields in the payload will be transmitted to the Esabora service.',
    'type': 'object',
    'required': ['treatment_name'],
    'properties': {
        'endpoint': {
            'description': _('Endpoint name, such as modbdd or addevt. Defaults to modbdd'),
            'type': 'string',
            'examples': ['modbdd'],
        },
        'treatment_name': {
            'description': _('Treatment name, as specified in the Esabora service'),
            'type': 'string',
            'examples': ['IMPORT HISTOLOGE'],
        },
    },
}


class Esabora(BaseResource, HTTPResource):

    service_url = models.URLField(
        blank=False,
        verbose_name=_('Service URL'),
        help_text=_('Base Web Service URL, such as https://example.domain/ws/rest/'),
    )

    api_key = models.CharField(max_length=256, default='', blank=True, verbose_name=_('API key'))

    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('Esabora')

    def post(self, path, payload, **kwargs):
        url = urllib.parse.urljoin(self.service_url, path)
        headers = {'Authorization': f'Bearer {self.api_key}'}
        try:
            response = self.requests.post(url, json=payload, headers=headers, timeout=5, **kwargs)
        except requests.RequestException as e:
            raise APIError(
                'Esabora platform "%s" connection error: %s' % (self.service_url, exception_to_text(e)),
                log_error=True,
                data={
                    'code': 'connection-error',
                    'service_url': self.service_url,
                    'error': str(e),
                },
            )
        try:
            data = response.json()
        except json.JSONDecodeError as e:
            raise APIError(
                'Esabora platform "%s" invalid JSON response: %s' % (self.service_url, exception_to_text(e)),
                log_error=True,
                data={
                    'status_code': response.status_code,
                },
            )
        if not response.ok:
            raise APIError(
                'Esabora platform "%s" answered with HTTP error' % (self.service_url),
                log_error=True,
                data={
                    'status_code': response.status_code,
                    'content': data,
                },
            )
        return data

    @endpoint(
        name='do-search',
        description=_('Multi-criterion search'),
        perm='can_access',
        methods=['post'],
        post={'request_body': {'schema': {'application/json': MULT_SCHEMA}}},
        json_schema_response={},
    )
    def do_search(self, request, post_data):
        payload = {
            'searchName': post_data['search_name'],
            'criterionList': [
                {'criterionName': name, 'criterionValueList': [value]}
                for name, value in post_data['criterions'].items()
            ],
        }
        data = self.post('mult/', payload, params={'task': 'doSearch'})
        columns = {slugify(c).replace('-', '_'): c for c in data['columnList']}
        keys = {slugify(c).replace('-', '_'): c for c in data['keyList']}
        cleaned_data = {
            'meta': {
                'nbResults': data['nbResults'],
                'searchId': data['searchId'],
                'columns_name': columns,
                'keys_name': keys,
            },
            'data': [
                esabora_row_to_object(list(columns.keys()), list(keys.keys()), row) for row in data['rowList']
            ],
        }
        return cleaned_data

    @endpoint(
        name='do-treatment',
        description=_('Create a new treatment'),
        perm='can_access',
        methods=['post'],
        post={'request_body': {'schema': {'application/json': DO_TREATMENT_SCHEMA}}},
        json_schema_response={},
    )
    def do_treatment(self, request, post_data):
        endpoint = post_data.pop('endpoint', None) or 'modbdd'
        payload = get_treatment_payload(post_data)

        data = self.post(f'{endpoint}/', payload, params={'task': 'doTreatment'})
        keys = [slugify(c).replace('-', '_') for c in data['keyList']]
        cleaned_data = esabora_row_to_object([], keys, data)
        cleaned_data['action'] = data['action']
        return cleaned_data


def esabora_row_to_object(columns, key_list, row):
    key_data_list = row.get('keyDataList', [])
    all_values = row.get('columnDataList', []) + key_data_list
    obj = dict(zip(columns + key_list, all_values))
    obj['text'] = all_values[0] if all_values else None
    if key_data_list:
        obj['id'] = key_data_list[0]
    return obj


def get_treatment_payload(post_data):
    payload = {'treatmentName': post_data.pop('treatment_name'), 'fieldList': []}
    for key, value in post_data.items():
        field = {
            'fieldName': key,
        }
        if isinstance(value, list):
            populate_document_field(field, value)
        elif isinstance(value, dict):
            populate_document_field(field, [value])
        else:
            field['fieldValue'] = value

        payload['fieldList'].append(field)

    return payload


def populate_document_field(field, value):
    field['fieldDocumentUpdate'] = 1
    field['fieldValue'] = []
    # one or more files were sent,
    # let's compute their size in bytes based on the base64 payload (without = padding)
    # cf https://stackoverflow.com/a/45401395/2844093
    for f in value:
        if not f:
            continue
        size = (len(f['content']) * 3) / 4 - f['content'].count('=', -2)
        field['fieldValue'].append(
            {'documentContent': f['content'], 'documentName': f['filename'], 'documentSize': size}
        )
