from datetime import datetime
import json
import re

import falcon
import six


class DateTimeEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return o.isoformat()
        return json.JSONEncoder.default(self, o)


class Middleware(object):
    def __init__(self, help_messages=True):
        """help_messages: display validation/error messages"""
        self.debug = bool(help_messages)

    def bad_request(self, title, description):
        """Shortcut to respond with 400 Bad Request"""
        if self.debug:
            raise falcon.HTTPBadRequest(title, description)
        else:
            raise falcon.HTTPBadRequest()

    def get_json(self, field, **kwargs):
        """Helper to access JSON fields in the request body

        Optional built-in validators.
        """
        value = None
        if field in self.req.json:
            value = self.req.json[field]
            kwargs.pop('default', None)
        elif 'default' not in kwargs:
            self.bad_request("Missing JSON field",
                             "Field '{}' is required".format(field))
        else:
            value = kwargs.pop('default')
        validators = kwargs
        return self.validate(field, value, **validators)

    def validate(self, field, value, dtype=None, default=None, min=None, max=None, match=None, choices=None):
        """JSON field validators:

        dtype      data type
        default    value used if field is not provided in the request body
        min        minimum length (str) or value (int, float)
        max        maximum length (str) or value (int, float)
        match      regular expression
        choices    list to which the value should be limited
        """
        err_title = "Validation error"

        if dtype:
            if dtype == str and type(value) in six.string_types:
                pass

            elif type(value) is not dtype:
                msg = "Data type for '{}' is '{}' but should be '{}'"
                self.bad_request(err_title,
                                 msg.format(field, type(value).__name__,  dtype.__name__))

        if type(value) in six.string_types:
            if min and len(value) < min:
                self.bad_request(err_title,
                                 "Minimum length for '{}' is '{}'".format(field, min))

            if max and len(value) > max:
                self.bad_request(err_title,
                                 "Maximum length for '{}' is '{}'".format(field, max))

        elif type(value) in (int, float):
            if min and value < min:
                self.bad_request(err_title,
                                 "Minimum value for '{}' is '{}'".format(field, min))

            if max and value > max:
                self.bad_request(err_title,
                                 "Maximum value for '{}' is '{}'".format(field, max))

        if match and not re.match(match, re.escape(value)):
            self.bad_request(err_title,
                             "'{}' does not match Regex: {}".format(field, match))

        if choices and value not in choices:
            self.bad_request(err_title,
                             "{} must be one of {}".format(field, choices))
        return value

    def process_request(self, req, resp):
        """Middleware request"""
        if not req.content_length:
            return
        body = req.stream.read()
        req.json = {}
        self.req = req
        req.get_json = self.get_json  # helper function
        try:
            req.json = json.loads(body.decode('utf-8'))
        except UnicodeDecodeError:
            self.bad_request("Invalid encoding", "Could not decode as UTF-8")
        except ValueError:
            self.bad_request("Malformed JSON", "Syntax error")

    def process_response(self, req, resp, resource, req_succeeded):
        """Middleware response"""
        if getattr(resp, "json", None) is not None:
            resp.body = str.encode(json.dumps(resp.json, cls=DateTimeEncoder))