from __future__ import annotations

from django.db import transaction
from django.utils import timezone

from apps.audit import services as audit
from apps.modules.models import Module, ModuleDependency, ModuleRequest, OrganizationModule
from apps.modules.registry import REGISTRY
from domain.access_policy import CORE_MODULE
from domain.exceptions import (
    ModuleDependencyError,
    ModuleNotAvailableError,
    OrganizationStateError,
    UnknownModuleError,
)


def enabled_module_codes(organization) -> set[str]:
    """Modules usable by this organization. `core` is always on."""
    codes = set(
        OrganizationModule.objects.filter(organization=organization, is_enabled=True).values_list(
            "module_id", flat=True
        )
    )
    codes.add(CORE_MODULE)
    return codes


def is_module_enabled(organization, module_code: str) -> bool:
    return module_code in enabled_module_codes(organization)


@transaction.atomic
def sync_registry() -> dict[str, int]:
    """Mirror the code registry into modules / dependencies / permissions (idempotent)."""
    from apps.access.models import Permission  # local import: access depends on modules

    created_modules = 0
    for spec in REGISTRY.values():
        _, created = Module.objects.update_or_create(
            code=spec.code,
            defaults={
                "title_fa": spec.title_fa,
                "description_fa": spec.description_fa,
                "status": spec.status,
                "is_core": spec.is_core,
                "sort_order": spec.sort_order,
            },
        )
        created_modules += int(created)

    for spec in REGISTRY.values():
        wanted = set(spec.depends_on)
        existing = set(
            ModuleDependency.objects.filter(module_id=spec.code).values_list("depends_on_id", flat=True)
        )
        for code in wanted - existing:
            ModuleDependency.objects.create(module_id=spec.code, depends_on_id=code)
        ModuleDependency.objects.filter(module_id=spec.code, depends_on_id__in=existing - wanted).delete()

    created_permissions = 0
    for spec in REGISTRY.values():
        for perm in spec.permissions:
            _, created = Permission.objects.update_or_create(
                code=perm.code,
                defaults={
                    "module_id": spec.code,
                    "action": perm.action,
                    "name_fa": perm.name_fa,
                    "is_platform_only": perm.platform_only,
                },
            )
            created_permissions += int(created)
            if created and not perm.platform_only and not spec.is_core:
                _grant_new_permission_to_managers(spec.code, perm.code)
    sync_super_admin_grants()
    return {"modules_created": created_modules, "permissions_created": created_permissions}


def sync_super_admin_grants() -> int:
    """The platform super admin role is "every permission, everywhere": give it the permissions that
    were added after the role was seeded (missing ones only, existing grants are never changed)."""
    from apps.access.models import Permission, Role, RolePermission

    added = 0
    for role in Role.objects.filter(code="super_admin", organization__kind="platform"):
        have = set(RolePermission.objects.filter(role=role).values_list("permission_id", flat=True))
        for permission in Permission.objects.exclude(pk__in=have):
            RolePermission.objects.create(role=role, permission=permission, scope="all", can_delegate=True)
            added += 1
    return added


def _grant_new_permission_to_managers(module_code: str, permission_code: str) -> None:
    """A permission added to a module later reaches the standard manager role of every organization
    that already uses the module (other roles stay untouched; managers can adjust afterwards)."""
    from apps.access.models import Permission, Role, RolePermission

    permission = Permission.objects.get(code=permission_code)
    org_ids = OrganizationModule.objects.filter(module_id=module_code, enabled_at__isnull=False).values_list("organization_id", flat=True)
    for role in Role.objects.filter(organization_id__in=org_ids, code="org_manager", is_system=True):
        RolePermission.objects.get_or_create(
            role=role, permission=permission, defaults={"scope": "organization", "can_delegate": True}
        )


def request_module(*, actor, organization, module_code: str, request=None) -> ModuleRequest:
    """A manager asks for a module to be enabled for their organization (the super admin decides)."""
    if organization.is_platform:
        raise OrganizationStateError("Modules are enabled per customer organization only.")
    module = _get_module(module_code)
    if module.is_core or module.status != Module.Status.AVAILABLE:
        raise ModuleNotAvailableError(f"Module '{module_code}' cannot be requested.")
    if module_code in enabled_module_codes(organization):
        raise ModuleNotAvailableError("This module is already enabled.")
    if ModuleRequest.objects.filter(
        organization=organization, module=module, status=ModuleRequest.Status.PENDING
    ).exists():
        raise ModuleNotAvailableError("A request for this module is already waiting for approval.")
    row = ModuleRequest.objects.create(organization=organization, module=module, requested_by=actor)
    audit.record(
        actor=actor, action="module.requested", organization=organization, entity_type="module",
        entity_id=module_code, request=request,
    )
    return row


@transaction.atomic
def decide_module_request(*, actor, module_request: ModuleRequest, approve: bool, note: str = "", request=None):
    if module_request.status != ModuleRequest.Status.PENDING:
        raise ModuleNotAvailableError("This request was already decided.")
    if approve:
        enable_module(
            actor=actor, organization=module_request.organization,
            module_code=module_request.module_id, request=request,
        )
    module_request.status = ModuleRequest.Status.APPROVED if approve else ModuleRequest.Status.REJECTED
    module_request.note = (note or "").strip()[:500]
    module_request.decided_by = actor
    module_request.decided_at = timezone.now()
    module_request.save()
    audit.record(
        actor=actor, action="module.request_approved" if approve else "module.request_rejected",
        organization=module_request.organization, entity_type="module", entity_id=module_request.module_id,
        after={"note": module_request.note}, request=request,
    )
    return module_request


def _get_module(code: str) -> Module:
    try:
        return Module.objects.get(code=code)
    except Module.DoesNotExist:
        raise UnknownModuleError(f"Unknown module '{code}'.")


@transaction.atomic
def _grant_default_permissions(organization, module_code: str) -> None:
    """First enablement only: hand the module's permissions to the standard roles. Afterwards
    the roles are ordinary data that managers may change; we never overwrite them again."""
    from apps.access.models import Permission, Role, RolePermission
    from apps.modules.registry import REGISTRY

    spec = REGISTRY.get(module_code)
    if spec is None:
        return
    plans = {"org_manager": [(p.code, "organization", True) for p in spec.permissions if not p.platform_only],
             "employee": [(code, scope, False) for code, scope in spec.employee_grants]}
    for role_code, grants in plans.items():
        role = Role.objects.filter(organization=organization, code=role_code, is_system=True).first()
        if role is None:
            continue
        for code, scope, delegate in grants:
            permission = Permission.objects.filter(code=code).first()
            if permission:
                RolePermission.objects.get_or_create(
                    role=role, permission=permission, defaults={"scope": scope, "can_delegate": delegate}
                )


def enable_module(*, actor, organization, module_code: str, request=None) -> OrganizationModule:
    if organization.is_platform:
        raise OrganizationStateError("Modules are enabled per customer organization only.")
    if not organization.is_active:
        raise OrganizationStateError("The organization is inactive.")

    module = _get_module(module_code)
    if module.is_core:
        raise ModuleNotAvailableError("Core is always enabled.")
    if module.status != Module.Status.AVAILABLE:
        raise ModuleNotAvailableError(f"Module '{module_code}' is not available yet.")

    enabled = enabled_module_codes(organization)
    missing = sorted(
        ModuleDependency.objects.filter(module=module)
        .exclude(depends_on_id__in=enabled)
        .values_list("depends_on_id", flat=True)
    )
    if missing:
        raise ModuleDependencyError(
            f"Enable these modules first: {', '.join(missing)}", modules=missing
        )

    state, _ = OrganizationModule.objects.get_or_create(organization=organization, module=module)
    before = {"is_enabled": state.is_enabled}
    if not state.is_enabled:
        if state.enabled_at is None:
            _grant_default_permissions(organization, module_code)
        sync_super_admin_grants()
        state.is_enabled = True
        state.enabled_at = timezone.now()
        state.changed_by = actor
        state.save()
        audit.record(
            actor=actor,
            action="module.enabled",
            organization=organization,
            entity_type="module",
            entity_id=module_code,
            before=before,
            after={"is_enabled": True},
            request=request,
        )
    return state


@transaction.atomic
def disable_module(*, actor, organization, module_code: str, request=None) -> OrganizationModule:
    module = _get_module(module_code)
    if module.is_core:
        raise ModuleNotAvailableError("Core cannot be disabled.")

    enabled = enabled_module_codes(organization)
    blockers = sorted(
        ModuleDependency.objects.filter(depends_on=module, module_id__in=enabled).values_list(
            "module_id", flat=True
        )
    )
    if blockers:
        raise ModuleDependencyError(
            f"Disable these dependent modules first: {', '.join(blockers)}", modules=blockers
        )

    state, _ = OrganizationModule.objects.get_or_create(organization=organization, module=module)
    if state.is_enabled:
        state.is_enabled = False
        state.disabled_at = timezone.now()
        state.changed_by = actor
        state.save()
        audit.record(
            actor=actor,
            action="module.disabled",
            organization=organization,
            entity_type="module",
            entity_id=module_code,
            before={"is_enabled": True},
            after={"is_enabled": False},
            request=request,
        )
    return state


