import json
from functools import partial

from bson import ObjectId
from pymongo import Connection

from helpers import int_or_default, jsonify

from bottle import Bottle, route, run, request, response, abort, error
from bson.json_util import loads as bson_loads

class Kule(object):
    """Wraps bottle app."""

    def __init__(self, database=None, host=None, port=None,
                 collections=None):
        self.connection = self.connect(database, host, port)
        self.collections = collections

    def connect(self, database, host=None, port=None):
        """Connects to the MongoDB"""
        return Connection(host=host, port=port)[database]

    def get_collection(self, collection):
        """Returns the given collection if it permitted"""
        if self.collections and collection not in self.collections:
            abort(403)
        return self.connection[collection]

    def get_detail(self, collection, pk):
        """Returns a single document."""
        cursor = self.get_collection(collection)
        data = cursor.find_one({"_id": ObjectId(pk)}) or abort(404)
        return jsonify(self.get_bundler(cursor)(data))

    def put_detail(self, collection, pk):
        """Updates whole document."""
        collection = self.get_collection(collection)
        if '_id' in request.json:
            # we are ignoring id fields of bundle,
            # because _id field is immutable
            del request.json['_id']
        collection.update({"_id": ObjectId(pk)}, request.json)
        response.status = 202
        return jsonify(request.json)

    def patch_detail(self, collection, pk):
        """Updates specific parts of the document."""
        collection = self.get_collection(collection)
        collection.update({"_id": ObjectId(pk)},
                          {"$set": request.json})
        response.status = 202
        return self.get_detail(collection.name, str(pk))

    def delete_detail(self, collection, pk):
        """Deletes a single document"""
        collection = self.get_collection(collection)
        collection.remove({"_id": ObjectId(pk)})
        response.status = 204

    def post_list(self, collection):
        """Creates new document"""
        collection = self.get_collection(collection)
        inserted = collection.insert(request.json)
        response.status = 201
        return jsonify({"_id": inserted})

    def get_list(self, collection):
        """Returns paginated objects."""
        collection = self.get_collection(collection)
        limit = int_or_default(request.query.limit, 20)
        offset = int_or_default(request.query.offset, 0)
        query = self.get_query()
        fields = self.get_fields()
        cursor = collection.find(query, fields=fields)

        meta = {
            "limit": limit,
            "offset": offset,
            "total_count": cursor.count(),
        }

        objects = cursor.skip(offset).limit(limit)
        objects = map(self.get_bundler(collection), objects)

        return jsonify({"meta": meta,
                        "objects": objects})

    def get_query(self):
        """Loads the given json-encoded query."""
        query = request.GET.get("query")
        return bson_loads(query) if query else {}

    def get_fields(self):
        """Loads the given json-encoded fields."""
        fields = request.GET.get("fields")
        return json.loads(fields) if fields else None

    def get_bundler(self, collection):
        """Returns a bundler function for collection"""
        method_name = "build_%s_bundle" % collection.name
        return getattr(self, method_name, self.build_bundle)

    def build_bundle(self, data):
        """Dummy bundler"""
        return data

    def empty_response(self, *args, **kwargs):
        """Empty response"""

    # we are returning an empty response for OPTIONS method
    # it's required for enabling CORS.
    options_list = empty_response
    options_detail = empty_response

    def get_error_handler(self):
        """Customized errors"""
        return {