import json
from logging import getLogger
from pathlib import Path
from typing import Any, BinaryIO, Dict, Optional, Tuple, Union

from surrealist.clients.http_client import HttpClient
from surrealist.connections.connection import Connection, connected
from surrealist.enums import Transport
from surrealist.errors import (CompatibilityError, HttpClientError,
                               HttpConnectionError, SurrealConnectionError,
                               WrongParameterError)
from surrealist.result import SurrealResult, to_result
from surrealist.utils import (AC, DB, DEFAULT_TIMEOUT, ENCODING, ERR, HTTP_OK,
                              NS, OK)

logger = getLogger("surrealist.connections.http")


class HttpConnection(Connection):
    """
    Represents http transport and abilities to work with SurrealDb. It is not a recommended connection, use websocket in 
    any doubt; this connection exists for compatibility reasons. Some features as live query are not possible on this
    transport, but import and export are.

    Refer to surrealist documentation: https://github.com/kotolex/surrealist?tab=readme-ov-file#transports
    Refer to: https://docs.surrealdb.com/docs/integration/http

    Each method call creates a new http short-live connection.

    On creating, this object tries to create a connection with specified data and will raise exception on fail.
    """

    def __init__(self, url: str, db_params: Optional[Dict] = None, credentials: Tuple[str, str] = None,
                 timeout: int = DEFAULT_TIMEOUT):
        super().__init__(db_params, credentials, timeout)
        self._url = url
        self._http_client = HttpClient(url, headers=db_params, credentials=credentials, timeout=timeout)
        self._sign(credentials, db_params, url)
        self._connected = True
        masked_creds = None if not credentials else (credentials[0], "******")
        logger.info("Connected to %s, params: %s, credentials: %s, timeout: %s", url, db_params, masked_creds, timeout)

    def _use_rpc(self, data) -> SurrealResult:
        _, text = self._rpc(data)
        return to_result(text)

    def _sign(self, credentials, db_params, url):
        user, password, ns, db, ac = None, None, None, None, None
        if credentials:
            user, password = credentials
        if db_params:
            ns = db_params.get(NS)
            db = db_params.get(DB)
            ac = db_params.get(AC)
        try:
            result = self._signin(user, password, namespace=ns, database=db, access=ac)
            if result.is_error():
                logger.error("Cant sign in to %s with given credentials", url)
                raise SurrealConnectionError(f"Cant sign in to {url} with given credentials\n"
                                             f"Info: {result.additional_info}\n")
            self._http_client.set_token(result.result)
            self._token = result.result
        except HttpClientError:
            logger.error("Cant connect to %s", url)
            raise SurrealConnectionError(f"Cant connect to {url}\n"
                                         f"Is your SurrealDB started and work on that url? "
                                         f"Refer to https://docs.surrealdb.com/docs/introduction/start")

    def transport(self) -> Transport:
        """
        Returns the transport type for http connection
        """
        return Transport.HTTP

    @connected
    def import_data(self, path: Union[str, Path]) -> SurrealResult:
        """
        This method imports a SurrealQL script file into a local or remote SurrealDB database server.

        Refer to:
        https://docs.surrealdb.com/docs/integration/http#import
        https://docs.surrealdb.com/docs/cli/import

        Refer to: https://docs.surrealdb.com/docs/integration/http#import
        :param path: path to file for import 
        :return: result of request
        """
        with open(path, 'rb') as file:
            logger.info("Operation: IMPORT. Path: %s", path)
            _, text = self._simple_request("POST", "import", file, type_of_content="FILE")
        return to_result(text)

    @connected
    def export(self) -> str:
        """
        This method exports all data for a specific namespace and database.

        Refer to: https://docs.surrealdb.com/docs/integration/http#export

        Refer to: https://docs.surrealdb.com/docs/cli/export/


        :return: text of the exported file to use or save
        """
        text = raise_if_not_http_ok(self._simple_get("export"))
        return text

    @connected
    def ml_import(self, path: Union[str, Path]) -> SurrealResult:
        """
        This method imports a SurrealQL ML file into a local or remote SurrealDB database server.

        Refer to:
        https://docs.surrealdb.com/docs/integration/http#ml-import
        https://docs.surrealdb.com/docs/cli/ml/import

        :param path: path to file for import
        :return: result of request
        """
        with open(path, 'rb') as file:
            logger.info("Operation: ML IMPORT. Path: %s", path)
            _, text = self._simple_request("POST", "ml/import", file.read().decode(ENCODING), type_of_content="STR")
        return to_result(text)

    @connected
    def ml_export(self, name: str, version: str) -> str:
        """
        This method exports a SurrealML machine learning model from a specific namespace and database. As machine
        learning files can be large, the endpoint outputs a chunked HTTP response.

        Refer to: https://docs.surrealdb.com/docs/integration/http#ml-export

        Refer to: https://docs.surrealdb.com/docs/cli/ml/export

        Example:
        http_connection.ml_export("prediction", "1.0.0")

        :param name: name of ML model
        :param version: version of ML model
        :return:  text of the exported file to use or save
        """
        logger.info("Operation: ML EXPORT. Name %s Version %s", name, version)
        text = raise_if_not_http_ok(self._simple_get(f"ml/export/{name}/{version}"))
        return text

    @connected
    def use(self, namespace: str, database: Optional[str] = None) -> None:
        """
        This method specifies the namespace and optional database for the current connection. For http-connection, it
        actually means using headers "surreal-ns" and "surreal-db", no checks for namespace or database to be existent
        will be made

        Example:
        http_connection.use("test", "test") # use test namespace and test database for this connection

        :param namespace: name of the namespace to use
        :param database: name of the database to use (optional)
        :return: None
        """
        logger.info("Operation: USE. Namespace: %s, database %s", namespace, database or "None")
        self._db_params = {NS: namespace}
        if database:
            self._db_params[DB] = database
        self._http_client.set_db_params(self._db_params)

    @connected
    def graphql(self, query: Dict, pretty: Optional[bool] = False) -> SurrealResult:
        """
        This method allows you to execute GraphQL queries against the database.
        The query parameter is a dictionary with the following fields:
        - query (required): The GraphQL query string.
        - variables or vars (optional): An object containing variables for the query.
        - operationName or operation (optional): The name of the operation to execute.

        Refer to: https://surrealdb.com/docs/surrealdb/querying/graphql
        Example: https://github.com/kotolex/surrealist/tree/master/examples/graph_ql.py

        Important Note: GraphQL validates all schemas for all tables in the database, so if there are some errors,
        you get an error back, even if the problem is not with your data

        Examples:
        connection.graphql({"query": "{ author { id name } }"}, pretty=True)

        :param query: dictionary with all parameters
        :param pretty: optional boolean parameter, indicating whether the output should be pretty-printed.
        :return: result of request
        :raise WrongParameterError: if query is not valid dictionary
        """
        allowed_fields = ("query", "variables", "vars", "operationName", "operation")
        if "query" not in query or any(field not in allowed_fields for field in query.keys()):
            raise WrongParameterError("Query parameter should be a dictionary with 3 fields\n"
                                      "Please see https://surrealdb.com/docs/surrealdb/integration/rpc#graphql")
        if pretty:
            query["pretty"] = True
        logger.info("Operation: GRAPHQL. Query: %s, pretty: %s", query, pretty)
        status, text = self._simple_request("POST", "graphql", query)
        if status == HTTP_OK:
            body = json.loads(text)
            if "errors" in body:
                return SurrealResult(result=body["errors"], code=status, query=query, status=ERR, full_body=body)
            return SurrealResult(result=body, code=status, query=query, status=OK)
        return SurrealResult(result=text, code=status, query=query, status=ERR)

    def reset(self):
        """
        Http transport cannot use reset method, you should use websocket transport for that

        :raise CompatibilityError: on any use
        """
        message = "Http transport cannot use reset, use websockets instead"
        logger.error(message)
        raise CompatibilityError(message)

    def live(self, table_name, callback, return_diff: bool = False):
        """
        Http transport cannot use live queries, you should use websocket transport for that

        :raise CompatibilityError: on any use
        """
        message = "Http transport cannot use live queries, use websockets instead"
        logger.error(message)
        raise CompatibilityError(message)

    def custom_live(self, custom_query, callback):
        """
        Http transport cannot use live queries, you should use websocket transport for that

        :raise CompatibilityError: on any use
        """
        message = "Http transport cannot use custom live queries, use websockets instead"
        logger.error(message)
        raise CompatibilityError(message)

    def kill(self, live_query_id: str):
        """
        Http transport cannot use KILL operation, you should use websocket transport for that

        :raise CompatibilityError: on any use
        """
        message = "Http transport cannot use kill, use websockets instead"
        logger.error(message)
        raise CompatibilityError(message)

    def let(self, name: str, value: Any):
        """
        Http transport cannot use LET method, you should use websocket for that

        :raise CompatibilityError: on any use
        """
        message = "Http transport cannot use LET method, you should use websocket for that"
        logger.error(message)
        raise CompatibilityError(message)

    def unset(self, name: str):
        """
        Http transport cannot use UNSET method, you should use websocket for that

        :raise CompatibilityError: on any use
        """
        message = "Http transport cannot use UNSET method, you should use websocket for that"
        logger.error(message)
        raise CompatibilityError(message)

    def _simple_get(self, endpoint: str) -> Tuple[int, str]:
        with self._http_client.get(endpoint) as resp:
            status, text = resp.status, resp.read().decode(ENCODING)
            _body = "is empty" if not text else text
            logger.info("Response from /%s, status_code: %s, body: %s", endpoint, status, _body)
            return status, text

    def _rpc(self, data: Union[Dict, str]) -> Tuple[int, str]:
        return self._simple_request("POST", "rpc", data)

    def _simple_request(self, method, endpoint: str, data: Union[Dict, str, BinaryIO],
                        type_of_content: str = "JSON") -> Tuple[int, str]:
        with self._http_client.request(method, data, endpoint, type_of_content=type_of_content) as resp:
            status, text = resp.status, resp.read().decode(ENCODING)
            if type_of_content == "FILE":
                data.close()
            _body = "is empty" if not text else text
            logger.info("Response from /%s, status_code: %s, body %s", endpoint, status, _body)
        return status, text


def raise_if_not_http_ok(result: Tuple[int, str]) -> str:
    """
    Helper for methods which need only success responses

    :param result: pair of status_code and text from the http response
    :return: text of the response if status is 200, else raise
    :raise HttpConnectionError: if status code is not 200
    """
    status, text = result
    if status != HTTP_OK:
        logger.error("Status code is %s, check your data! Info %s", status, text)
        text = text.replace(',', ',\n')
        raise HttpConnectionError(f"Status code is {status}, check your data! Info:\n{text}")
    return text
