"""DMS permissions — query layer.

All DB reads for ACLPolicy, ACLRule, DocumentPermission, and ShareLink go
through this module.  Views and services must not build raw ORM queries.
"""

from __future__ import annotations

import uuid

from django.db.models import QuerySet

from simorgh.apps.dms.common.exceptions import AssetNotFound
from simorgh.apps.dms.permissions.models import (
    ACLPolicy,
    ACLRule,
    ACLSubjectType,
    DocumentPermission,
    ShareLink,
)


# ---------------------------------------------------------------------------
# ACLPolicy
# ---------------------------------------------------------------------------

def get_policy(tenant_id: int, public_id: str) -> ACLPolicy:
    """Return a non-deleted ACLPolicy for the given tenant.

    Raises :exc:`~dms.common.exceptions.AssetNotFound` if not found.
    """
    try:
        uid = uuid.UUID(str(public_id))
    except (ValueError, AttributeError) as exc:
        raise AssetNotFound(f"Invalid ACL policy id: {public_id!r}") from exc
    try:
        return ACLPolicy.objects.get(
            tenant_id=tenant_id, public_id=uid, is_deleted=False
        )
    except ACLPolicy.DoesNotExist as exc:
        raise AssetNotFound(f"ACLPolicy {public_id!r} not found.") from exc


def list_policies_for_subject(
    tenant_id: int,
    subject_type: str,
    subject_id: str,
    *,
    include_inactive: bool = False,
) -> QuerySet:
    """List all non-deleted policies attached to a specific resource."""
    qs = ACLPolicy.objects.filter(
        tenant_id=tenant_id,
        subject_type=subject_type,
        subject_id=subject_id,
        is_deleted=False,
    ).prefetch_related("rules")
    if not include_inactive:
        qs = qs.filter(is_active=True)
    return qs.order_by("name")


def list_all_policies(
    tenant_id: int,
    *,
    subject_type: str | None = None,
    include_inactive: bool = False,
) -> QuerySet:
    """List all non-deleted policies for a tenant (admin/ops view)."""
    qs = ACLPolicy.objects.filter(tenant_id=tenant_id, is_deleted=False)
    if subject_type is not None:
        qs = qs.filter(subject_type=subject_type)
    if not include_inactive:
        qs = qs.filter(is_active=True)
    return qs.order_by("subject_type", "name")


# ---------------------------------------------------------------------------
# ACLRule
# ---------------------------------------------------------------------------

def get_rule(policy: ACLPolicy, public_id: str) -> ACLRule:
    """Return a non-deleted ACLRule belonging to ``policy``.

    Raises :exc:`~dms.common.exceptions.AssetNotFound` if not found.
    """
    try:
        uid = uuid.UUID(str(public_id))
    except (ValueError, AttributeError) as exc:
        raise AssetNotFound(f"Invalid ACL rule id: {public_id!r}") from exc
    try:
        return ACLRule.objects.get(policy=policy, public_id=uid, is_deleted=False)
    except ACLRule.DoesNotExist as exc:
        raise AssetNotFound(f"ACLRule {public_id!r} not found.") from exc


def list_rules_for_policy(policy: ACLPolicy) -> QuerySet:
    """Return all non-deleted rules for a policy, ordered by priority desc."""
    return ACLRule.objects.filter(policy=policy, is_deleted=False).order_by(
        "-priority", "action"
    )


# ---------------------------------------------------------------------------
# DocumentPermission
# ---------------------------------------------------------------------------

def get_document_permission(document, public_id: str) -> DocumentPermission:
    """Return a DocumentPermission for the given document.

    Raises :exc:`~dms.common.exceptions.AssetNotFound` if not found.
    """
    try:
        uid = uuid.UUID(str(public_id))
    except (ValueError, AttributeError) as exc:
        raise AssetNotFound(f"Invalid document permission id: {public_id!r}") from exc
    try:
        return DocumentPermission.objects.get(
            document=document, public_id=uid, is_deleted=False
        )
    except DocumentPermission.DoesNotExist as exc:
        raise AssetNotFound(f"DocumentPermission {public_id!r} not found.") from exc


def list_document_permissions(document) -> QuerySet:
    """Return all active (non-deleted) permissions for a document."""
    return DocumentPermission.objects.filter(
        document=document, is_deleted=False
    ).order_by("action", "principal_type")


def get_effective_policies_for_document(document) -> list[ACLPolicy]:
    """Return all active, non-deleted policies that apply to a document.

    Walks the hierarchy: document → folder ancestors → repository.
    Returns policies in order from most-specific (document) to least
    specific (repository).
    """
    policies: list[ACLPolicy] = []
    tenant_id = document.tenant_id

    # 1) Policies on the document itself
    doc_policies = list(
        list_policies_for_subject(
            tenant_id, ACLSubjectType.DOCUMENT, str(document.public_id)
        )
    )
    policies.extend(doc_policies)

    # 2) Policies on folder ancestors (walk up via materialized_path)
    if document.folder_id is not None:
        folder = document.folder
        # Collect ancestor folder public_ids from materialized_path
        path_parts = folder.materialized_path.split("/") if folder.materialized_path else []
        for part in reversed(path_parts):
            folder_policies = list(
                list_policies_for_subject(tenant_id, ACLSubjectType.FOLDER, part)
            )
            # Only include inheritable policies (except the direct folder)
            for p in folder_policies:
                if p.inheritable or part == str(folder.public_id):
                    policies.append(p)

    # 3) Policies on the repository
    if document.repository_id is not None:
        repo_policies = list(
            list_policies_for_subject(
                tenant_id,
                ACLSubjectType.REPOSITORY,
                str(document.repository.public_id),
            )
        )
        for p in repo_policies:
            if p.inheritable:
                policies.append(p)

    return policies


# ---------------------------------------------------------------------------
# ShareLink
# ---------------------------------------------------------------------------

def get_share_link(tenant_id: int, public_id: str) -> ShareLink:
    """Return a non-deleted ShareLink by its public_id (not the token)."""
    try:
        uid = uuid.UUID(str(public_id))
    except (ValueError, AttributeError) as exc:
        raise AssetNotFound(f"Invalid share link id: {public_id!r}") from exc
    try:
        return ShareLink.objects.select_related("document").get(
            tenant_id=tenant_id, public_id=uid, is_deleted=False
        )
    except ShareLink.DoesNotExist as exc:
        raise AssetNotFound(f"ShareLink {public_id!r} not found.") from exc


def get_share_link_by_token(token: str) -> ShareLink | None:
    """Return a ShareLink by its public token UUID, or None if not found.

    Does NOT filter by tenant — token uniqueness is enforced at the DB level.
    Soft-deleted links are excluded.
    """
    try:
        tok = uuid.UUID(str(token))
    except (ValueError, AttributeError):
        return None
    return (
        ShareLink.objects.select_related("document")
        .filter(token=tok, is_deleted=False)
        .first()
    )


def list_share_links_for_document(document) -> QuerySet:
    """Return all non-deleted share links for a document."""
    return ShareLink.objects.filter(document=document, is_deleted=False).order_by(
        "-created_at"
    )
