"""DMS permissions bounded context — models.

Entities
--------
ACLPolicy
    A named, reusable access-control policy attached to a DMS resource
    (repository, folder, or document).  A policy defines WHO can do WHAT
    on WHICH resource via its ACLRule set.

ACLRule
    A single principal → action mapping inside an ACLPolicy.
    Principal can be a specific user, an IAM role, an org-node, or EVERYONE
    (all active tenant members).  Effect is ALLOW or DENY; deny-wins conflict
    resolution: DENY always overrides ALLOW for the same action.

DocumentPermission
    A lightweight, direct permission grant/denial on a *specific* document.
    Use this when a one-off override is needed without creating a full policy.
    Takes precedence over inherited policy rules (highest priority tier).
    Supports optional expiry and an inheritance flag for sub-resources.

ShareLink
    A signed, expiring token that grants limited access to a document to
    external parties (no Simorgh account required).
    Controls: download flag, preview flag, expiry, max-use count, optional
    password.  The token is a UUID generated at creation and never changed.

Design decisions
----------------
Inheritance model
    ACLPolicy.inheritable = True means the policy cascades to sub-folders
    and documents below the attached resource.  Resolution walks up the
    folder/repository hierarchy; DocumentPermission always wins.

Deny-wins conflict resolution
    When the same (principal, action) appears as both ALLOW and DENY across
    all applicable rules, DENY takes precedence.  Higher ``priority`` rules
    are evaluated first within a single policy.

Principal polymorphism
    principal_id is a free CharField.  Callers store:
      USER      → str(user.pk)  (integer id)
      ROLE      → role.slug
      ORG_NODE  → str(org_node.pk)
      EVERYONE  → "" (empty string)

Future ABAC
    ``ACLRule`` has no condition field in this phase.  A future ``conditions``
    JSONField can be added without breaking the existing schema.
"""

from __future__ import annotations

import uuid
from typing import ClassVar

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from simorgh.core.db.scoped import ScopedSoftDeleteManager
from simorgh.core.models import AuditedModel, SoftDeleteModel, TenantScopedModel, UUIDModel


# ---------------------------------------------------------------------------
# Choices
# ---------------------------------------------------------------------------

class ACLSubjectType(models.TextChoices):
    REPOSITORY = "repository", _("Repository")
    FOLDER = "folder", _("Folder")
    DOCUMENT = "document", _("Document")


class ACLPrincipalType(models.TextChoices):
    USER = "user", _("User")
    ROLE = "role", _("IAM Role")
    ORG_NODE = "org_node", _("Organisation Node")
    EVERYONE = "everyone", _("Everyone (tenant members)")


class ACLAction(models.TextChoices):
    VIEW = "view", _("View")
    DOWNLOAD = "download", _("Download")
    CREATE = "create", _("Create")
    UPDATE = "update", _("Update")
    DELETE = "delete", _("Delete")
    PUBLISH = "publish", _("Publish")
    MANAGE = "manage", _("Manage (admin)")
    SHARE = "share", _("Share / create links")


class ACLEffect(models.TextChoices):
    ALLOW = "allow", _("Allow")
    DENY = "deny", _("Deny")


# ---------------------------------------------------------------------------
# ACLPolicy
# ---------------------------------------------------------------------------

class ACLPolicy(UUIDModel, TenantScopedModel, AuditedModel, SoftDeleteModel):
    """A named access-control policy attached to one DMS resource.

    The ``subject_type`` + ``subject_id`` pair identifies the resource.
    ``subject_id`` holds the ``public_id`` (UUID hex) of the resource.
    When ``inheritable`` is True the policy cascades to every descendant
    folder / document below the attached resource.
    """

    name = models.CharField(_("name"), max_length=200)
    description = models.TextField(_("description"), blank=True, default="")

    subject_type = models.CharField(
        _("subject type"),
        max_length=20,
        choices=ACLSubjectType.choices,
    )
    subject_id = models.CharField(
        _("subject id"),
        max_length=40,
        help_text=_("public_id (UUID) of the resource this policy is attached to."),
    )
    inheritable = models.BooleanField(
        _("inheritable"),
        default=True,
        help_text=_("Whether sub-folders and documents inherit this policy."),
    )
    is_active = models.BooleanField(_("active"), default=True)

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("ACL policy")
        verbose_name_plural = _("ACL policies")
        ordering = ("subject_type", "name")
        indexes = [
            models.Index(
                fields=("tenant", "subject_type", "subject_id", "is_deleted"),
                name="dms_aclpolicy_subject_idx",
            ),
            models.Index(
                fields=("tenant", "is_active", "is_deleted"),
                name="dms_aclpolicy_active_idx",
            ),
        ]

    def __str__(self) -> str:
        return f"{self.name} → {self.subject_type}:{self.subject_id}"


# ---------------------------------------------------------------------------
# ACLRule
# ---------------------------------------------------------------------------

class ACLRule(UUIDModel, TenantScopedModel, AuditedModel, SoftDeleteModel):
    """A single principal → action permission rule inside an ACLPolicy.

    Multiple rules in the same policy combine with OR semantics per action;
    DENY always overrides ALLOW at the resolver level (deny-wins).
    ``priority`` is used only for ordering within a single policy display;
    conflict resolution is deny-wins regardless of priority.
    """

    policy = models.ForeignKey(
        ACLPolicy,
        on_delete=models.CASCADE,
        related_name="rules",
        verbose_name=_("policy"),
    )
    principal_type = models.CharField(
        _("principal type"),
        max_length=20,
        choices=ACLPrincipalType.choices,
    )
    principal_id = models.CharField(
        _("principal id"),
        max_length=40,
        blank=True,
        default="",
        help_text=_("Empty for EVERYONE.  User PK / role slug / org_node PK as string."),
    )
    action = models.CharField(
        _("action"),
        max_length=20,
        choices=ACLAction.choices,
    )
    effect = models.CharField(
        _("effect"),
        max_length=10,
        choices=ACLEffect.choices,
        default=ACLEffect.ALLOW,
    )
    priority = models.PositiveSmallIntegerField(
        _("priority"),
        default=0,
        help_text=_("Higher value = displayed/evaluated first within the same policy."),
    )

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("ACL rule")
        verbose_name_plural = _("ACL rules")
        ordering = ("-priority", "action")
        constraints = [
            models.UniqueConstraint(
                fields=("policy", "principal_type", "principal_id", "action"),
                condition=models.Q(is_deleted=False),
                name="dms_aclrule_unique_per_policy_principal_action",
            ),
        ]
        indexes = [
            models.Index(
                fields=("policy", "effect", "is_deleted"),
                name="dms_aclrule_policy_effect_idx",
            ),
        ]

    def __str__(self) -> str:
        return (
            f"{self.effect.upper()} {self.action} "
            f"for {self.principal_type}:{self.principal_id or '*'}"
        )


# ---------------------------------------------------------------------------
# DocumentPermission
# ---------------------------------------------------------------------------

class DocumentPermission(UUIDModel, TenantScopedModel, AuditedModel, SoftDeleteModel):
    """A direct permission grant/denial on a specific document.

    Highest-priority tier in the resolution chain — always evaluated before
    any inherited ACLPolicy rules.  Use this for one-off overrides.

    The ``expires_at`` field allows temporary grants (e.g. external auditor
    access for 30 days).  The service layer silently skips expired entries
    during resolution but does NOT auto-delete them (keeps audit trail).

    When ``inheritable`` is True, the permission propagates to child folders
    and documents when the document is used as a template folder root.
    """

    document = models.ForeignKey(
        "dms.Document",
        on_delete=models.CASCADE,
        related_name="acl_permissions",
        verbose_name=_("document"),
    )
    principal_type = models.CharField(
        _("principal type"),
        max_length=20,
        choices=ACLPrincipalType.choices,
    )
    principal_id = models.CharField(
        _("principal id"),
        max_length=40,
        blank=True,
        default="",
        help_text=_("Empty for EVERYONE."),
    )
    action = models.CharField(
        _("action"),
        max_length=20,
        choices=ACLAction.choices,
    )
    effect = models.CharField(
        _("effect"),
        max_length=10,
        choices=ACLEffect.choices,
        default=ACLEffect.ALLOW,
    )
    granted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("granted by"),
    )
    expires_at = models.DateTimeField(
        _("expires at"),
        null=True,
        blank=True,
        help_text=_("NULL = no expiry."),
    )
    inheritable = models.BooleanField(
        _("inheritable"),
        default=False,
        help_text=_("Whether this permission propagates to child resources."),
    )
    notes = models.TextField(_("notes"), blank=True, default="")

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("document permission")
        verbose_name_plural = _("document permissions")
        ordering = ("action", "principal_type")
        constraints = [
            models.UniqueConstraint(
                fields=("document", "principal_type", "principal_id", "action"),
                condition=models.Q(is_deleted=False),
                name="dms_docperm_unique_per_doc_principal_action",
            ),
        ]
        indexes = [
            models.Index(
                fields=("document", "effect", "is_deleted"),
                name="dms_docperm_doc_effect_idx",
            ),
            models.Index(
                fields=("tenant", "principal_type", "principal_id", "is_deleted"),
                name="dms_docperm_principal_idx",
            ),
        ]

    def __str__(self) -> str:
        return (
            f"{self.effect.upper()} {self.action} on doc {self.document_id} "
            f"for {self.principal_type}:{self.principal_id or '*'}"
        )


# ---------------------------------------------------------------------------
# ShareLink
# ---------------------------------------------------------------------------

class ShareLink(UUIDModel, TenantScopedModel, AuditedModel, SoftDeleteModel):
    """A signed, expiring token that grants limited access to a document.

    The token is a random UUID generated at creation; it is the public
    identifier for the share link and is included in share URLs.

    Flags
    -----
    allow_download  — whether the token grants file download
    allow_preview   — whether the token grants preview/thumbnail access
    is_active       — quick kill-switch; revoke without deleting the record

    Rate limiting
    -------------
    max_uses + use_count implement simple token exhaustion.  Each successful
    ``use_share_link()`` call increments ``use_count``.

    Password protection
    -------------------
    ``password_hash`` stores a Django-compatible hash (make_password).
    Empty = no password required.

    Organisation isolation
    ----------------------
    The link is always scoped to the owning tenant so token enumeration across
    tenants is prevented at the query level.
    """

    document = models.ForeignKey(
        "dms.Document",
        on_delete=models.CASCADE,
        related_name="share_links",
        verbose_name=_("document"),
    )
    token = models.UUIDField(
        _("token"),
        unique=True,
        default=uuid.uuid4,
        editable=False,
        db_index=True,
    )
    label = models.CharField(
        _("label"),
        max_length=200,
        blank=True,
        default="",
        help_text=_("Human-readable label, e.g. 'For client review – Q3 2026'."),
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("created by"),
    )
    expires_at = models.DateTimeField(
        _("expires at"),
        null=True,
        blank=True,
        help_text=_("NULL = never expires."),
    )
    max_uses = models.PositiveIntegerField(
        _("max uses"),
        null=True,
        blank=True,
        help_text=_("NULL = unlimited."),
    )
    use_count = models.PositiveIntegerField(_("use count"), default=0)
    allow_download = models.BooleanField(_("allow download"), default=True)
    allow_preview = models.BooleanField(_("allow preview"), default=True)
    is_active = models.BooleanField(_("active"), default=True)
    notes = models.TextField(_("notes"), blank=True, default="")
    password_hash = models.CharField(
        _("password hash"),
        max_length=128,
        blank=True,
        default="",
        help_text=_("Django make_password() hash; empty = no password."),
    )

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("share link")
        verbose_name_plural = _("share links")
        ordering = ("-created_at",)
        indexes = [
            models.Index(
                fields=("document", "is_active", "is_deleted"),
                name="dms_sharelink_doc_active_idx",
            ),
        ]

    def __str__(self) -> str:
        return f"ShareLink({self.token}) for doc {self.document_id}"

    @property
    def is_expired(self) -> bool:
        if self.expires_at is None:
            return False
        from django.utils import timezone
        return timezone.now() >= self.expires_at

    @property
    def is_exhausted(self) -> bool:
        if self.max_uses is None:
            return False
        return self.use_count >= self.max_uses
