#!/usr/bin/env python3
import argparse
import asyncio
import ssl
import sys
import typing

import aiohttp_cors
import uvloop
from aiohttp import web

# make asyncio use the event loop provided by uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

import inspect
import logging
import signal
from concurrent.futures import ThreadPoolExecutor
from functools import wraps
from typing import Any, Callable, Optional

# from aiohttp_swagger import setup_swagger
import sockjs
from aiohttp import web
from aiohttp.abc import AbstractView
from aiohttp_session import get_session
from aiohttp_session import setup as setup_session
from aiohttp_session.redis_storage import RedisStorage

# from apps.setup import app_startup
from aiohttp_utils import run as runner

from navigator.applications import AppBase, AppHandler, app_startup
from navigator.conf import (
    APP_DIR,
    BASE_DIR,
    EMAIL_CONTACT,
    INSTALLED_APPS,
    LOCAL_DEVELOPMENT,
    SECRET_KEY,
    SESSION_PREFIX,
    SESSION_STORAGE,
    SESSION_URL,
    STATIC_DIR,
    Context,
    config,
)

# Exception Handlers
from navigator.handlers import nav_exception_handler, shutdown

# get the authentication library
from navigator.modules.auth import AuthHandler, auth_middleware, login_required
from navigator.resources import WebSocket, channel_handler

__version__ = "1.0.0.dev0"


def get_version():
    return __version__


def Response(
    content: Any,
    text: str = "",
    body: Any = None,
    status: int = 200,
    headers: dict = None,
    content_type: str = "text/plain",
    charset: str = "utf-8",
) -> web.Response:
    """
    Response.
    Web Response Definition for Navigator
    """
    response = {"content_type": content_type, "charset": charset, "status": status}
    if headers:
        response["headers"] = headers
    if isinstance(content, str) or text is not None:
        response["text"] = content if content else text
    else:
        response["body"] = content if content else body
    return web.Response(**response)


class Application(object):
    app: Any = None
    _auth = None
    debug = False
    parser = None
    use_ssl = False
    _routes: list = []
    _reload = False
    _logger = None
    path = ""
    host = "0.0.0.0"
    port = 5000
    _loop = None

    def __init__(self, app: AppHandler = None, *args: typing.Any, **kwargs: typing.Any):
        # configuring asyncio loop
        try:
            self._loop = asyncio.get_event_loop()
        except RuntimeError:
            logger.debug(
                "Couldn't get event loop for current thread. Creating a new event loop to be used!"
            )
            self._loop = asyncio.new_event_loop()
            asyncio.set_event_loop(self._loop)
        # self._loop.set_exception_handler(nav_exception_handler)
        # May want to catch other signals too
        signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
        for s in signals:
            self._loop.add_signal_handler(
                s, lambda s=s: asyncio.create_task(shutdown(self._loop, s))
            )
        self._executor = ThreadPoolExecutor()
        if "debug" in kwargs:
            self.debug = kwargs["debug"]
        self.parser = argparse.ArgumentParser(description="Navigator App")
        self.parser.add_argument("--path")
        self.parser.add_argument("--host")
        self.parser.add_argument("--port")
        self.parser.add_argument("-r", "--reload", action="store_true")
        self.parser.add_argument(
            "-d", "--debug", action="store_true", help="Enable Debug"
        )
        self.parser.add_argument(
            "--traceback", action="store_true", help="Return the Traceback on Error"
        )
        self.parse_arguments()
        if not app:
            # create an instance of AppHandler
            self.app = AppBase(Context)
        else:
            self.app = app(Context)
        self._logger = self.get_logger(self.app.Name)

    def parse_arguments(self):
        args = self.parser.parse_args()
        try:
            self.path = args.path
        except (KeyError, ValueError, TypeError):
            self.path = None
        try:
            self.host = args.host
            if not self.host:
                self.host = "0.0.0.0"
        except (KeyError, ValueError, TypeError):
            self.host = "0.0.0.0"
        try:
            self.port = args.port
            if not self.port:
                self.port = 5000
        except (KeyError, ValueError, TypeError):
            self.port = 5000
        try:
            self.debug = args.debug
        except (KeyError, ValueError, TypeError):
            pass
        try:
            self._reload = args.reload
        except (KeyError, ValueError, TypeError):
            self._reload = False

    def get_app(self) -> web.Application:
        return self.app.App

    def get_logger(self, name="Navigator"):
        logging_format = f"[%(asctime)s] %(levelname)-5s %(name)-{len(name)}s "
        # logging_format += "%(module)-7s::l%(lineno)d: "
        # logging_format += "%(module)-7s: "
        logging_format += "%(message)s"
        logging.basicConfig(
            format=logging_format, level=logging.INFO, datefmt="%Y:%m:%d %H:%M:%S"
        )
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("websockets").setLevel(logging.INFO)
        logging.getLogger("aiohttp.web").setLevel(logging.INFO)
        return logging.getLogger(name)

    def setup_app(self) -> web.Application:
        app = self.get_app()
        # # TODO: iterate over modules folder
        self._auth = AuthHandler(
            type=SESSION_STORAGE,
            name=SESSION_PREFIX,
            url=SESSION_URL,
            backends=(
                "session",
                "hosts",
            ),
        )
        self._auth.configure(app)
        # adding middleware for Authorization
        app.middlewares.append(auth_middleware)
        # setup The Application and Sub-Applications Startup
        app_startup(INSTALLED_APPS, app, Context)
        app["auth"] = self._auth
        # Configure Routes
        self.app.configure()
        cors = aiohttp_cors.setup(
            app,
            defaults={
                "*": aiohttp_cors.ResourceOptions(
                    allow_credentials=True,
                    expose_headers="*",
                    allow_methods="*",
                    allow_headers="*",
                    max_age=3600,
                )
            },
        )
        self.app.setup_cors(cors)
        self.app.setup_docs()
        # # auto-configure swagger
        # long_description = """
        # Asynchronous RESTful API for data source connections, REST consumer and Query API, used by Navigator, powered by MobileInsight
        # """
        # setup_swagger(app,
        #     api_base_url='/',
        #     title='API',
        #     api_version='2.0.0',
        #     description=long_description,
        #     contact=EMAIL_CONTACT,
        #     swagger_url="/api/doc")
        # TODO: configure documentation
        return app

    def add_websockets(self) -> None:
        """
        add_websockets.
        description: enable support for websockets in the main App
        """
        app = self.get_app()
        if self.debug:
            print("Enabling WebSockets")
        # websockets
        app.router.add_route("GET", "/ws", WebSocket)
        # websocket channels
        app.router.add_route("GET", "/ws/{channel}", channel_handler)

    def add_routes(self, routes: list) -> None:
        """
        add_route
        description: append a route to routes dict
        """
        # TODO: avoid to add same route different times
        # self._routes.append(route)
        self.get_app().add_routes(routes)

    def add_static(self, route: str, path: str):
        """
        add_static
        description: register new route to static path.
        """
        self.get_app().add_static(route, path)

    def add_view(self, route: str, handler: Any):
        self.get_app().router.add_view(route, handler)

    def threaded_func(self, func: Callable, threaded: bool = False):
        @wraps(func)
        async def _wrap(request):
            result = None
            try:
                if threaded:

                    def blocking_function():
                        return asyncio.new_event_loop().run_until_complete(
                            func(request)
                        )

                    result = await self._loop.run_in_executor(
                        self._executor, blocking_function
                    )
                else:
                    result = await func(request)
                return result
            except Exception as err:
                self._logger.exception(err)

        return _wrap

    def route(self, route: str, method: str = "GET", threaded: bool = False):
        """
        route.
        description: decorator for register a new HTTP route.
        """

        def _decorator(func):
            self.app.App.router.add_route(
                method, route, self.threaded_func(func, threaded)
            )
            return func

        return _decorator

    def add_get(self, route: str, threaded: bool = False) -> Callable:
        def _decorator(func):
            self.app.App.router.add_get(route, self.threaded_func(func, threaded))
            return func

        return _decorator

    def Response(self, content: Any) -> web.Response:
        return web.Response(text=content)

    def get(self, route: str):
        def _decorator(func):
            self.app.App.router.add_get(route, func)

            @wraps(func)
            async def _wrap(request, *args, **kwargs):
                try:
                    return f"{func(request, args, **kwargs)}"
                except Exception as err:
                    self._logger.exception(err)

            return _wrap

        return _decorator

    def add_sock_endpoint(
        self, handler: Callable, name: str, route: str = "/sockjs/"
    ) -> None:
        app = self.get_app()
        sockjs.add_endpoint(app, handler, name=name, prefix=route)

    def run(self):
        # getting the app
        # getting the resource App
        app = self.setup_app()
        if self.debug is True:
            if LOCAL_DEVELOPMENT:
                import aiohttp_debugtoolbar
                from aiohttp_debugtoolbar import toolbar_middleware_factory

                aiohttp_debugtoolbar.setup(app, hosts=[self.host], enabled=True)
            if self._reload:
                runner(
                    app=app,
                    app_uri="navigator.runserver:app",
                    reload=True,
                    port=self.port,
                    host=self.host,
                )
            else:
                web.run_app(app, host=self.host, port=self.port)
        else:
            if self.use_ssl:
                ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
                ssl_context.load_cert_chain(SSL_CERT, SSL_KEY)
                web.run_app(
                    app, host=self.host, port=self.port, ssl_context=ssl_context
                )
            if self.path:
                web.run_app(app, path=self.path)
            else:
                web.run_app(app, host=self.host, port=self.port)
