# from sqlalchemy.orm.session import Session
from sqlalchemy.orm.scoping import scoped_session
from typing import Dict, Any, Tuple

from sqlalchemy.orm.exc import NoResultFound
from hydrus.data.db_models import (Graph, BaseProperty, RDFClass, Instance,
                                   Terminal)
from hydrus.data.exceptions import (
    PageNotFound,
    InvalidSearchParameter,
    IncompatibleParameters,
    OffsetOutOfRange)


def apply_filter(object_id: str, search_props: Dict[str, Any],
                 triples: Graph, session: scoped_session) -> bool:
    """Check whether objects has properties with query values or not.
    :param object_id: Id of the instance.
    :param search_props: Dictionary of query parameters with property id and values.
    :param triples: All triples.
    :param session: sqlalchemy scoped session.
    :return: True if the instance has properties with given values, False otherwise.
    """
    for prop in search_props:
        # For nested properties
        if isinstance(search_props[prop], dict):
            data = session.query(triples).filter(
                triples.GraphIII.subject == object_id, triples.GraphIII.predicate == prop).one()
            if apply_filter(data.object_, search_props[prop], triples, session) is False:
                return False
        else:
            data = session.query(triples).filter(
                triples.GraphIIT.subject == object_id, triples.GraphIIT.predicate == prop).one()
            terminal = session.query(Terminal).filter(
                Terminal.id == data.object_).one()
            if terminal.value != search_props[prop]:
                return False
    return True


def recreate_iri(API_NAME: str, path: str, search_params: Dict[str, Any]) -> str:
    """Recreate the IRI with query arguments
    :param API_NAME: API name specified while starting the server.
    :param path: endpoint
    :param search_params: List of query parameters.
    :return: Recreated IRI.
    """
    iri = "/{}/{}?".format(API_NAME, path)
    for param in search_params:
        # Skip page, pageIndex or offset parameters as they will be updated to point to
        # next, previous and last page
        if param == "page" or param == "pageIndex" or param == "offset":
            continue
        iri += "{}={}&".format(param, search_params[param])
    return iri


def parse_search_params(search_params: Dict[str, Any],
                        properties: BaseProperty,
                        session: scoped_session) -> Dict[str, Any]:
    """Parse search parameters and create a dict with id of parameters as keys.
    :param search_params: Dictionary having input search parameters.
    :param properties: All properties.
    :param session: sqlalchemy session.
    :return: A dictionary having property ids as keys.
    """
    search_props = dict()
    pagination_parameters = ["page", "pageIndex", "limit", "offset"]
    for param in search_params:
        # Skip if the parameter is a pagination parameter
        if param in pagination_parameters:
            continue
        # For one level deep nested parameters
        if "[" in param and "]" in param:
            prop_name = param.split('[')[0]
            try:
                prop_id = session.query(properties).filter(
                    properties.name == prop_name).one().id
                if prop_id not in search_props:
                    search_props[prop_id] = {}
                nested_prop_id = session.query(properties).filter(
                    properties.name == param[param.find('[') + 1:param.find(']')]).one().id
                search_props[prop_id][nested_prop_id] = search_params[param]
            except NoResultFound:
                raise InvalidSearchParameter(param)
        # For normal parameters
        else:
            try:
                prop_id = session.query(properties).filter(
                    properties.name == param).one().id
                search_props[prop_id] = search_params[param]
            except NoResultFound:
                raise InvalidSearchParameter(param)
    return search_props


def calculate_page_limit_and_offset(paginate: bool, page_size: int, page: int, result_length: int,
                                    offset: int, limit: int) -> Tuple[int, int]:
    """Calculate page limit and offset for pagination.
    :param paginate: Showing whether pagination is enable/disable.
    :param page_size: Number maximum elements showed in a page.
    :param page: page number.
    :param result_length: Length of the list containing desired elements.
    :param offset: offset value.
    :param limit: page limit.
    :return: page limit and offset.
    """
    if limit is not None:
        page_size = limit
    if paginate is True:
        if offset is None:
            offset = (page - 1) * page_size
        limit = page_size
    else:
        offset = 0
        limit = result_length

    return limit, offset


def pre_process_pagination_parameters(search_params: Dict[str, Any], paginate: bool,
                                      page_size: int, result_length: int) -> Tuple[int, int, int]:
    """Pre-process pagination related query parameters passed by client.
    :param search_params: Dict of all search parameters.
    :param paginate: Indicates if pagination is enabled/disabled.
    :param page_size: Maximum element a page can contain.
    :param result_length: Length of the list of containing desired items.
    :return: returns page number, page limit and offset.
    """
    incompatible_parameters = ["page", "pageIndex", "offset"]
    incompatible_parameters_len = len(incompatible_parameters)
    # Find any pair of incompatible parameters
    for i in range(incompatible_parameters_len):
        if incompatible_parameters[i] not in search_params:
            continue
        if i != incompatible_parameters_len - 1:
            for j in range(i+1, incompatible_parameters_len):
                if incompatible_parameters[j] in search_params:
                    raise IncompatibleParameters([incompatible_parameters[i],
                                                  incompatible_parameters[j]])
    try:
        # Extract page number from query arguments
        if "pageIndex" in search_params:
            page = int(search_params.get("pageIndex"))
            offset = None
        elif "offset" in search_params:
            offset = int(search_params.get("offset"))
            page = offset//page_size + 1
            if offset > result_length:
                raise OffsetOutOfRange(str(offset))
        else:
            page = int(search_params.get("page", 1))
            offset = None
        if "limit" in search_params:
            limit = int(search_params.get("limit"))
        else:
            limit = None
    except ValueError:
        raise PageNotFound(page)
    page_limit, offset = calculate_page_limit_and_offset(paginate=paginate, page_size=page_size,
                                                         page=page, result_length=result_length,
                                                         offset=offset, limit=limit)
    return page, page_limit, offset


def attach_hydra_view(collection_template: Dict[str, Any], paginate_param: str,
                      result_length: int, page_size: int,
                      iri: str, offset: int = None,
                      page: int = None, last: int = None) -> None:
    """Attaches hydra:view to the collection template.
    :param collection_template: the collection template.
    :param paginate_param: type of paginate parameter used.
    :param result_length: length of the result set.
    :param page_size: size of the page.
    :param iri: IRI of the collection with query parameters except "page", "pageIndex" and "offset".
    :param offset: offset used for pagination, None if not used.
    :param page: page number used for pagination, None if not used.
    :param last: Page number of the last page only used when "page" or "pageIndex"
                 is used for pagination, None otherwise.
    """
    if paginate_param == "offset":
        collection_template["view"] = {
            "@id": "{}{}={}".format(iri, paginate_param, offset),
            "@type": "PartialCollectionView",
            "first": "{}{}=0".format(iri, paginate_param),
            "last": "{}{}={}".format(iri, paginate_param, result_length-page_size)
        }
        if offset > page_size:
            collection_template["view"]["previous"] = "{}{}={}".format(iri,
                                                                       paginate_param,
                                                                       offset - page_size)
        if offset < result_length-page_size:
            collection_template["view"]["next"] = "{}{}={}".format(iri,
                                                                   paginate_param,
                                                                   offset + page_size)
    else:
        collection_template["view"] = {
            "@id": "{}{}={}".format(iri, paginate_param, page),
            "@type": "PartialCollectionView",
            "first": "{}{}=1".format(iri, paginate_param),
            "last": "{}{}={}".format(iri, paginate_param, last)
        }
        if page != 1:
            collection_template["view"]["previous"] = "{}{}={}".format(iri, paginate_param,
                                                                       page-1)
        if page != last:
            collection_template["view"]["next"] = "{}{}={}".format(iri, paginate_param,
                                                                   page + 1)