from django.urls import path
from django.http import JsonResponse, HttpResponseNotAllowed
from django.shortcuts import render
from django.conf import settings
from banjo import http
from banjo import views
from banjo.forms import ApiRouteForm
import json

urlpatterns = [
    path(settings.API_PREFIX, views.api, name="api"),
    path(settings.API_PREFIX + ".json", views.api_json, name="api_json"),
]
user_defined_routes = []

class JsonRequiresDict(TypeError):
    pass

def validate_args(args):
    """Checks that args (the param type signature) is valid.
    """
    if args is None: return
    allowed_types = [str, bool, int, float]
    if not isinstance(args, dict):
        raise ValueError("args must be a dict if provided")
    for param, type_ in args.items():
        if type_ not in allowed_types:
            raise ValueError("param {} in args must be one of: {}".format(
                    param, ', '.join(str(t) for t in allowed_types)))

def create_view(fn, method, args=None):
    """Creates a django view function.
    The view function checks that the HTTP method is correct, 
    extracts the request's params, passes them to ``fn``, 
    and returns the result. Args, if provided, should be 
    a dict of ``{param_name: type}``, where type is in 
    ``[str, bool, int, float]``.
    """
    method = method.upper()
    def view(request):
        if request.method != method:
            return HttpResponseNotAllowed([method])  
        if args:
            if request.headers.get('Content-Type') == 'application/json':
                data = json.loads(request.body.decode("utf-8"))
            elif method == 'GET':
                data = request.GET
            else:
                data = request.POST
            FormClass = ApiRouteForm.for_args(args)
            form = FormClass(data)
            if form.is_valid():
                try:
                    result = fn(form.cleaned_data)
                    if not isinstance(result, dict):
                        raise JsonRequiresDict(f"Banjo views must return a dict, not {result}.")
                    return JsonResponse(result)
                except http.BadRequest as e:
                    return JsonResponse({'error': str(e)}, status=e.status_code)
            else:
                return JsonResponse({'errors': form.errors}, status=400)
        else:
            try:
                result = fn({})
                if not isinstance(result, dict):
                    raise JsonRequiresDict(f"Banjo views must return a dict, not {result}.")
                return JsonResponse(result)
            except http.BadRequest as e:
                return JsonResponse({'error': str(e)}, status=e.status_code)

    view.__name__ = fn.__name__
    view.__doc__ = fn.__doc__
    view.method = method
    validate_args(args)
    view.args = args or {}
    return view

def create_api_view(url, fn, method, args=None):
    """Creates an API view.
    An API view responds to GET requests with a HTML template--
    the response data for GET routes and a form for POST routes.
    For POST routes, the view processes the form.
    """
    method = method.upper()
    fn.method = method
    def api_view(request):
        fn.args = args or {}
        route = views.describe_route(path(url, fn, name=fn.__name__))

        if method == "GET" and request.method == "GET":
            if args:
                form = ApiRouteForm.for_args(args)(request.GET)
                if form.is_valid():
                    try:
                        result = fn(form.cleaned_data)
                    except http.BadRequest as e:
                        result = {'error': str(e), 'status_code': e.status_code}
                else:
                    result = None
            else:
                form = None
                try:
                    result = fn({})
                except http.BadRequest as e:
                    result = {'error': str(e), 'status_code': e.status_code}
            return render(request, "banjo/api_get.html", {
                "route": route,
                "result": json.dumps(result, indent=True) if result else None,
                "form": form,
            })
        if method == "POST":
            if request.method == "POST":
                if args:
                    form = ApiRouteForm.for_args(args)(request.POST)
                    if form.is_valid():
                        try:
                            result = fn(form.cleaned_data)
                        except http.BadRequest as e:
                            result = {'error': str(e), 'status_code': e.status_code}
                        form = ApiRouteForm.for_args(args)()
                    else:
                        result = None
                else:
                    form = None
                    try:
                        result = fn({})
                    except http.BadRequest as e:
                        result = {'error': str(e), 'status_code': e.status_code}
            else:
                if args:
                    form = ApiRouteForm.for_args(args)()
                else:
                    form = None
                result = None

            return render(request, "banjo/api_post.html", {
                "route": route,
                "result": json.dumps(result, indent=True) if result else None,
                "form": form,
            })

        return HttpResponseNotAllowed([request.method])

    api_view.__name__ = fn.__name__
    api_view.__doc__ = fn.__doc__
    validate_args(args)
    api_view.args = args or {}
    return api_view
    
def route_get(url, args=None):
    """A decorator which registers a HTTP GET route in the Banjo app.
    """
    def bind_url_to_view(fn):
        view = create_view(fn, "GET", args=args)
        api_view = create_api_view(url, fn, "GET", args=args)
        urlpatterns.append(path(url, view, name=view.__name__))
        urlpatterns.append(path(settings.API_PREFIX + '/' + url, api_view, 
                name="api_" + view.__name__))
        user_defined_routes.append(path(url, view, name=view.__name__))
        return view
    return bind_url_to_view

def route_post(url, args=None):
    """A decorator which registers a HTTP POST route in the Banjo app.
    """
    def bind_url_to_view(fn):
        view = create_view(fn, "POST", args=args)
        api_view = create_api_view(url, fn, "POST", args=args)
        urlpatterns.append(path(url, view, name=view.__name__))
        urlpatterns.append(path(settings.API_PREFIX + '/' + url, api_view, 
                name="api_" + view.__name__))
        user_defined_routes.append(path(url, view, name=view.__name__))
        return view
    return bind_url_to_view
