# -*- coding: utf-8 -*-
import json
from flask import request
from flask_restful import abort, Resource
from werkzeug.exceptions import NotFound
from flask import current_app
from ..decorators import require_admin_role
from ..models import ValidationError
from ..validator import Validator


class BaseResource(Resource):
    """
    Common base class for all schema validating resources.
    It is protected by default by the admin role.

    The admin role should be a string defined in the app's
    config['API_AUTH_ADMIN_ROLE'], and defaults to 'admin'.

    Set config['API_AUTH_ADMIN_ROLE'] to None to grant access to any
    registered api user.

    """

    method_decorators = [require_admin_role()]

    def __init__(self, *args, **kwargs):
        super(BaseResource, self).__init__(*args, **kwargs)
        self.validator = Validator(allow_unknown=True)

    def validate_schema(self, data, schema, abort_on_failure=True):
        self.validator.schema = schema
        is_valid = self.validator.validate(data)

        if not is_valid and abort_on_failure:
            abort(422, message=self.validator.errors)

        return is_valid, self.validator.errors

    def parse_request_data(self, valid_types=(dict, list)):
        try:
            request_data = json.loads(request.data)
            if not isinstance(request_data, valid_types):
                raise ValueError()
            return request_data
        except ValueError:
            abort(422, message="The incoming data is not a JSON object")


class ModelResource(BaseResource):
    model = None

    def save_object(self, obj, is_update=False):
        obj.save()

    def get_search_results(self):
        """
        By default return all objects using the search method.
        """
        return self.model.manager.search()


class GenericDetailResource(ModelResource):
    """
    This resource represents the basic details, update and delete operations
    """
    updatable_fields = []

    def get_object(self, pk):
        try:
            return self.model.manager.get(pk)
        except NotFound as e:
            current_app.logger.exception(e)
            abort(404, message='{} with key {} not found'.format(self.model.__name__, pk))

    def get(self, pk):
        return self.get_object(pk).clear_data

    def put(self, pk):
        request_data = self.parse_request_data(dict)
        invalid_keys = [key for key in request_data.keys() if key not in self.updatable_fields]
        if invalid_keys:
            abort(422, message='Invalid keys: {}'.format(invalid_keys))

        obj = self.get_object(pk)

        try:
            obj.update_data(request_data)
        except ValidationError as e:
            return e.message, 422

        self.save_object(obj, is_update=True)
        return obj.clear_data

    # Support PATCH as an alias to PUT
    patch = put

    def delete_object(self, pk):
        self.model.manager.delete([pk])

    def delete(self, pk):
        self.get_object(pk)
        self.delete_object(pk)
        return '', 204


class GenericListResource(ModelResource):

    enable_multipost = False
    post_blacklist_fields = []

    def get(self):
        return [obj.clear_data for obj in self.get_search_results()]

    def sanitize_request_data(self, request_data):
        request_data = dict(request_data)
        for field in self.post_blacklist_fields:
            request_data.pop(field, None)
        return request_data

    def post(self):
        request_data = self.parse_request_data((dict, list) if self.enable_multipost else dict)
        if isinstance(request_data, list):
            return self.multipost(request_data)

        request_data = self.sanitize_request_data(request_data)

        try:
            obj = self.model(request_data)
        except ValidationError as e:
            return e.message, 422

        try:
            self.model.manager.get(obj.key)
        except NotFound:
            self.save_object(obj, False)
            return obj.clear_data, 201
        else:
            msg = '{} with key {} already exists, use the PUT method to update the item'.format(self.model.__name__,
                                                                                                obj.key)
            abort(409, message=msg)

    def multipost(self, object_list):
        response = {
            'success': 0,
            'failed': 0,
            'errors': []
        }
        objects = []

        for object_data in object_list:
            object_data = self.sanitize_request_data(object_data)
            try:
                objects.append(self.model(object_data))
                response['success'] += 1
            except ValidationError as e:
                response['failed'] += 1
                response['errors'].append({
                    'data': object_data,
                    'error': e.message
                })

        self.model.manager.bulk_save(objects)
        return response
