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

from django import template
from django.contrib.auth import get_permission_codename
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.template.defaultfilters import stringfilter
from django.utils.html import format_html, mark_safe
from django.utils.translation import gettext as _

from passerelle.utils import get_trusted_services

from ..models import AccessRight, ResourceLog

register = template.Library()


@register.inclusion_tag('passerelle/includes/access-rights-table.html', takes_context=True)
def access_rights_table(context, resource, permission):
    resource_type = ContentType.objects.get_for_model(resource)
    rights = AccessRight.objects.filter(
        resource_type=resource_type, resource_pk=resource.id, codename=permission
    )
    context['permission'] = permission
    context['access_rights_list'] = rights
    context['resource_type'] = resource_type.id
    context['resource_pk'] = resource.id
    context['trusted_services'] = get_trusted_services()
    return context


@register.filter(name='resource_type')
def as_resource_type(resource):
    return ContentType.objects.get_for_model(resource).id


@register.inclusion_tag('passerelle/includes/resource-logs-table.html', takes_context=True)
def resource_logs_table(context, resource):
    request = context.get('request')
    page = request.GET.get('page', 1)

    connector = resource.get_connector_slug()
    context['connector'] = connector
    context['slug'] = resource.slug
    qs = ResourceLog.objects.filter(appname=connector, slug=resource.slug).order_by('-timestamp')

    paginator = Paginator(qs, 10)
    try:
        logrecords = paginator.page(page)
    except PageNotAnInteger:
        logrecords = paginator.page(1)
    except (EmptyPage,):
        logrecords = paginator.page(paginator.num_pages)

    context['logrecords'] = logrecords
    return context


@register.inclusion_tag('passerelle/includes/resource-jobs-table.html', takes_context=True)
def resource_jobs_table(context, resource):
    request = context.get('request')
    page = request.GET.get('page', 1)

    qs = resource.jobs_set().order_by('-creation_timestamp')

    paginator = Paginator(qs, 10)
    try:
        jobs = paginator.page(page)
    except PageNotAnInteger:
        jobs = paginator.page(1)
    except (EmptyPage,):
        jobs = paginator.page(paginator.num_pages)

    context['jobs'] = jobs
    return context


@register.filter
def can_edit(obj, user):
    return user.has_perm(get_permission_codename('change', obj._meta), obj=obj)


@register.filter
def can_delete(obj, user):
    return user.has_perm(get_permission_codename('delete', obj._meta), obj=obj)


@register.filter
@stringfilter
def censor(string):
    return re.sub(r'://([^/]*):([^/]*?)@', r'://\1:***@', string)


def render_json_schema(schema):
    if not isinstance(schema, dict):
        if schema is True:
            return mark_safe('<em>%s</em>') % _('always valid')
        if schema is False:
            return mark_safe('<em>%s</em>') % _('always invalid')
        return format_html('<tt>{!r}</tt>', schema)

    def many_of(name, schemas):
        s = format_html('<b>{}</b>', name)
        parts = [render_json_schema(schema) for schema in schemas]
        if any('\n' in part for part in parts):
            s += '<ul>'
            for part in parts:
                s += format_html('<li>{0}</li>\n', part)
            s += '</ul>'
        else:
            s += ' [ ' + ' | '.join(parts) + ' ]'
        return mark_safe(s)

    def html_type(s):
        return '<span class="type">%s</span>' % s

    if 'anyOf' in schema:
        return many_of('anyOf', schema['anyOf'])

    if 'oneOf' in schema and not schema.get('type') == 'object':
        return many_of('oneOf', schema['oneOf'])

    if 'allOf' in schema:
        return many_of('allOf', schema['allOf'])

    original_schema = schema
    schema = schema.copy()
    schema.pop('$schema', None)
    schema.pop('$id', None)
    title = schema.pop('title', None)
    description = schema.pop('description', None)
    typ = schema.pop('type', None)
    if typ == 'null':
        return mark_safe(html_type('null'))
    if typ == 'string':
        enum = schema.pop('enum', [])
        min_length = schema.pop('minLength', '')
        max_length = schema.pop('maxLength', '')
        pattern = schema.pop('pattern', '')
        pattern_description = schema.pop('pattern_description', '')
        if enum:
            enum = mark_safe(' | '.join([format_html('<tt>{}</tt>', json.dumps(el)) for el in enum]))
        s = 'string'
        if max_length or min_length:
            s += format_html('[{0}:{1}]', min_length, max_length)
        s = html_type(s)
        if enum:
            s += ' %s' % enum
        if pattern_description:
            s += format_html(' <em>{}</em>', pattern_description)
        elif pattern:
            s += format_html(' /<tt>{}</tt>/', pattern)
        if schema:
            s += format_html('\n{!r}', schema)
        return mark_safe(s)
    if typ == 'integer':
        if not schema:
            return mark_safe(html_type('integer'))
    if typ == 'number':
        if not schema:
            return mark_safe(html_type('number'))
    if typ == 'array':
        s = html_type('array') + ' '
        if 'items' in schema:
            s += render_json_schema(schema['items'])
        return mark_safe(s)
    if typ == 'object':
        s = html_type('object')
        unflatten = schema.pop('unflatten', False)
        merge_extra = schema.pop('merge_extra', False)
        properties = schema.pop('properties', {})
        pattern_properties = schema.pop('patternProperties', {})
        required_keys = schema.pop('required', [])
        additional_properties = schema.pop('additionalProperties', True)
        one_of = schema.pop('oneOf', [])
        if unflatten:
            s += format_html(', <em class="unflatten">{}</em>', _('unflatten'))
        if merge_extra:
            s += format_html(', <em class="merge-extra">{}</em>', _('merge extra'))
        if not additional_properties:
            s += format_html(
                ', <em class="additional-properties-false">{}</em>', _('no additional properties')
            )
        if title:
            s += format_html(', <em class="title">{}</em>', title)
        if schema:
            s += format_html('<tt class="raw">{!r}</tt>', schema)
        if description:
            s += format_html('\n<p class="description">{}</p>', description)
        s += ' '

        def render_property_schema(key, html, sub):
            nonlocal s

            required = key in required_keys
            sub_description = sub.pop('description', '')
            sub_title = sub.pop('title', '')
            s += format_html('<li>{0}', html)
            if required:
                s += format_html('<span title="{}" class="required">*</span>', _('required'))
            if description or sub:
                s += ' :'
            if sub_title:
                s += format_html(' <em>{0}</em>', sub_title)
            elif sub_description and '\n' not in sub_description:
                s += format_html(' <em>{0}</em>', sub_description)
            if sub_title or '\n' in sub_description:
                s += format_html('\n<p class="description">{}</p>', sub_description)
            if sub:
                s += format_html('\n{0}', render_json_schema(sub))
            s += '</li>'

        if properties or pattern_properties:
            s += '\n<ul>'

        if properties:
            keys = properties
            if not isinstance(properties, collections.OrderedDict):
                keys = sorted(properties, key=lambda key: key.lower())
            for key in keys:
                sub = properties.get(key, {}).copy()
                render_property_schema(key, format_html('<tt>{0}</tt>', key), sub)
            if one_of and one_of[0].get('required'):
                s += many_of('oneOf', one_of)

        if pattern_properties:
            s += format_html('<li><span>{0}</span>', _('Pattern properties'))
            s += '\n<ul>'
            for key, sub in pattern_properties.items():
                if key:
                    pattern_key = format_html('/<tt>{0}</tt>/', key)
                else:
                    pattern_key = format_html('<em>any</em>')
                render_property_schema(key, pattern_key, sub)
            s += '</ul>'

        if properties or pattern_properties:
            s += '</ul>'

        return mark_safe(s)
    if typ == 'boolean':
        if not schema:
            return mark_safe(html_type('boolean'))
    enum = schema.pop('enum', [])
    if enum and not schema:
        return mark_safe(' | '.join([format_html('<tt>{}</tt>', json.dumps(el)) for el in enum]))
    required_keys = schema.pop('required', [])
    if required_keys and not schema:
        return format_html('<em>{0} {1!r}</em>', _('required'), ', '.join(required_keys))
    return format_html('<em>{0} {1!r}</em>', _('unknown validation'), original_schema)


@register.simple_tag(takes_context=False)
def render_body_schemas(body_schemas):
    if not body_schemas:
        return ''

    s = mark_safe('<ul>')
    for key in body_schemas:
        if key == 'application/json':
            s += mark_safe('<li><tt>application/json</tt> : <span class="json-schema">')
            s += render_json_schema(body_schemas['application/json'])
            s += mark_safe('</span></li>')
        else:
            s += format_html('<li><tt>{0}</tt></li>', key)
    s += mark_safe('<ul>')
    return mark_safe(s)


@register.filter
def json_dumps(d):
    return json.dumps(d, indent=2)
