import json

import cloudinary
from cloudinary.api_client.call_api import call_json_api
from cloudinary.utils import (unique, build_distribution_domain, base64url_encode, json_encode, compute_hex_hash,
                              SIGNATURE_SHA256, build_array)


class Search(object):
    ASSETS = 'resources'

    _endpoint = ASSETS

    _KEYS_WITH_UNIQUE_VALUES = {
        'sort_by': lambda x: next(iter(x)),
        'aggregate': lambda agg: agg["type"] if isinstance(agg, dict) and "type" in agg else agg,
        'with_field': None,
        'fields': None,
    }

    _ttl = 300  # Used for search URLs

    """Build and execute a search query."""

    def __init__(self):
        self.query = {}

    def expression(self, value):
        """Specify the search query expression."""
        self.query["expression"] = value
        return self

    def max_results(self, value):
        """Set the max results to return"""
        self.query["max_results"] = value
        return self

    def next_cursor(self, value):
        """Get next page in the query using the ``next_cursor`` value from a previous invocation."""
        self.query["next_cursor"] = value
        return self

    def sort_by(self, field_name, direction=None):
        """Add a field to sort results by. If not provided, direction is ``desc``."""
        if direction is None:
            direction = 'desc'
        self._add("sort_by", {field_name: direction})
        return self

    def aggregate(self, value):
        """Aggregate field."""
        self._add("aggregate", value)
        return self

    def with_field(self, value):
        """Request an additional field in the result set."""
        self._add("with_field", value)
        return self

    def fields(self, value):
        """Request which fields to return in the result set."""
        self._add("fields", value)
        return self

    def ttl(self, ttl):
        """
        Sets the time to live of the search URL.

        :param ttl: The time to live in seconds.
        :return: self
        """
        self._ttl = ttl
        return self

    def to_json(self):
        return json.dumps(self.as_dict())

    def execute(self, **options):
        """Execute the search and return results."""
        options["content_type"] = 'application/json'
        uri = [self._endpoint, 'search']
        return call_json_api('post', uri, self.as_dict(), **options)

    def as_dict(self):
        to_return = {}

        for key, value in self.query.items():
            if key in self._KEYS_WITH_UNIQUE_VALUES:
                value = unique(value, self._KEYS_WITH_UNIQUE_VALUES[key])

            to_return[key] = value

        return to_return

    def to_url(self, ttl=None, next_cursor=None, **options):
        """
        Creates a signed Search URL that can be used on the client side.

        :param ttl: The time to live in seconds.
        :param next_cursor: Starting position.
        :param options: Additional url delivery options.
        :return: The resulting search URL.
        """
        api_secret = options.get("api_secret", cloudinary.config().api_secret or None)
        if not api_secret:
            raise ValueError("Must supply api_secret")

        if ttl is None:
            ttl = self._ttl

        query = self.as_dict()

        _next_cursor = query.pop("next_cursor", None)
        if next_cursor is None:
            next_cursor = _next_cursor

        b64query = base64url_encode(json_encode(query, sort_keys=True))

        prefix = build_distribution_domain(options)

        signature = compute_hex_hash("{ttl}{b64query}{api_secret}".format(
            ttl=ttl,
            b64query=b64query,
            api_secret=api_secret
        ), algorithm=SIGNATURE_SHA256)

        return "{prefix}/search/{signature}/{ttl}/{b64query}{next_cursor}".format(
            prefix=prefix,
            signature=signature,
            ttl=ttl,
            b64query=b64query,
            next_cursor="/{}".format(next_cursor) if next_cursor else "")

    def endpoint(self, endpoint):
        self._endpoint = endpoint
        return self

    def _add(self, name, value):
        if name not in self.query:
            self.query[name] = []
        self.query[name].extend(build_array(value))
        return self
