from typing import TYPE_CHECKING
from uuid import UUID

from flask import request
from flask_login import AnonymousUserMixin

from app.common.data.interfaces.collections import get_collection, get_submission
from app.common.data.interfaces.grants import get_grant
from app.common.data.models_user import User
from app.common.data.types import OrganisationModeEnum, RoleEnum, SubmissionModeEnum

if TYPE_CHECKING:
    from app.common.data.models import GrantRecipient, Organisation, Submission


class AuthorisationHelper:
    @staticmethod
    def is_logged_in(user: User | AnonymousUserMixin) -> bool:
        return user.is_authenticated

    @staticmethod
    def has_logged_in(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        return bool(user.last_logged_in_at_utc)

    @staticmethod
    def is_platform_admin(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        return any(
            RoleEnum.ADMIN in role.permissions and role.organisation_id is None and role.grant_id is None
            for role in user.roles
        )

    @staticmethod
    def is_platform_member(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        return any(
            RoleEnum.MEMBER in role.permissions and role.organisation_id is None and role.grant_id is None
            for role in user.roles
        )

    @staticmethod
    def has_platform_admin_role(role: RoleEnum, user: User | AnonymousUserMixin) -> bool:
        """
        Will return True if the user has the specified role for as a platform user.
        ADMIN role grants all roles
        """
        if isinstance(user, AnonymousUserMixin):
            return False
        return any(
            (role in user_role.permissions or RoleEnum.ADMIN in user_role.permissions)
            and user_role.organisation_id is None
            and user_role.grant_id is None
            for user_role in user.roles
        )

    @staticmethod
    def is_deliver_org_admin(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        if AuthorisationHelper.is_platform_admin(user=user):
            return True
        return any(
            RoleEnum.ADMIN in role.permissions
            and role.organisation_id
            and role.grant_id is None
            and role.organisation.can_manage_grants
            for role in user.roles
        )

    @staticmethod
    def is_deliver_org_member(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        if AuthorisationHelper.is_platform_admin(user=user):
            return True
        return any(
            (RoleEnum.MEMBER in role.permissions or RoleEnum.ADMIN in role.permissions)
            and role.organisation_id
            and role.grant_id is None
            and role.organisation.can_manage_grants
            for role in user.roles
        )

    @staticmethod
    def is_deliver_grant_admin(grant_id: UUID, user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        if AuthorisationHelper.is_platform_admin(user=user):
            return True

        grant = get_grant(grant_id)

        for role in user.roles:
            if RoleEnum.ADMIN in role.permissions:
                # entire org admin
                if role.organisation_id == grant.organisation_id and role.grant_id is None:
                    return True
                # specific grant admin
                if role.organisation_id == grant.organisation_id and role.grant_id == grant_id:
                    return True

        return False

    @staticmethod
    def is_deliver_grant_member(grant_id: UUID, user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        if AuthorisationHelper.is_platform_admin(user=user):
            return True

        grant = get_grant(grant_id)

        for role in user.roles:
            if RoleEnum.MEMBER in role.permissions or RoleEnum.ADMIN in role.permissions:
                # entire org member
                if role.organisation_id == grant.organisation_id and role.grant_id is None:
                    return True
                # specific grant member
                if role.organisation_id == grant.organisation_id and role.grant_id == grant_id:
                    return True

        return False

    @staticmethod
    def has_deliver_grant_role(grant_id: UUID, role: RoleEnum, user: User | AnonymousUserMixin) -> bool:
        """
        Will return True if the user has the specified role for the grant.
        Platform admin overrides anything else.
        """
        if isinstance(user, AnonymousUserMixin):
            return False
        if AuthorisationHelper.is_platform_admin(user=user):
            return True
        match role:
            case RoleEnum.ADMIN:
                return AuthorisationHelper.is_deliver_grant_admin(grant_id, user)
            case RoleEnum.MEMBER:
                return AuthorisationHelper.is_deliver_grant_member(grant_id, user)
            case _:
                raise ValueError(f"Unknown role {role}")

    @staticmethod
    def has_access_grant_role(
        grant_recipient: "GrantRecipient", role: RoleEnum, user: User | AnonymousUserMixin
    ) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False
        match role:
            case RoleEnum.DATA_PROVIDER:
                return AuthorisationHelper.is_access_grant_data_provider(grant_recipient, user)
            case RoleEnum.CERTIFIER:
                return AuthorisationHelper.is_access_grant_certifier(grant_recipient, user)
            case RoleEnum.MEMBER:
                return AuthorisationHelper.is_access_grant_member(grant_recipient, user)
            case _:
                raise ValueError(f"Unknown role {role}")

    @staticmethod
    def is_deliver_grant_funding_user(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False

        if AuthorisationHelper.is_platform_admin(user):
            return True

        if any(role.organisation and role.organisation.can_manage_grants for role in user.roles):
            return True

        return False

    @staticmethod
    def can_replace_dataset(user: User | AnonymousUserMixin, collection_id: UUID) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False

        collection = get_collection(collection_id)
        if collection.is_closed:
            return False
        if AuthorisationHelper.is_platform_admin(user=user) or AuthorisationHelper.is_deliver_grant_admin(
            collection.grant_id, user
        ):
            return True

        return False

    @staticmethod
    def can_edit_collection(user: User | AnonymousUserMixin, collection_id: UUID) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False

        collection = get_collection(collection_id)
        if collection.is_editable_for_current_status and AuthorisationHelper.is_deliver_grant_admin(
            collection.grant_id, user
        ):
            return True

        if AuthorisationHelper.is_platform_admin(user=user):
            # TODO: Decision point
            return False

        return False

    @staticmethod
    def can_request_or_allow_changes(user: User, submission: "Submission | UUID") -> bool:
        # Platform admin should be able to reopen submissions
        # The grant team should be able to reopen submissions. Their roles would be:
        # - grant member
        # Form designers (deliver org members) should not be able to reopen live submissions
        if isinstance(submission, UUID):
            submission = get_submission(submission)

        if AuthorisationHelper.is_platform_admin(user):
            return True

        has_deliver_grant_role = AuthorisationHelper.has_deliver_grant_role(
            submission.collection.grant.id, RoleEnum.MEMBER, user
        )

        if submission.mode == SubmissionModeEnum.TEST:
            return has_deliver_grant_role

        is_deliver_org_member = AuthorisationHelper.is_deliver_org_member(user)

        return has_deliver_grant_role and not is_deliver_org_member

    @staticmethod
    def can_validate_submission(user: User, submission: Submission) -> bool:
        if AuthorisationHelper.is_platform_admin(user):
            return True

        has_deliver_grant_role = AuthorisationHelper.has_deliver_grant_role(
            submission.collection.grant.id, RoleEnum.MEMBER, user
        )

        if submission.mode == SubmissionModeEnum.TEST:
            return has_deliver_grant_role

        is_deliver_org_member = AuthorisationHelper.is_deliver_org_member(user)

        return has_deliver_grant_role and not is_deliver_org_member

    @staticmethod
    def has_access_org_access(user: User | AnonymousUserMixin, organisation_id: UUID) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False

        # TODO: agree with product and dev what the policy on access to platform admins should be
        #       assuming they should only have blanket access to test data when that exists
        # TODO: organisations are likely lazy loaded here, we should be careful with that and audit
        #       these before we're done
        return any(
            role.organisation_id == organisation_id and not role.organisation.can_manage_grants for role in user.roles
        )

    @staticmethod
    def _has_access_grant_permission(
        grant_recipient: "GrantRecipient", permission: RoleEnum, user: User | AnonymousUserMixin
    ) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False

        for role in user.roles:
            if permission in role.permissions:
                # entire org member
                if role.organisation_id == grant_recipient.organisation_id and role.grant_id is None:
                    return True
                # specific grant member
                if (
                    role.organisation_id == grant_recipient.organisation_id
                    and role.grant_id == grant_recipient.grant_id
                ):
                    return True
        return False

    @staticmethod
    def is_access_grant_data_provider(grant_recipient: "GrantRecipient", user: User | AnonymousUserMixin) -> bool:
        return AuthorisationHelper._has_access_grant_permission(grant_recipient, RoleEnum.DATA_PROVIDER, user)

    @staticmethod
    def is_access_grant_member(grant_recipient: "GrantRecipient", user: User | AnonymousUserMixin) -> bool:
        return AuthorisationHelper._has_access_grant_permission(grant_recipient, RoleEnum.MEMBER, user)

    @staticmethod
    def is_access_grant_certifier(grant_recipient: "GrantRecipient", user: User | AnonymousUserMixin) -> bool:
        return AuthorisationHelper._has_access_grant_permission(grant_recipient, RoleEnum.CERTIFIER, user)

    @staticmethod
    def has_access_grant_recipient_role(user: User | AnonymousUserMixin) -> bool:
        if isinstance(user, AnonymousUserMixin):
            return False

        if any(role.organisation and not role.organisation.can_manage_grants for role in user.roles):
            return True

        return False

    @staticmethod
    def is_deliver_user_testing_access(
        user: User | AnonymousUserMixin, *, user_organisation: Organisation | None = None
    ) -> bool:
        if not AuthorisationHelper.is_deliver_grant_funding_user(user):
            return False

        # if we've got a grant recipient context we can be specific for the organisation too
        # this is likely only possibly in lower envs as deliver users won't be applying for or reporting on grants
        if user_organisation and user_organisation.mode == OrganisationModeEnum.LIVE:
            return False

        return request.path.startswith("/access/")
