import json
import typing
from itertools import product
from typing import cast

import structlog
from httpx import HTTPStatusError

from csirtmu.auth_keycloak_fixture_loader.config import AppConfig, Client, Realm, Scope, User
from csirtmu.auth_keycloak_fixture_loader.keycloak_api_client import KeycloakAPIClient


class FixtureLoader:
    def __init__(self) -> None:
        self.logger = structlog.get_logger()
        self.config = AppConfig.get()
        self.keycloak = KeycloakAPIClient(
            base_url=self.config.keycloak.base_url,
            master_user=self.config.keycloak.master_user,
            master_pass=self.config.keycloak.master_password,
            public_url=self.config.keycloak.public_url,
            legacy=self.config.legacy,
        )

    def load_fixtures(self) -> None:
        for realm in self.config.keycloak.realms:
            try:
                self.load_realm(realm)
            except HTTPStatusError:
                self.logger.exception(f"Failed to load realm: {realm.name}")

    def load_realm(self, realm: Realm):
        # Create the realm
        self.keycloak.create_realm(realm=realm.name)

        # Create client scopes
        for scope in realm.scopes:
            self._create_client_scope_with_claims(realm_name=realm.name, scope=scope)

        # Create clients
        for client in realm.clients:
            self._create_client_in_realm(client=client, realm_name=realm.name)

        # Create users
        for user in realm.users:
            self._create_user_in_realm(user=user, realm_name=realm.name)

        # # Enable token exchange
        # try:
        #     self._configure_token_exchange_in_realm(realm=realm)
        # except HTTPStatusError:
        #     self.logger.exception(
        #         "Token exchange is probably not enabled on the Keycloak server."
        #         "It is experimental feature and requires extra steps to enable."
        #         "For Docker distribution of Keycloak, set"
        #         '"-Dkeycloak.profile.feature.token_exchange=enabled '
        #         '-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled" '
        #         "in JAVA_OPTS_APPEND environment variable."
        #     )

        # Create roles
        for role in realm.roles:
            self.keycloak.create_role_in_realm(realm=realm.name, role_name=role.name, role_description=role.description)

        # Create and setup groups
        for group in realm.groups:
            self.keycloak.create_group_in_realm(realm=realm.name, group_name=group.name)
            group_id = self.keycloak.get_realm_group_by_name(realm=realm.name, group_name=group.name)

            self._add_roles_to_group(realm_name=realm.name, group_id=group_id, role_names=group.rolenames)
            self._add_users_to_group(realm_name=realm.name, group_id=group_id, user_names=group.usernames)

        # Setup role mapping scope (capability)
        if realm.role_mapping_scope is not None:
            self._configure_role_mapping_scope(realm=realm)

    def _configure_role_mapping_scope(self, realm: Realm) -> None:
        if not realm.role_mapping_scope:
            return

        client_scope_id = self._find_client_scope_id_by_name_in_realm(
            scope_name=realm.role_mapping_scope, realm_name=realm.name
        )
        if not client_scope_id:
            msg = f"Role mapping scope {realm.role_mapping_scope} does not exist in realm {realm.name}"
            raise ValueError(msg)

        for client in realm.clients:
            self.keycloak.add_default_client_scope_to_client(
                realm=realm.name, client_name=client.name, client_scope_id=client_scope_id
            )

        self.keycloak.add_realm_roles_mapping_to_client_scope(
            realm=realm.name, client_scope_id=client_scope_id, client_scope_name=realm.role_mapping_scope
        )

    def _add_roles_to_group(self, role_names: list[str], realm_name: str, group_id: str) -> None:
        for role_name in role_names:
            role_id = self.keycloak.get_realm_role_by_name(role_name=role_name, realm=realm_name)
            self.keycloak.add_role_to_group(realm=realm_name, group_id=group_id, role_name=role_name, role_id=role_id)

    def _add_users_to_group(self, realm_name: str, user_names: list[str], group_id: str) -> None:
        for user_name in user_names:
            user_id = self.keycloak.get_user_id_in_realm_by_username(realm=realm_name, username=user_name)
            if not user_id:
                msg = f"User {user_name} not found!"
                raise ValueError(msg)
            self.keycloak.add_user_to_group(realm=realm_name, user_id=user_id, group_id=group_id)

    def _find_client_scope_id_by_name_in_realm(self, scope_name: str, realm_name: str) -> str | None:
        if searched_scope := next(
            (
                cast("dict[str, str]", searched_scope)
                for searched_scope in self.keycloak.get_client_scopes(realm=realm_name)
                if searched_scope["name"] == scope_name
            ),
            None,
        ):
            return searched_scope["id"]
        return None

    def _find_client_id_by_name_in_realm(self, client_name: str, realm_name: str) -> typing.Any | None:
        if searched_client := next(
            (
                searched_client
                for searched_client in self.keycloak.get_clients(realm=realm_name)
                if searched_client["clientId"] == client_name
            ),
            None,
        ):
            return searched_client["id"]
        return None

    def _create_client_in_realm(self, client: Client, realm_name: str) -> None:
        self.keycloak.create_client(realm=realm_name, client_name=client.name, client_secret=client.secret)
        for scope_name in client.add_default_scopes:
            if scope_id := self._find_client_scope_id_by_name_in_realm(scope_name=scope_name, realm_name=realm_name):
                self.keycloak.add_default_client_scope_to_client(
                    realm=realm_name, client_name=client.name, client_scope_id=scope_id
                )
            else:
                self.logger.warning(
                    f"Failed to add scope: {scope_name} to client {client.name}, the scope does not exist."
                )

        for scope_name in client.add_optional_scopes:
            if scope_id := self._find_client_scope_id_by_name_in_realm(scope_name=scope_name, realm_name=realm_name):
                self.keycloak.add_optional_client_scope_to_client(
                    realm=realm_name, client_name=client.name, client_scope_id=scope_id
                )
            else:
                self.logger.warning(
                    f"Failed to add scope: {scope_name} to client {client.name}, the scope does not exist."
                )

        self.keycloak.set_redirect_uris_for_client(
            realm=realm_name, client_name=client.name, redirect_uris=client.redirect_uris
        )
        self.keycloak.set_post_logout_redirect_uris_for_client(
            realm=realm_name, client_name=client.name, redirect_uris=client.post_logout_redirect_uris
        )
        self.keycloak.set_web_origins_for_client(
            realm=realm_name, client_name=client.name, web_origins=client.web_origins
        )
        if client.include_audience_mapper:
            self._create_audience_mapper(
                self._find_client_id_by_name_in_realm(client.name, realm_name), client.name, realm_name
            )

    def _create_audience_mapper(self, client_id: typing.Any | None, client_name: str, realm_name: str) -> None:
        if client_id is not None:
            self.keycloak.create_audience_mapper(typing.cast("str", client_id), client_name, realm_name)

    def _create_client_scope_with_claims(self, realm_name: str, scope: Scope) -> None:
        # First, create the scope
        self.keycloak.create_client_scope(realm=realm_name, name=scope.name)

        # Next, find ID of the scope
        if not (
            client_scope_id := self._find_client_scope_id_by_name_in_realm(scope_name=scope.name, realm_name=realm_name)
        ):
            msg = f"Failed to create scope: {scope.name} in realm: {realm_name}"
            raise ValueError(msg)

        # Add Claims to the scope
        for claim in scope.claims:
            if isinstance(claim.capability, str):
                capability = claim.capability
            else:
                try:
                    capability = json.dumps(claim.capability)
                except ValueError as e:
                    self.logger.warning(
                        f"Failed to JSON Serialize following claim's capability: {claim.name},"
                        f"attempting to log value: {claim.capability}, original exception"
                        f"{e!s}"
                    )
                    continue

            self.keycloak.add_oidc_claim_to_client_scope(
                realm=realm_name,
                client_scope_id=client_scope_id,
                mapper_name=claim.name,
                claim_name=claim.name,
                claim_value=capability,
            )

        # make scope default
        if scope.default:
            self.keycloak.make_client_scope_default(realm=realm_name, client_scope_id=client_scope_id)

    def _create_user_in_realm(self, user: User, realm_name: str) -> None:
        self.keycloak.create_user(
            username=user.username,
            first_name=user.first_name,
            last_name=user.last_name,
            password=user.password,
            email=user.email,
            realm=realm_name,
        )
        if not user.authorized:
            self.keycloak.remove_role_mapping_from_user(
                realm=realm_name, username=user.username, role_name=f"default-roles-{realm_name.lower()}"
            )

    def _configure_token_exchange_in_realm(self, realm: Realm) -> None:
        sources = [client.name for client in realm.clients]
        targets = [client.name for client in realm.clients if client.exchange_target]

        for source, target in product(sources, targets):
            if source == target:
                continue

            self.keycloak.enable_token_exchange_from_client_to_client(
                realm=realm.name, source_client_name=source, target_client_name=target
            )
            self.logger.info(f"Enabled token exchange from client {source} to client {target} in realm {realm.name}")
