from typing import Optional

from flask import request, Request, has_request_context
from keycloak import KeycloakAuthenticationError, KeycloakConnectionError

from api.auth.keycloak_client import Keycloak
from api.auth.keycloak_user import KeycloakUser
from core.get_value import get_value
from core.logger import Logger
from data.schemas.administration.user import User
from data.schemas.administration.user_dao import userDao
from data.schemas.permission.role_dao import roleDao
from data.schemas.permission.role_user import RoleUser
from data.schemas.permission.role_user_dao import roleUserDao

logger = Logger(__name__)


class RouteUserExtension:

    @classmethod
    def _get_user_id_from_token(cls) -> Optional[str]:
        token = cls.get_token()
        if not token:
            return None

        try:
            user_info = Keycloak.client.userinfo(token)
        except KeycloakAuthenticationError:
            user_info = {}
        except KeycloakConnectionError:
            user_info = {}

        return get_value(user_info, "sub", str)

    @staticmethod
    def get_token() -> Optional[str]:
        if "Authorization" not in request.headers:
            return None

        if len(request.headers.get("Authorization").split()) < 2:
            return None

        return request.headers.get("Authorization").split()[1]

    @classmethod
    async def get_user(cls) -> Optional[User]:
        if not has_request_context():
            return None

        user_id = cls._get_user_id_from_token()
        if not user_id:
            return None

        user = await userDao.find_by_keycloak_id(user_id)
        if user is None:
            return None

        return user

    @classmethod
    async def get_dev_user(cls) -> Optional[User]:
        return await userDao.find_single_by(
            [{User.keycloak_id: cls.get_token()}, {User.deleted: False}]
        )

    @classmethod
    async def get_user_or_default(cls) -> Optional[User]:
        user = await cls.get_user()
        if user is None:
            user = await cls.get_dev_user()
        return user

    @classmethod
    def _flatten_groups(cls, groups):
        flat_list = []
        for group in groups:
            flat_list.append(group)
            if "subGroups" in group and group["subGroups"]:
                flat_list.extend(cls._flatten_groups(group["subGroups"]))
        return flat_list

    @classmethod
    async def _map_keycloak_groups_with_roles(cls, user: User):
        try:
            roles = {x.name: x for x in await roleDao.get_all()}
            groups = cls._flatten_groups(Keycloak.admin.get_groups(full_hierarchy=True))
            groups_with_role = [x["name"] for x in groups if x["name"] in roles.keys()]

            user_groups_with_role = [
                x["name"]
                for x in Keycloak.admin.get_user_groups(user.keycloak_id)
                if x["name"] in roles.keys()
            ]
            user_roles = set(
                x.name for x in await user.roles if x.name in groups_with_role
            )
            missing_groups = set(user_groups_with_role) - set(user_roles)
            missing_roles = set(user_roles) - set(user_groups_with_role)

            if len(missing_groups) > 0:
                await roleUserDao.create_many(
                    [
                        RoleUser(0, (await roleDao.get_by_name(group)).id, user.id)
                        for group in missing_groups
                    ]
                )

            if len(missing_roles) > 0:
                await roleUserDao.delete_many(
                    [
                        await roleUserDao.get_single_by(
                            [
                                {RoleUser.role_id: roles[role].id},
                                {RoleUser.user_id: user.id},
                            ]
                        )
                        for role in missing_roles
                    ]
                )
        except Exception as e:
            logger.error("Failed to map user groups", e)

    @classmethod
    async def _create_user(cls, kc_user: KeycloakUser):
        try:
            user = await userDao.find_by_keycloak_id(kc_user.id)
            if user is not None:
                logger.warning(f"User {user.id}:{kc_user.id} already exists")
                return

            await userDao.create(User(0, kc_user.id))
            users = await userDao.get_all()
            if len(users) == 1:
                user = await userDao.get_by_keycloak_id(kc_user.id)
                admin_role = await roleDao.get_by_name("admin")
                logger.warning(f"Assigning admin role to first user {user.id}")
                await roleUserDao.create(RoleUser(0, admin_role.id, user.id))
        except Exception as e:
            logger.error("Failed to find or create user", e)

    @classmethod
    async def verify_login(cls, req: Request) -> bool:
        auth_header = req.headers.get("Authorization", None)

        if not auth_header or not auth_header.startswith("Bearer "):
            return False

        token = auth_header.split(" ")[1]
        try:
            # Verify token using Keycloak
            user_info = Keycloak.client.userinfo(token)
            if not user_info:
                return False

            user = await cls.get_user()
            if user is None:
                u_id = await cls._create_user(KeycloakUser(user_info))
                await cls._map_keycloak_groups_with_roles(await userDao.get_by_id(u_id))
                return True
            else:
                await cls._map_keycloak_groups_with_roles(user)

            if user.deleted:
                return False
        except KeycloakAuthenticationError:
            return False

        return True
