import json
import time
import attr
from attr.validators import instance_of as io, optional
from typing import *

import couchbase_core._libcouchbase as LCB
from couchbase_core import mk_formstr
from couchbase.options import OptionBlock, OptionBlockTimeOut, forward_args, timedelta
from couchbase.management.admin import METHMAP
from couchbase.management.generic import GenericManager

from couchbase.exceptions import (ErrorMapper, HTTPException, QueryIndexAlreadyExistsException,
                                  WatchQueryIndexTimeoutException, QueryIndexNotFoundException,
                                  InvalidArgumentException)

try:
    from typing import Protocol
except BaseException:
    from typing_extensions import Protocol


class QueryErrorMapper(ErrorMapper):
    @staticmethod
    def mapping():
        # type: (...) -> Dict[CBErrorType,Dict[Any, CBErrorType]]
        return {HTTPException: {".*[iI]ndex.*already exists.*": QueryIndexAlreadyExistsException,
                                ".*[iI]ndex.*[nN]ot [fF]ound.*": QueryIndexNotFoundException}}


def is_null_or_empty(
    value  # type: str
) -> bool:
    return not (value and value.split())


@QueryErrorMapper.wrap
class QueryIndexManager(GenericManager):
    def __init__(self, parent_cluster):
        """
        Query Index Manager
        The Query Index Manager interface contains the means for managing indexes used for queries.
        :param parent_cluster: Parent cluster
        """
        super(QueryIndexManager, self).__init__(parent_cluster)

    def _http_request(self, **kwargs):
        # the kwargs can override the defaults
        imeth = None
        method = kwargs.get('method', 'GET')
        if not method in METHMAP:
            raise InvalidArgumentException("Unknown HTTP Method", method)

        imeth = METHMAP[method]
        return self._admin_bucket._http_request(
            type=LCB.LCB_HTTP_TYPE_QUERY,
            path=kwargs['path'],
            method=imeth,
            content_type=kwargs.get('content_type', 'application/json'),
            post_data=kwargs.get('content', None),
            response_format=LCB.FMT_JSON,
            timeout=kwargs.get('timeout', None))

    def _validate_scope_and_collection(self,  # type: "QueryIndexManager"
                                       scope=None,  # type: str
                                       collection=None  # type: str
                                       ) -> bool:
        if not (scope and scope.split()) and (collection and collection.split()):
            raise InvalidArgumentException(
                "Both scope and collection must be set.  Invalid scope.")
        if (scope and scope.split()) and not (collection and collection.split()):
            raise InvalidArgumentException(
                "Both scope and collection must be set.  Invalid collection.")

    def _build_keyspace(self,  # type: "QueryIndexManager"
                        bucket,  # type: str
                        scope=None,  # type: str
                        collection=None  # type: str
                        ) -> str:

        # None AND empty check done in validation, only check for None
        if scope and collection:
            return "`{}`.`{}`.`{}`".format(bucket, scope, collection)

        if scope:
            return "`{}`.`{}`".format(bucket, scope)

        return "`{}`".format(bucket)

    def _create_index(self, bucket_name, fields,
                      index_name=None, **kwargs):

        scope_name = kwargs.get("scope_name", None)
        collection_name = kwargs.get("collection_name", None)
        self._validate_scope_and_collection(scope_name, collection_name)

        primary = kwargs.get("primary", False)
        condition = kwargs.get("condition", None)

        if primary and fields:
            raise TypeError('Cannot create primary index with explicit fields')
        elif not primary and not fields:
            raise ValueError('Fields required for non-primary index')

        if condition and primary:
            raise ValueError('cannot specify condition for primary index')

        query_str = ""

        if not fields:
            query_str += "CREATE PRIMARY INDEX"
        else:
            query_str += "CREATE INDEX"

        if index_name and index_name.split():
            query_str += " `{}` ".format(index_name)

        query_str += " ON {} ".format(self._build_keyspace(
            bucket_name, scope_name, collection_name))

        if fields:
            field_names = ["`{}`".format(f) for f in fields]
            query_str += "({})".format(", ".join(field_names))

        if condition:
            query_str += " WHERE {}".format(condition)

        options = {}
        deferred = kwargs.get("deferred", False)
        if deferred:
            options["defer_build"] = deferred

        num_replicas = kwargs.get("num_replicas", None)
        if num_replicas:
            options["num_replica"] = num_replicas

        if options:
            query_str += " WITH {{{}}}".format(
                ", ".join(["'{0}':{1}".format(k, v) for k, v in options.items()]))

        def possibly_raise(error):
            if isinstance(error, list) and "msg" in error[0] and "already exists" in error[0]["msg"]:
                if not kwargs.get('ignore_if_exists', False):
                    raise

        try:
            resp = self._http_request(
                path="",
                method="POST",
                content=mk_formstr({"statement": query_str}),
                content_type='application/x-www-form-urlencoded',
                **kwargs
            ).value
            if "errors" in resp and possibly_raise(resp["errors"]):
                msg = resp["errors"][0].get("msg", "Index already exists")
                raise QueryIndexAlreadyExistsException.pyexc(
                    msg, resp["errors"])
        except HTTPException as h:
            error = getattr(
                getattr(
                    h,
                    'objextra',
                    None),
                'value',
                {}).get(
                'errors',
                "")
            if possibly_raise(error):
                raise

    def _drop_index(self, bucket_name, index_name=None, **kwargs):

        scope_name = kwargs.get("scope_name", None)
        collection_name = kwargs.get("collection_name", None)
        self._validate_scope_and_collection(scope_name, collection_name)

        # previous ignore_missing was a viable kwarg - should only have ignore_if_not_exists
        ignore_missing = kwargs.pop("ignore_missing", None)
        if ignore_missing:
            kwargs["ignore_if_not_exists"] = ignore_missing

        query_str = ""
        keyspace = self._build_keyspace(
            bucket_name, scope_name, collection_name)
        if not index_name:
            query_str += "DROP PRIMARY INDEX ON {}".format(keyspace)
        else:
            if scope_name and collection_name:
                query_str += "DROP INDEX `{0}` ON {1}".format(
                    index_name, keyspace)
            else:
                query_str += "DROP INDEX {0}.`{1}`".format(
                    keyspace, index_name)

        def possibly_raise(error):
            if isinstance(error, list) and "msg" in error[0] and "not found" in error[0]["msg"]:
                if not kwargs.get('ignore_if_not_exists', False):
                    return True
        try:
            resp = self._http_request(