import uuid
from typing import cast

import sentry_sdk
from flask import Blueprint, abort, current_app, redirect, render_template, request, session, url_for
from flask.typing import ResponseReturnValue
from flask_login import login_user, logout_user

from app.common.auth.authorisation_helper import AuthorisationHelper
from app.common.auth.decorators import collection_is_open_for_sign_up, redirect_if_authenticated
from app.common.auth.forms import SignInForm
from app.common.auth.sso import MSAL_ERROR_AUTHORIZATION_CODE_WAS_ALREADY_REDEEMED, build_auth_code_flow, build_msal_app
from app.common.data import interfaces
from app.common.data.types import AuthMethodEnum, RoleEnum
from app.common.forms import GenericSubmitForm
from app.common.security.utils import sanitise_redirect_url
from app.extensions import auto_commit_after_request, notification_service

auth_blueprint = Blueprint(
    "auth",
    __name__,
    url_prefix="/",
)


@auth_blueprint.route(
    "/request-a-link-to-sign-in/<string:grant_slug>/<string:collection_slug>", methods=["GET", "POST"]
)
@collection_is_open_for_sign_up
@redirect_if_authenticated
@auto_commit_after_request
def collection_request_a_link_to_public_sign_up(grant_slug: str, collection_slug: str) -> ResponseReturnValue:
    link_expired = request.args.get("link_expired", False)

    grant = interfaces.grants.get_grant_by_slug(grant_slug)
    collection = interfaces.collections.get_collection_by_slug(grant.id, collection_slug)

    form = SignInForm(is_public_sign_in=True)
    if form.validate_on_submit():
        email = cast(str, form.email_address.data)

        user = interfaces.user.get_user_by_email(email_address=email)

        magic_link = interfaces.magic_link.create_magic_link(
            user=user,
            email=email,
            redirect_to_path=sanitise_redirect_url(
                url_for(
                    "access_grant_funding.eligible_to_apply",
                    grant_slug=grant_slug,
                    collection_slug=collection_slug,
                )
            ),
            collection=collection,
        )

        notification = notification_service.send_magic_link(
            email,
            magic_link_url=url_for("auth.claim_magic_link", magic_link_code=magic_link.code, _external=True),
            magic_link_expires_at_utc=magic_link.expires_at_utc,
            request_new_magic_link_url=url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug=grant_slug,
                collection_slug=collection_slug,
                _external=True,
            ),
        )
        session["magic_link_email_notification_id"] = notification.id
        session["magic_link_requested"] = True

        return redirect(url_for("auth.check_email", magic_link_id=magic_link.id))

    return render_template(
        "access_grant_funding/auth/public_sign_up_magic_link.html",
        form=form,
        link_expired=link_expired,
        grant=grant,
        collection=collection,
    )


@auth_blueprint.route("/request-a-link-to-sign-in", methods=["GET", "POST"])
@redirect_if_authenticated
@auto_commit_after_request
def request_a_link_to_sign_in() -> ResponseReturnValue:
    form = SignInForm()
    link_expired = request.args.get("link_expired", False)
    if form.validate_on_submit():
        email = cast(str, form.email_address.data)

        internal_domains = current_app.config["INTERNAL_DOMAINS"]
        if email.endswith(internal_domains):
            # upgrade authorisation requests for internal users to make sure they
            # still have valid access to their admin account
            session["next"] = sanitise_redirect_url(session.get("next", url_for("access_grant_funding.index")))
            session["flow"] = build_auth_code_flow(scopes=current_app.config["MS_GRAPH_PERMISSIONS_SCOPE"])
            return redirect(session["flow"]["auth_uri"]), 302

        user = interfaces.user.get_user_by_email(email_address=email)

        magic_link = interfaces.magic_link.create_magic_link(
            user=user,
            email=email,
            redirect_to_path=sanitise_redirect_url(session.pop("next", url_for("access_grant_funding.index"))),
        )

        notification = notification_service.send_magic_link(
            email,
            magic_link_url=url_for("auth.claim_magic_link", magic_link_code=magic_link.code, _external=True),
            magic_link_expires_at_utc=magic_link.expires_at_utc,
            request_new_magic_link_url=url_for("auth.request_a_link_to_sign_in", _external=True),
        )
        session["magic_link_email_notification_id"] = notification.id
        session["magic_link_requested"] = True

        return redirect(url_for("auth.check_email", magic_link_id=magic_link.id))

    return render_template(
        "access_grant_funding/auth/sign_in_magic_link.html",
        form=form,
        link_expired=link_expired,
        service_desk_url=current_app.config["ACCESS_SERVICE_DESK_URL"],
    )


@auth_blueprint.get("/check-your-email/<uuid:magic_link_id>")
@redirect_if_authenticated
def check_email(magic_link_id: uuid.UUID) -> ResponseReturnValue:
    magic_link = interfaces.magic_link.get_magic_link(id_=magic_link_id)
    if not magic_link or not magic_link.is_usable:
        return abort(404)

    notification_id = session.pop("magic_link_email_notification_id", None)
    return render_template(
        "access_grant_funding/auth/check_email.html",
        email=magic_link.email,
        notification_id=notification_id,
        collection=magic_link.collection,
    )


@auth_blueprint.route("/sign-in/<magic_link_code>", methods=["GET", "POST"])
@redirect_if_authenticated
@auto_commit_after_request
def claim_magic_link(magic_link_code: str) -> ResponseReturnValue:
    magic_link = interfaces.magic_link.get_magic_link(code=magic_link_code)
    if not magic_link or not magic_link.is_usable:
        if magic_link:
            session["next"] = magic_link.redirect_to_path

            if magic_link.collection:
                return redirect(
                    url_for(
                        "auth.collection_request_a_link_to_public_sign_up",
                        link_expired=True,
                        grant_slug=magic_link.collection.grant.slug,
                        collection_slug=magic_link.collection.slug,
                    )
                )

        return redirect(
            url_for(
                "auth.request_a_link_to_sign_in",
                link_expired=True,
            )
        )

    form = GenericSubmitForm()
    if form.validate_on_submit():
        user = magic_link.user
        if not user:
            user = interfaces.user.upsert_user_by_email(email_address=str(magic_link.email))
        interfaces.magic_link.claim_magic_link(magic_link=magic_link, user=user)
        if not login_user(user):
            return abort(400)

        session["auth"] = AuthMethodEnum.MAGIC_LINK

        if magic_link.collection:
            session["signing_up_for_collection_id"] = magic_link.collection_id
        else:
            session.pop("signing_up_for_collection_id", None)

        auto_submit = session.pop("magic_link_requested", False)
        current_app.logger.info(
            "Magic link claim page submitted: auto_submit=%(auto_submit)s",
            dict(auto_submit=auto_submit),
        )
        return redirect(sanitise_redirect_url(magic_link.redirect_to_path))

    auto_submit = session.get("magic_link_requested", False)
    return render_template(
        "access_grant_funding/auth/claim_magic_link.html", form=form, magic_link=magic_link, auto_submit=auto_submit
    )


@auth_blueprint.route("/sso/permissions-error", methods=["GET", "POST"])
def signed_in_but_no_permissions() -> ResponseReturnValue:
    return render_template(
        "common/auth/mhclg-user-not-authorised.html",
        service_desk_url=current_app.config["DELIVER_SERVICE_DESK_URL"],
        invite_expired=str(request.args.get("invite_expired", False)) == "True",
    ), 403


@auth_blueprint.route("/sso/sign-in", methods=["GET", "POST"])
@redirect_if_authenticated
def sso_sign_in() -> ResponseReturnValue:
    form = GenericSubmitForm()
    if form.validate_on_submit():
        session["flow"] = build_auth_code_flow(scopes=current_app.config["MS_GRAPH_PERMISSIONS_SCOPE"])
        return redirect(session["flow"]["auth_uri"]), 302
    return render_template("common/auth/sign_in_sso.html", form=form)


@auth_blueprint.route("/sso/get-token", methods=["GET"])
@redirect_if_authenticated
@auto_commit_after_request
def sso_get_token() -> ResponseReturnValue:
    result = build_msal_app().acquire_token_by_auth_code_flow(session.get("flow", {}), request.args)
    if "error" in result:
        if MSAL_ERROR_AUTHORIZATION_CODE_WAS_ALREADY_REDEEMED in result.get("error_codes", []):
            current_app.logger.warning(
                "Authorization code was already redeemed: %(error)s.", {"error": result.get("error_description")}
            )
            return redirect(url_for("auth.sign_out"))
        return abort(500, f"Azure AD get-token flow failed with: {result}")

    sso_user = result["id_token_claims"]

    # Does the user exist already?
    user = interfaces.user.get_user_by_azure_ad_subject_id(azure_ad_subject_id=sso_user["sub"])

    # Platform admin route
    if "FS_PLATFORM_ADMIN" in sso_user.get("roles", []):
        user = interfaces.user.upsert_user_and_set_platform_admin_role(
            azure_ad_subject_id=sso_user["sub"], email_address=sso_user["preferred_username"], name=sso_user["name"]
        )

    # Invitations route
    elif user is None:
        user_invites = interfaces.user.get_invitations_by_email(email=sso_user["preferred_username"])
        if not any(invite.is_usable for invite in user_invites):
            return redirect(
                url_for("auth.signed_in_but_no_permissions", invite_expired=len(user_invites) > 0),
            )
        user = interfaces.user.create_user_and_claim_invitations(
            azure_ad_subject_id=sso_user["sub"],
            email_address=sso_user["preferred_username"],
            name=sso_user["name"],
        )

    # Existing User with roles route
    elif user and user.roles:
        user = interfaces.user.upsert_user_by_azure_ad_subject_id(
            azure_ad_subject_id=sso_user["sub"],
            email_address=sso_user["preferred_username"],
            name=sso_user["name"],
        )
        # if a user was previously a platform admin and we haven't received "FS_PLATFORM_ADMIN" in their active roles
        # for this sign-in we should remove the role that gives the platform admin permissions to all grants
        if AuthorisationHelper.is_platform_admin(user):
            interfaces.user.remove_permissions_from_user(
                user, permissions=[RoleEnum.MEMBER, RoleEnum.ADMIN], organisation_id=None, grant_id=None
            )
            if not user.roles:
                return redirect(
                    url_for("auth.signed_in_but_no_permissions", invite_expired=False),
                )

    # No user and no roles means they should 403 for now
    else:
        return redirect(
            url_for("auth.signed_in_but_no_permissions", invite_expired=False),
        )

    # For all other valid users with roles after the above, finish the flow and redirect
    redirect_to_path = sanitise_redirect_url(session.pop("next", url_for("index")))
    session.pop("flow", None)

    if not login_user(user):
        return abort(400)

    session["auth"] = AuthMethodEnum.SSO

    return redirect(redirect_to_path)


@auth_blueprint.get("/sign-out")
def sign_out() -> ResponseReturnValue:
    logout_user()
    sentry_sdk.set_user(None)

    auth_method = session.pop("auth", None)

    if collection_id := session.pop("signing_up_for_collection_id", None):
        collection = interfaces.collections.get_collection(collection_id)
        return redirect(
            url_for(
                "access_grant_funding.public_sign_up_start_page",
                grant_slug=collection.grant.slug,
                collection_slug=collection.slug,
            )
        )

    match auth_method:
        case AuthMethodEnum.SSO:
            return redirect(url_for("auth.sso_sign_in"))
        case AuthMethodEnum.MAGIC_LINK:
            return redirect(url_for("auth.request_a_link_to_sign_in"))
        case _:
            return redirect(url_for("index"))
