# -*- coding: utf-8 -*-

import copy
import functools
from rest_framework import status as http_status
import json
import logging
import os

from flask import request, make_response
from mako.lookup import TemplateLookup
from mako.template import Template
import markupsafe
from werkzeug.exceptions import NotFound
import werkzeug.wrappers

from framework import sentry
from framework.exceptions import HTTPError
from framework.flask import app, redirect
from framework.sessions import session

from website import settings

logger = logging.getLogger(__name__)

TEMPLATE_DIR = settings.TEMPLATES_PATH

_TPL_LOOKUP = TemplateLookup(
    default_filters=[
        'unicode',  # default filter; must set explicitly when overriding
    ],
    directories=[
        TEMPLATE_DIR,
        settings.ADDON_PATH,
    ],
    module_directory='/tmp/mako_modules'
)

_TPL_LOOKUP_SAFE = TemplateLookup(
    default_filters=[
        'unicode',  # default filter; must set explicitly when overriding
        'temp_ampersand_fixer',  # FIXME: Temporary workaround for data stored in wrong format in DB. Unescape it before it gets re-escaped by Markupsafe. See [#OSF-4432]
        'h',
    ],
    imports=[
        'from website.util.sanitize import temp_ampersand_fixer',  # FIXME: Temporary workaround for data stored in wrong format in DB. Unescape it before it gets re-escaped by Markupsafe. See [#OSF-4432]
    ],
    directories=[
        TEMPLATE_DIR,
        settings.ADDON_PATH,
    ],
    module_directory='/tmp/mako_modules',
)

REDIRECT_CODES = [
    http_status.HTTP_301_MOVED_PERMANENTLY,
    http_status.HTTP_302_FOUND,
]

class Rule(object):
    """ Container for routing and rendering rules."""

    @staticmethod
    def _ensure_list(value):
        if not isinstance(value, list):
            return [value]
        return value

    def __init__(self, routes, methods, view_func_or_data, renderer,
                 view_kwargs=None, endpoint_suffix=''):
        """Rule constructor.

        :param routes: Route or list of routes
        :param methods: HTTP method or list of methods
        :param view_func_or_data: View function or data; pass data
            if view returns a constant data dictionary
        :param renderer: Renderer object or function
        :param view_kwargs: Optional kwargs to pass to view function
        :param endpoint_suffix: Optional suffix to append to endpoint name;
            useful for disambiguating routes by HTTP verb

        """
        if not callable(renderer):
            raise ValueError('Argument renderer must be callable.')
        self.routes = self._ensure_list(routes)
        self.methods = self._ensure_list(methods)
        self.view_func_or_data = view_func_or_data
        self.renderer = renderer
        self.view_kwargs = view_kwargs or {}
        self.endpoint_suffix = endpoint_suffix


def wrap_with_renderer(fn, renderer, renderer_kwargs=None, debug_mode=True):
    """

    :param fn: View function; must return a dictionary or a tuple containing
        (up to) a dictionary, status code, headers, and redirect URL
    :param renderer: Renderer object or function
    :param renderer_kwargs: Optional kwargs to pass to renderer
    :return: Wrapped view function

    """
    @functools.wraps(fn)
    def wrapped(*args, **kwargs):
        if session:
            session_error_code = session.data.get('auth_error_code')
        else:
            session_error_code = None
        if session_error_code:
            return renderer(
                HTTPError(session_error_code),
                **renderer_kwargs or {}
            )
        try:
            if renderer_kwargs:
                kwargs.update(renderer_kwargs)
            data = fn(*args, **kwargs)
        except HTTPError as error:
            data = error
        except Exception as error:
            logger.exception(error)
            if settings.SENTRY_DSN and not app.debug:
                sentry.log_exception()
            if debug_mode:
                raise
            data = HTTPError(
                http_status.HTTP_500_INTERNAL_SERVER_ERROR,
                message=repr(error),
            )
        return renderer(data, **renderer_kwargs or {})
    return wrapped


def data_to_lambda(data):
    """Create a lambda function that takes arbitrary arguments and returns
    a deep copy of the passed data. This function must deep copy the data,
    else other code operating on the returned data can change the return value
    of the lambda.

    """
    return lambda *args, **kwargs: copy.deepcopy(data)


view_functions = {}

def process_rules(app, rules, prefix=''):
    """Add URL routes to Flask / Werkzeug lookup table.

    :param app: Flask / Werkzeug app
    :param rules: List of Rule objects
    :param prefix: Optional prefix for rule URLs

    """
    for rule in rules:

        # Handle view function
        if callable(rule.view_func_or_data):

            view_func = rule.view_func_or_data
            renderer_name = getattr(
                rule.renderer,
                '__name__',
                rule.renderer.__class__.__name__
            )
            endpoint = '{}__{}'.format(
                renderer_name,
                rule.view_func_or_data.__name__
            )
            view_functions[endpoint] = rule.view_func_or_data

        # Handle view data: wrap in lambda and build endpoint from
        # HTTP methods
        else:

            view_func = data_to_lambda(rule.view_func_or_data)
            endpoint = '__'.join(
                route.replace('/', '') for route in rule.routes
            )

        # Wrap view function with renderer
        wrapped_view_func = wrap_with_renderer(
            view_func,
            rule.renderer,
            rule.view_kwargs,
            debug_mode=app.debug
        )

        # Add routes
        for url in rule.routes:
            try:
                app.add_url_rule(
                    prefix + url,
                    endpoint=endpoint + rule.endpoint_suffix,
                    view_func=wrapped_view_func,
                    methods=rule.methods,
                )
            except AssertionError:
                raise AssertionError('URLRule({}, {})\'s view function name is overwriting an existing endpoint'.format(prefix + url, view_func.__name__ + rule.endpoint_suffix))


### Renderer helpers ###

def render_mustache_string(tpl_string, data):
    import pystache
    return pystache.render(tpl_string, context=data)

def render_jinja_string(tpl, data):
    pass

mako_cache = {}
def render_mako_string(tpldir, tplname, data, trust=True):
    """Render a mako template to a string.

    :param tpldir:
    :param tplname:
    :param data:
    :param trust: Optional. If ``False``, markup-save escaping will be enabled
    """

    show_errors = settings.DEBUG_MODE  # thanks to abought
    # TODO: The "trust" flag is expected to be temporary, and should be removed
    #       once all templates manually set it to False.

    lookup_obj = _TPL_LOOKUP_SAFE if trust is False else _TPL_LOOKUP

    tpl = mako_cache.get(tplname)
    if tpl is None:
        with open(os.path.join(tpldir, tplname)) as f:
            tpl_text = f.read()
        tpl = Template(
            tpl_text,
            format_exceptions=show_errors,
            lookup=lookup_obj,
            input_encoding='utf-8',
            output_encoding='utf-8',
            default_filters=lookup_obj.template_args['default_filters'],
            imports=lookup_obj.template_args['imports']  # FIXME: Temporary workaround for data stored in wrong format in DB. Unescape it before it gets re-escaped by Markupsafe. See [#OSF-4432]
        )
    # Don't cache in debug mode
    if not app.debug: