"""
    Main class for routing
"""
from abc import ABC, abstractmethod
# pylint:disable=invalid-name
import logging
from flask import request, abort, g
from werkzeug.routing import RequestRedirect, MethodNotAllowed, NotFound
from jwt.exceptions import InvalidTokenError
import jwt


from ._entity import BaseEntity

logger = logging.getLogger()


class BaseRouting(ABC):
    # pylint:disable=missing-class-docstring
    @abstractmethod
    def before_middleware(self) -> None:
        # pylint:disable=missing-function-docstring
        pass


class Routing(BaseRouting):
    """
    :param app: Flask application instance
    :param extensions: :class:`~flask_jwt_router._extensions`
    :param entity: :class:`~flask_jwt_router._entity`
    """
    def __init__(self, app, extensions, entity: BaseEntity):
        self.app = app
        self.extensions = extensions
        self.logger = logger
        self.entity = entity

    def _prefix_api_name(self, w_routes=None):
        """
        If the config has JWT_ROUTER_API_NAME defined then
        update each white listed route with an api name
        :example: "/user" -> "/api/v1/user"
        :param w_routes:
        :return List[str]:
        """
        api_name = self.extensions.api_name
        if not api_name:
            return w_routes
        # Prepend the api name to the white listed route
        named_white_routes = []
        for route_name in w_routes:
            verb, path = route_name
            named_white_routes.append((verb, f"{api_name}{path}"))
        return named_white_routes

    def _add_static_routes(self, path: str) -> bool:
        """
        Always allow /static/ in path and handle static_url_path from Flask **kwargs
        :param path:
        :return:
        """
        paths = path.split("/")
        defined_static = self.app.static_url_path[1:]
        if path == "favicon.ico" or\
                paths[1] == "static" or\
                paths[1] == defined_static:
            return True
        return False

    # pylint:disable=no-self-use
    def _handle_pre_flight(self, method: str) -> bool:
        """
        Handle pre-flight requests with any verb
        :param method
        :return: {bool}
        """
        if method == "OPTIONS":
            return True
        return False

    def _handle_query_params(self, white_route: str, path: str):
        """
        Handles dynamic query params
        All we care about that a path segment has no url conversion.
        We compare it's the same as the whitelist segment & let Flask
        / Werkzeug handle the url matching
        :param white_route:
        :param path:
        :return bool:
        """
        if "<" not in white_route:
            return False

        route_segments = white_route.split("/")
        path_segments = path.split("/")

        for r, p in zip(route_segments, path_segments):
            if len(r) > 0:
                if r[0] != "<":
                    if r != p:
                        return False
        return True

    def _allow_public_routes(self, white_routes):
        """
        Create a list of tuples ie [("POST", "/users")] as public routes.
        Returns False if current route and verb are white listed.
        :param flask_request:
        :param white_routes: List[Tuple]:
        :returns bool:
        """
        method = request.method
        path = request.path
        for white_route in white_routes:
            if self._handle_pre_flight(method):
                return False
            if method == white_route[0] and path == white_route[1]:
                return False
            if method == white_route[0] and self._handle_query_params(white_route[1], path):
                return False
        return True

    def _does_route_exist(self, url: str, method: str) -> bool:
        adapter = self.app.url_map.bind('')
        try:
            adapter.match(url, method=method)
        except RequestRedirect as e:
            # recursively match redirects
            return self._does_route_exist(e.new_url, method)
        except (MethodNotAllowed, NotFound):
            # no match
            return False
        return True

    def before_middleware(self) -> None:
        """
        Handles ignored & whitelisted & static routes with api name
        If it's not static, ignored whitelisted then authorize
        :return: Callable or None
        """
        #pylint:disable=inconsistent-return-statements
        path = request.path
        method = request.method
        is_static = self._add_static_routes(path)
        if not is_static:
            # Handle ignored routes
            if self._does_route_exist(path, method):
                is_ignored = False
                ignored_routes = self.extensions.ignored_routes
                if len(ignored_routes) > 0:
                    is_ignored = not self._allow_public_routes(ignored_routes)
                if not is_ignored:
                    white_routes = self._prefix_api_name(self.extensions.whitelist_routes)
                    not_whitelist = self._allow_public_routes(white_routes)
                    if not_whitelist:
                        self._handle_token()

    def _handle_token(self):
        """
        Checks the headers contain a Bearer string OR params.
        Checks to see that the route is white listed.
        :return None:
        """
        try:
            if request.args.get("auth"):
                token = request.args.get("auth")
            else:
                bearer = request.headers.get("Authorization")
                token = bearer.split("Bearer ")[1]
        except AttributeError:
            return abort(401)
        try:
            decoded_token = jwt.decode(
                token,
                self.extensions.secret_key,
                algorithms="HS256"
            )
        except InvalidTokenError:
            return abort(401)
        try:
            entity = self.entity.get_entity_from_token(decoded_token)
            setattr(g, self.entity.get_entity_from_ext().__tablename__, entity)
            return None
        except ValueError:
            return abort(401)