"""
Pluggable utilities for Hydrus.
===============================
Imports :
contextlib.contextmanager : This function is a decorator that can be used to
define a factory function for with statement context managers, without needing
to create a class or separate __enter__() and __exit__() methods.
Ref- https://docs.python.org/2/library/contextlib.html#contextlib.contextmanager
flask.appcontext_pushed : Signal is sent when an application context is pushed.
The sender is the application.
Ref- http://flask.pocoo.org/docs/0.12/api/#flask.appcontext_pushed
Ref- https://speakerdeck.com/mitsuhiko/advanced-flask-patterns-1
flask.g : Used to attach values to global variables
doc_writer_sample : Sample script used to create Hydra APIDocumentation
hydra_python_core.engine : An SQLalchemy DB engine
sqlalchemy.orm.sessionmaker : Used to create a SQLalchemy Session
sqlalchemy.orm.session.Session : SQLalchemy Session class
Ref- http://docs.sqlalchemy.org/en/latest/orm/session_basics.html
hydra_python_core.doc_writer.HydraDoc : Class for Hydra Documentation
"""  # nopep8

from contextlib import contextmanager
from flask import appcontext_pushed
from flask import g
from hydrus.samples import doc_writer_sample
from hydrus.data.db_models import engine
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm.session import Session
from hydra_python_core.doc_writer import HydraDoc
from flask.app import Flask
from typing import Any, Iterator


@contextmanager
def set_authentication(application: Flask, authentication: bool) -> Iterator:
    """
    Set the whether API needs to be authenticated or not (before it is run in main.py).

    :param application: Flask app object
            <flask.app.Flask>
    :param authentication : Bool. API Auth needed or not
            <bool>


    Raises:
        TypeError: If `authentication` is not a boolean value.

    """
    if not isinstance(authentication, bool):
        raise TypeError("Authentication flag must be of type <bool>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.authentication_ = authentication
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_authentication() -> bool:
    """
    Check whether API needs to be authenticated or not.
    Return and sets False if not found.
    :return authentication : Bool. API Auth needed or not
            <bool>
    """
    try:
        authentication = getattr(g, 'authentication_')
    except AttributeError:
        authentication = False
        g.authentication_ = authentication
    return authentication


@contextmanager
def set_api_name(application: Flask, api_name: str) -> Iterator:
    """
    Set the server name or EntryPoint for the app (before it is run in main.py).
    :param application: Flask app object
            <flask.app.Flask>
    :param api_name : API/Server name or EntryPoint
            <str>

    Raises:
        TypeError: If `api_name` is not a string.

    """
    if not isinstance(api_name, str):
        raise TypeError("The api_name is not of type <str>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.api_name = api_name
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_api_name() -> str:
    """
    Get the server API name.
    Returns an sets "api" as api_name if not found.
    :return api_name : API/Server name or EntryPoint
            <str>
    """
    try:
        api_name = getattr(g, 'api_name')
    except AttributeError:
        api_name = "api"
        g.api_name = api_name
    return api_name


@contextmanager
def set_page_size(application: Flask, page_size: int) -> Iterator:
    """
    Set the page_size of a page view.
    :param application: Flask app object
            <flask.app.Flask>
    :param page_size : Number of maximum elements a page can contain
            <int>

    Raises:
        TypeError: If `page_size` is not an int.

    """
    if not isinstance(page_size, int):
        raise TypeError("The page_size is not of type <int>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.page_size = page_size
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_page_size() -> int:
    """
    Get the page_size of a page-view.
    :return page_size : Number of maximum elements a page view can contain.
            <int>
    """
    try:
        page_size = getattr(g, 'page_size')
    except AttributeError:
        page_size = 10
        g.page_size = page_size
    return page_size


@contextmanager
def set_pagination(application: Flask, paginate: bool) -> Iterator:
    """
    Enable or disable pagination.
    :param application: Flask app object
            <flask.app.Flask>
    :param paginate : Pagination enabled or not
            <bool>

    Raises:
        TypeError: If `paginate` is not a bool.

    """
    if not isinstance(paginate, bool):
        raise TypeError("The CLI argument 'pagination' is not of type <bool>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.paginate = paginate
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_pagination() -> bool:
    """
    Get the pagination status(Enable/Disable).
    :return paginate : Pagination enabled or not
            <bool>
    """
    try:
        paginate = getattr(g, 'paginate')
    except AttributeError:
        paginate = True
        g.paginate = paginate
    return paginate


@contextmanager
def set_doc(application: Flask, APIDOC: HydraDoc) -> Iterator:
    """
    Set the API Documentation for the app (before it is run in main.py).
    :param application: Flask app object
            <flask.app.Flask>
    :param APIDOC : Hydra Documentation object
            <hydra_python_core.doc_writer.HydraDoc>

    Raises:
        TypeError: If `APIDOC` is not an instance of `HydraDoc`.

    """
    if not isinstance(APIDOC, HydraDoc):
        raise TypeError(
            "The API Doc is not of type <hydra_python_core.doc_writer.HydraDoc>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.doc = APIDOC
    with appcontext_pushed.connected_to(handler, application):
        yield


@contextmanager
def set_token(application: Flask, token: bool) -> Iterator:
    """Set whether API needs to implement token based authentication.

    Raises:
        TypeError: If `token` is not a boolean value.

    """
    if not isinstance(token, bool):
        raise TypeError("Token flag must be of type <bool>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.token_ = token
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_doc() -> HydraDoc:
    """
    Get the server API Documentation.
    Returns and sets doc_writer_sample.api_doc if not found.
    :return apidoc : Hydra Documentation object
            <hydra_python_core.doc_writer.HydraDoc>
    """
    try:
        apidoc = getattr(g, 'doc')
    except AttributeError:
        g.doc = apidoc = doc_writer_sample.api_doc
    return apidoc


def get_token() -> bool:
    """Check wether API needs to be authenticated or not."""
    try:
        token = getattr(g, 'token_')
    except AttributeError:
        token = False
        g.token_ = token
    return token


@contextmanager
def set_hydrus_server_url(application: Flask, server_url: str) -> Iterator:
    """
    Set the server URL for the app (before it is run in main.py).
    :param application: Flask app object
            <flask.app.Flask>
    :param server_url : Server URL
            <str>

    Raises:
        TypeError: If the `server_url` is not a string.

    """
    if not isinstance(server_url, str):
        raise TypeError("The server_url is not of type <str>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.hydrus_server_url = server_url
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_hydrus_server_url() -> str:
    """
    Get the server URL.
    Returns and sets "http://localhost/" if not found.
    :return hydrus_server_url : Server URL
            <str>
    """
    try:
        hydrus_server_url = getattr(g, 'hydrus_server_url')
    except AttributeError:
        hydrus_server_url = "http://localhost/"
        g.hydrus_server_url = hydrus_server_url
    return hydrus_server_url


@contextmanager
def set_session(application: Flask, DB_SESSION: scoped_session) -> Iterator:
    """
    Set the Database Session for the app before it is run in main.py.
    :param application: Flask app object
            <flask.app.Flask>
    :param DB_SESSION: SQLalchemy Session object
            <sqlalchemy.orm.session.Session>

    Raises:
        TypeError: If `DB_SESSION` is not an instance of `scoped_session` or `Session`.

    """
    if not isinstance(DB_SESSION, scoped_session) and not isinstance(
            DB_SESSION, Session):
        raise TypeError(
            "The API Doc is not of type <sqlalchemy.orm.session.Session> or"
            " <sqlalchemy.orm.scoping.scoped_session>")

    def handler(sender: Flask, **kwargs: Any) -> None:
        g.dbsession = DB_SESSION
    with appcontext_pushed.connected_to(handler, application):
        yield


def get_session() -> scoped_session:
    """
    Get the Database Session from g.
    Returns and sets a default Session if not found
    :return session : SQLalchemy Session object
            <sqlalchemy.orm.scoped_session>
    """
    try:
        session = getattr(g, 'dbsession')
    except AttributeError:
        session = scoped_session(sessionmaker(bind=engine))
        g.dbsession = session
    return session


def get_collections_and_parsed_classes():
    """
    Get all the collections and parsed classes from
    the API doc.
    :return collections, parsed_classes
            <tuple>
    """
    api_doc = get_doc()
    parsed_classes = api_doc.parsed_classes
    collections = api_doc.collections
    return (collections, parsed_classes)
