from django.core.exceptions import ImproperlyConfigured
from elasticsearch import NotFoundError

from modelsearch.backends.elasticsearch7 import (
    Elasticsearch7AutocompleteQueryCompiler,
    Elasticsearch7Index,
    Elasticsearch7Mapping,
    Elasticsearch7SearchBackend,
    Elasticsearch7SearchQueryCompiler,
    Elasticsearch7SearchResults,
)
from modelsearch.index import class_is_indexed


class Elasticsearch8Mapping(Elasticsearch7Mapping):
    pass


class Elasticsearch8Index(Elasticsearch7Index):
    def put(self):
        self.es.indices.create(index=self.name, **self.backend.settings)

    def delete(self):
        try:
            self.es.indices.delete(index=self.name)
        except NotFoundError:
            pass

    def refresh(self):
        self.es.indices.refresh(index=self.name)

    def add_model(self, model):
        # Get mapping
        mapping = self.mapping_class(model)

        # Put mapping
        self.es.indices.put_mapping(index=self.name, **mapping.get_mapping())

    def add_item(self, item):
        # Make sure the object can be indexed
        if not class_is_indexed(item.__class__):
            return

        # Get mapping
        mapping = self.mapping_class(item.__class__)

        # Add document to index
        self.es.index(
            index=self.name,
            document=mapping.get_document(item),
            id=mapping.get_document_id(item),
        )


class Elasticsearch8SearchQueryCompiler(Elasticsearch7SearchQueryCompiler):
    mapping_class = Elasticsearch8Mapping


class Elasticsearch8SearchResults(Elasticsearch7SearchResults):
    def _backend_do_search(self, body, **kwargs):
        # As of Elasticsearch 7.15, the 'body' parameter is deprecated; instead, the top-level
        # keys of the body dict are now kwargs in their own right
        return self.backend.es.search(**body, **kwargs)


class Elasticsearch8AutocompleteQueryCompiler(Elasticsearch7AutocompleteQueryCompiler):
    mapping_class = Elasticsearch8Mapping


class Elasticsearch8SearchBackend(Elasticsearch7SearchBackend):
    mapping_class = Elasticsearch8Mapping
    index_class = Elasticsearch8Index
    query_compiler_class = Elasticsearch8SearchQueryCompiler
    autocomplete_query_compiler_class = Elasticsearch8AutocompleteQueryCompiler
    results_class = Elasticsearch8SearchResults
    timeout_kwarg_name = "request_timeout"

    def _get_host_config_from_url(self, url):
        """Given a parsed URL, return the host configuration to be added to self.hosts"""
        use_ssl = url.scheme == "https"
        port = url.port or (443 if use_ssl else 80)

        # the verify_certs and http_auth options are no longer valid in Elasticsearch 8
        return {
            "host": url.hostname,
            "port": port,
            "path_prefix": url.path,
            "scheme": url.scheme,
        }

    def _get_options_from_host_urls(self, urls):
        """Given a list of parsed URLs, return a dict of additional options to be passed into the
        Elasticsearch constructor; necessary for options that aren't valid as part of the 'hosts' config"""
        opts = super()._get_options_from_host_urls(urls)

        basic_auth = (urls[0].username, urls[0].password)
        # Ensure that all urls have the same credentials
        if any((url.username, url.password) != basic_auth for url in urls):
            raise ImproperlyConfigured(
                "Elasticsearch host configuration is invalid. "
                "Elasticsearch 8 does not support multiple hosts with differing authentication credentials."
            )

        if basic_auth != (None, None):
            opts["basic_auth"] = basic_auth

        return opts


SearchBackend = Elasticsearch8SearchBackend
