"""API for working with the query catalog."""

import json
import os
import re
from urllib.parse import quote
from uuid import uuid1

from pyparsing import ParseException
from rdflib.plugins.sparql import prepareQuery
from rdflib.plugins.sparql.parser import parseUpdate

from cmem.cmempy.api import get_json, request
from cmem.cmempy.config import get_cmem_base_uri, get_dp_api_endpoint
from cmem.cmempy.dp.proxy import sparql, update

# read queries are send to the normal query endpoint
QUERY_TYPES_READ = ("SELECT", "ASK", "DESCRIBE", "CONSTRUCT")
# update queries are sent to the update query endpoint
QUERY_TYPES_UPDATE_TRIPLE = ("UPDATE", "DELETE", "INSERT", "CLEAR", "LOAD")
QUERY_TYPES_UPDATE_GRAPHS = ("CREATE", "DROP", "COPY", "MOVE", "ADD")
QUERY_TYPES_UPDATE = QUERY_TYPES_UPDATE_TRIPLE + QUERY_TYPES_UPDATE_GRAPHS
# broken types
QUERY_TYPES_FAULTY = ("FAULTY", "UNKNOWN")
# all types
QUERY_TYPES = QUERY_TYPES_READ + QUERY_TYPES_UPDATE + QUERY_TYPES_FAULTY

UPDATE_CLASS = "https://vocab.eccenca.com/shui/SparqlUpdate"

QUERY_STRING = """
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX shui: <https://vocab.eccenca.com/shui/>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT DISTINCT ?query ?label ?text ?type ?description ?type_class
FROM <{{GRAPH_IRI}}>
WHERE {
  ?query a ?type_class .
  FILTER (?type_class IN (shui:SparqlQuery, shui:SparqlUpdate))
  ?query rdfs:label ?label .
  ?query shui:queryText ?text .
  OPTIONAL {?query shui:queryType ?type . }
  OPTIONAL {?query dct:description ?description .}
}
"""

DEFAULT_NS = "https://ns.eccenca.com/data/queries/"
DEFAULT_GRAPH = "https://ns.eccenca.com/data/queries/"


def get_query_status():
    """Get status information of run and running queries."""
    endpoint = get_dp_api_endpoint() + "/api/admin/currentQueries"
    return get_json(endpoint)


def cancel_query(query_id: str):
    """Cancel a running query."""
    endpoint = f"{get_dp_api_endpoint()}/api/admin/currentQueries/{query_id}"
    return request(endpoint, method="DELETE")


def get_query_editor_for_uri():
    """Get query editor URI pattern for a query URI."""
    return get_cmem_base_uri() + "/query?query={}&graph={}"


def get_query_editor_for_string():
    """Get query editor URI pattern for a query string."""
    return get_cmem_base_uri() + "/query?queryString={}"


class SparqlQuery:
    """A SPARQL query with optional placeholders."""

    # pylint: disable=too-many-arguments, too-many-instance-attributes

    def __init__(
        self,
        text,
        url=None,
        label=None,
        query_type=None,
        description=None,
        origin="unknown",
        placeholder=None,
    ):
        """Initialize a SparqlQuery object."""
        self.text = text
        self.short_url = None  # will be set with self.set_url
        self.url = self.set_url(url)
        self.label = label
        self.query_type = self.set_query_type(query_type, placeholder=placeholder)
        self.description = description
        # can be one of unknown, remote or file (used for get_editor_uri)
        self.origin = origin
        self.placeholder = placeholder

    def set_url(self, url=None):
        """Set (or generate) an URL for the query."""
        if url is not None:
            self.url = url
            self.short_url = url.replace(DEFAULT_NS, ":")
        else:
            uuid = str(uuid1())
            self.url = DEFAULT_NS + uuid
            self.short_url = ":" + uuid
        return self.url

    def __str__(self):
        """Get string representation (URL)."""
        return self.url

    def set_query_type(self, query_type=None, placeholder=None):
        """Set query type."""
        self.query_type = "UNKNOWN"
        if str(query_type).upper() in QUERY_TYPES:
            self.query_type = str(query_type).upper()
        else:
            parsed_type = str(self.get_query_type(placeholder=placeholder)).upper()
            if parsed_type in QUERY_TYPES:
                self.query_type = parsed_type
        return self.query_type

    def get_query_type(self, placeholder=None):
        """Get the type of query when the placeholders are filled.

        Tries to get the type of the query - returns UNKNOWN if not possible,
        UPDATE, when an update query or a more specific READ query type
        """
        if placeholder is None:
            placeholder = {}
        algebra_names = {
            "DescribeQuery": "DESCRIBE",
            "ConstructQuery": "CONSTRUCT",
            "SelectQuery": "SELECT",
            "AskQuery": "ASK",
        }
        # prepare query text to parse
        string = self.text
        try:
            # try to fill with placeholders
            string = self.get_filled_text(placeholder)
        except ValueError:
            pass
        # 1. try parse as SPARQL query first and map the algebra type to our type
        try:
            parsed_query = prepareQuery(string)
            if str(parsed_query.algebra.name) in algebra_names:
                # return the mapped read query type
                return algebra_names[str(parsed_query.algebra.name)]
        except ParseException:
            pass
        # 2. try to parse as UPDATE query
        try:
            parseUpdate(string)
            # return the default UPDATE query type
            return "UPDATE"
        except ParseException:
            pass
        # return the UNKNOWN query type
        return "UNKNOWN"

    def get_default_accept_header(self):
        """Return the default accept header string for the query.

        return value is based on the query type and biased towards
        command line shell environment.
        """
        default_header = {
            "SELECT": "text/csv",
            "ASK": "text/csv",
            "DESCRIBE": "text/turtle",
            "CONSTRUCT": "text/turtle",
        }
        return default_header.get(self.query_type, "*")

    def get_placeholder_keys(self, text=None):
        """Get all placeholder of a query text as a set of keys."""
        if text is None:
            text = self.text
        keys = re.findall(r"{{([a-zA-Z0-9_-]+)}}", text)
        return set(keys)

    def get_filled_text(self, placeholder):
        """Replace placeholders with given values and return text.

        raises an ValueError exception if not all placeholders are filled.
        """
        text = self.text
        for key, value in placeholder.items():
            text = text.replace("{{" + key + "}}", value)
        if self.get_placeholder_keys(text):
            raise ValueError(
                "Not all placeholders filled for executing the "
                "following query:\n"
                f"- Label: {self.label!s}\n"
                f"- ID: {self.url!s}\n"
                f"- Missing parameters: {self.get_placeholder_keys(text)}"
            )
        return text

    def get_csv_results(self, placeholder=None, owl_imports_resolution=True):
        """Get results as CSV text."""
        if self.query_type != "SELECT":
            raise ValueError("Wrong query type. CSV result only supported for SELECT queries.")
        results = self.get_results(
            placeholder=placeholder,
            owl_imports_resolution=owl_imports_resolution,
            accept="text/csv",
        )
        return results

    def get_json_results(self, placeholder=None, owl_imports_resolution=True):
        """Get results as parsed json object."""
        results = self.get_results(
            placeholder=placeholder,
            owl_imports_resolution=owl_imports_resolution,
            accept="application/sparql-results+json",
        )
        return json.loads(results)

    def get_results(
        self,
        placeholder=None,
        owl_imports_resolution=True,
        base64_encoded=False,
        accept="application/sparql-results+json",
        distinct=False,
        limit=None,
        offset=None,
        timeout=None,
        replace_placeholder=True,
    ):
        """Get results as raw output from cmem.cmempy.dp.proxy.sparql."""
        if placeholder is None:
            placeholder = {}
        if self.query_type is None:
            # try to do last minute determination of type
            self.query_type = self.get_query_type(placeholder)
        if replace_placeholder:
            query_text = self.get_filled_text(placeholder)
        else:
            query_text = self.text
        if self.query_type in QUERY_TYPES_UPDATE:
            return update.post(
                query=query_text,
                accept=accept,
                base64_encoded=base64_encoded,
                timeout=timeout,
            )
        return sparql.post(
            query=query_text,
            accept=accept,
            base64_encoded=base64_encoded,
            owl_imports_resolution=owl_imports_resolution,
            distinct=distinct,
            limit=limit,
            offset=offset,
            timeout=timeout,
        )

    def get_editor_url(self, graph: str = DEFAULT_GRAPH):
        """Get query editor URI for the query."""
        if self.origin == "remote":
            return get_query_editor_for_uri().format(self.url, graph)
        if self.origin == "file":
            return get_query_editor_for_string().format(quote(self.text, safe=""))
        raise ValueError("Unknown origin, can not provide editor URL: " + self.origin)


class QueryCatalog:
    """A representation of the query catalog."""

    def __init__(self, graph: str = DEFAULT_GRAPH):
        """Initialize the catalog."""
        self.queries = None
        self.graph = graph

    def get_query(self, identifier, placeholder=None):
        """Get a query by giving an url, a short_url or a file name."""
        if os.path.isfile(identifier):
            # do not fetch the catalog in case a file is requested
            with open(identifier, encoding="UTF-8") as file_handle:
                return SparqlQuery(
                    file_handle.read(),
                    label="query from file " + identifier,
                    origin="file",
                    placeholder=placeholder,
                )
        queries = self.get_queries()
        if DEFAULT_NS + identifier[1:] in queries:
            # if the identifier comes from shell (:uuid) then we remove : and add
            # the default namespace
            return queries[DEFAULT_NS + identifier[1:]]
        if identifier in queries:
            # maybe the correct full URI is given
            return queries[identifier]
        return None

    def get_queries(self):
        """Get the query catalog as a dictionary of SparqlQuery objects."""
        if self.queries is not None:
            return self.queries
        queries = {}
        results = SparqlQuery(QUERY_STRING, query_type="SELECT").get_json_results(
            placeholder={"GRAPH_IRI": self.graph}
        )
        for query in results["results"]["bindings"]:
            description = query.get("description", {})
            description = description.get("value", None)
            query_type = query.get("type", {}).get("value", "SELECT")
            if query["type_class"]["value"] == UPDATE_CLASS:
                query_type = "UPDATE"

            queries[query["query"]["value"]] = SparqlQuery(
                query["text"]["value"],
                url=query["query"]["value"],
                label=query["label"]["value"],
                query_type=query_type,
                description=description,
                origin="remote",
            )
        self.queries = queries
        return self.queries


QUERY_CATALOG = QueryCatalog()
