from os import environ

from django.db import transaction

from authentik.brands.models import Brand
from authentik.common.oauth.constants import SubModes
from authentik.core.models import Application, Group, User
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow, FlowStageBinding
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import (
    ClientTypes,
    IssuerMode,
    OAuth2LogoutMethod,
    OAuth2Provider,
    RedirectURI,
    RedirectURIMatchingMode,
    ScopeMapping,
)
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.password.models import PasswordStage

GROUP_SPECS = [
    ("nodedc:superadmin", False),
    ("nodedc:launcher:admin", False),
    ("nodedc:launcher:user", False),
    ("nodedc:taskmanager:admin", False),
    ("nodedc:taskmanager:user", False),
    ("nodedc:bim:access", False),
    # Module Foundry roles travel through the existing Authentik `groups`
    # claim and Launcher handoff. `access` remains a backward-compatible
    # member role until every existing assignment is migrated.
    ("nodedc:module-foundry:admin", False),
    ("nodedc:module-foundry:user", False),
    ("nodedc:module-foundry:blocked", False),
    ("nodedc:module-foundry:access", False),
]

APP_SPECS = [
    {
        "slug": "launcher",
        "name": "NODE.DC Launcher",
        "provider_name": "NODE.DC Launcher OIDC",
        "client_id_env": "LAUNCHER_OIDC_CLIENT_ID",
        "client_secret_env": "LAUNCHER_OIDC_CLIENT_SECRET",
        "redirect_uri_env": "LAUNCHER_OIDC_REDIRECT_URI",
        "logged_out_redirect_uri_env": "LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI",
        "default_logged_out_redirect_uri": "http://launcher.local.nodedc/auth/logged-out",
        "launch_url_env": "LAUNCHER_BASE_URL",
        "launch_url": "http://launcher.local.nodedc",
        "logout_uri_env": "LAUNCHER_LOGOUT_URI",
        "logout_uri": "http://launcher.local.nodedc/logout",
        "groups": ["nodedc:superadmin", "nodedc:launcher:admin", "nodedc:launcher:user"],
        "description": "NODE.DC control plane launcher.",
    },
    {
        "slug": "task-manager",
        "name": "NODE.DC Task Manager",
        "provider_name": "NODE.DC Task Manager OIDC",
        "client_id_env": "PLANE_OIDC_CLIENT_ID",
        "client_secret_env": "PLANE_OIDC_CLIENT_SECRET",
        "redirect_uri_env": "PLANE_OIDC_REDIRECT_URI",
        "launch_url_env": "TASK_BASE_URL",
        "launch_url": "http://task.local.nodedc",
        "logout_uri_env": "TASK_LOGOUT_URI",
        "logout_uri": "http://task.local.nodedc/logout",
        "groups": ["nodedc:superadmin", "nodedc:taskmanager:admin", "nodedc:taskmanager:user"],
        "description": "NODE.DC Plane-based task manager.",
    },
]


def required_env(name):
    value = environ.get(name, "").strip()
    if not value:
        raise RuntimeError(f"{name} is required")
    return value


def optional_env(name, default=""):
    return environ.get(name, default).strip()


def ensure_group(name, is_superuser=False):
    group, _ = Group.objects.get_or_create(name=name)
    group.is_superuser = is_superuser
    group.save()
    return group


def ensure_groups():
    groups = {}
    for name, is_superuser in GROUP_SPECS:
        groups[name] = ensure_group(name, is_superuser)
    return groups


def ensure_user_groups(groups):
    admin_email = (
        environ.get("NODEDC_BOOTSTRAP_ADMIN_EMAIL", "").strip()
        or environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "").strip()
    )
    if not admin_email:
        return None

    user = User.objects.filter(email__iexact=admin_email).first() or User.objects.filter(
        username=admin_email
    ).first()
    if user is None:
        user = User(username=admin_email, email=admin_email, name=admin_email, type="internal")

    user.username = admin_email
    user.email = admin_email
    user.is_active = True
    user.type = "internal"
    admin_password = (
        environ.get("NODEDC_BOOTSTRAP_ADMIN_PASSWORD")
        or environ.get("AUTHENTIK_BOOTSTRAP_PASSWORD")
    )
    if admin_password:
        user.set_password(admin_password)
    user.save()

    authentik_admins, _ = Group.objects.get_or_create(name="authentik Admins")
    if not authentik_admins.is_superuser:
        authentik_admins.is_superuser = True
        authentik_admins.save(update_fields=["is_superuser"])
    user.groups.add(authentik_admins)

    for name in groups:
        # The bootstrap owner must remain capable of entering Foundry after
        # the first provisioning run. Blocking is an explicit operator action,
        # never a default membership of the bootstrap principal.
        if name != "nodedc:module-foundry:blocked":
            user.groups.add(groups[name])
    return user


def ensure_groups_scope_mapping():
    mapping, _ = ScopeMapping.objects.get_or_create(
        name="NODE.DC OAuth Mapping: groups",
        defaults={
            "scope_name": "groups",
            "description": "Adds Authentik group names to NODE.DC OIDC tokens.",
            "expression": 'return {"groups": [group.name for group in request.user.groups.all()]}',
        },
    )
    mapping.scope_name = "groups"
    mapping.description = "Adds Authentik group names to NODE.DC OIDC tokens."
    mapping.expression = 'return {"groups": [group.name for group in request.user.groups.all()]}'
    mapping.save()
    return mapping


def ensure_profile_scope_mapping():
    expression = """
attributes = request.user.attributes or {}
display_name = request.user.name or request.user.username
name_parts = display_name.split(" ", 1)
avatar_url = attributes.get("picture") or attributes.get("avatar_url") or attributes.get("avatar")

return {
    "name": display_name,
    "given_name": name_parts[0] if name_parts else display_name,
    "family_name": name_parts[1] if len(name_parts) > 1 else "",
    "preferred_username": request.user.username,
    "nickname": request.user.username,
    "picture": avatar_url,
    "avatar_url": avatar_url,
}
""".strip()
    mapping, _ = ScopeMapping.objects.get_or_create(
        name="NODE.DC OAuth Mapping: profile context",
        defaults={
            "scope_name": "profile",
            "description": "Adds normalized NODE.DC profile claims to OIDC tokens.",
            "expression": expression,
        },
    )
    mapping.scope_name = "profile"
    mapping.description = "Adds normalized NODE.DC profile claims to OIDC tokens."
    mapping.expression = expression
    mapping.save()
    return mapping


def default_scope_mappings():
    scope_names = ["openid", "email", "offline_access"]
    mappings = list(ScopeMapping.objects.filter(scope_name__in=scope_names))
    mappings.append(ensure_profile_scope_mapping())
    mappings.append(ensure_groups_scope_mapping())
    return mappings


def ensure_nodedc_brand():
    auth_domain = environ.get("AUTH_DOMAIN", "auth.local.nodedc").strip() or "auth.local.nodedc"
    authentication_flow = Flow.objects.get(slug="default-authentication-flow")
    invalidation_flow = Flow.objects.get(slug="default-invalidation-flow")

    authentication_flow.name = "NODE.DC authentication"
    authentication_flow.title = "Работайте во всех измерениях."
    authentication_flow.layout = "stacked"
    authentication_flow.background = ""
    authentication_flow.save()

    identification_stage = IdentificationStage.objects.get(
        name="default-authentication-identification"
    )
    password_stage = PasswordStage.objects.get(name="default-authentication-password")
    password_stage.allow_show_password = True
    password_stage.save()
    identification_stage.user_fields = ["email"]
    identification_stage.password_stage = password_stage
    identification_stage.show_matched_user = False
    identification_stage.enable_remember_me = False
    identification_stage.save()
    FlowStageBinding.objects.filter(target=authentication_flow, stage=password_stage).delete()

    brand = Brand.objects.filter(domain=auth_domain).first()
    if brand is None:
        brand = Brand(domain=auth_domain)

    Brand.objects.exclude(brand_uuid=brand.brand_uuid).update(default=False)
    brand.default = True
    brand.domain = auth_domain
    brand.branding_title = "NODE.DC"
    brand.branding_logo = ""
    brand.branding_favicon = ""
    # Login styling is injected by flow templates only. Brand custom CSS is global
    # and Authentik applies it to Admin/User web components as well.
    brand.branding_custom_css = ""
    brand.flow_authentication = authentication_flow
    brand.flow_invalidation = invalidation_flow
    brand.attributes = {
        **(brand.attributes or {}),
        "settings": {
            **((brand.attributes or {}).get("settings") or {}),
            "locale": "ru",
            "theme": {
                **(((brand.attributes or {}).get("settings") or {}).get("theme") or {}),
                "base": "dark",
            },
        },
    }
    brand.save()
    return brand


def ensure_provider(spec, mappings):
    authorization_flow = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
    invalidation_flow = Flow.objects.get(slug="default-invalidation-flow")
    signing_key = (
        CertificateKeyPair.objects.filter(name="authentik Self-signed Certificate").first()
        or CertificateKeyPair.objects.first()
    )

    if signing_key is None:
        raise RuntimeError("No Authentik CertificateKeyPair exists for OIDC signing")

    provider = OAuth2Provider.objects.filter(name=spec["provider_name"]).first()
    if provider is None:
        provider = OAuth2Provider(name=spec["provider_name"])

    provider.name = spec["provider_name"]
    provider.client_type = ClientTypes.CONFIDENTIAL
    provider.client_id = required_env(spec["client_id_env"])
    provider.client_secret = required_env(spec["client_secret_env"])
    redirect_uri_values = [required_env(spec["redirect_uri_env"])]
    logged_out_redirect_uri = optional_env(
        spec.get("logged_out_redirect_uri_env", ""),
        spec.get("default_logged_out_redirect_uri", ""),
    )

    if logged_out_redirect_uri:
        redirect_uri_values.append(logged_out_redirect_uri)

    provider.redirect_uris = [
        RedirectURI(RedirectURIMatchingMode.STRICT, redirect_uri)
        for redirect_uri in dict.fromkeys(redirect_uri_values)
    ]
    provider.logout_uri = optional_env(spec.get("logout_uri_env", ""), spec["logout_uri"])
    provider.logout_method = OAuth2LogoutMethod.FRONTCHANNEL
    provider.include_claims_in_id_token = True
    provider.sub_mode = SubModes.USER_UUID
    provider.issuer_mode = IssuerMode.PER_PROVIDER
    provider.authorization_flow = authorization_flow
    provider.invalidation_flow = invalidation_flow
    provider.signing_key = signing_key
    provider.save()
    provider.property_mappings.set(mappings)
    return provider


def ensure_application(spec, provider, groups):
    application = Application.objects.filter(slug=spec["slug"]).first()
    if application is None:
        application = Application(slug=spec["slug"], name=spec["name"])

    application.name = spec["name"]
    application.slug = spec["slug"]
    application.group = "NODE.DC"
    application.provider = provider
    application.meta_launch_url = optional_env(spec.get("launch_url_env", ""), spec["launch_url"])
    application.meta_description = spec["description"]
    application.meta_publisher = "NODE.DC"
    application.open_in_new_tab = False
    application.policy_engine_mode = "any"
    application.save()

    PolicyBinding.objects.filter(target=application).exclude(
        group__name__in=spec["groups"]
    ).delete()
    for order, group_name in enumerate(spec["groups"]):
        binding = PolicyBinding.objects.filter(target=application, group=groups[group_name]).first()
        if binding is None:
            binding = PolicyBinding(target=application, group=groups[group_name])
        binding.enabled = True
        binding.negate = False
        binding.timeout = 30
        binding.failure_result = False
        binding.order = order
        binding.save()

    return application


@transaction.atomic
def main():
    brand = ensure_nodedc_brand()
    groups = ensure_groups()
    user = ensure_user_groups(groups)
    mappings = default_scope_mappings()
    applications = []
    providers = []

    for spec in APP_SPECS:
        provider = ensure_provider(spec, mappings)
        application = ensure_application(spec, provider, groups)
        providers.append(provider)
        applications.append(application)

    summary = {
        "groups": list(groups),
        "admin_user": user.email if user else None,
        "brand": brand.domain,
        "applications": [application.slug for application in applications],
        "providers": [provider.name for provider in providers],
    }
    print(summary)


main()
