"""DMS versioning bounded context — models.

Entities
--------
DocumentLock
    Represents an exclusive checkout lock on a Document.
    Invariant: at most one active (is_active=True, is_deleted=False) lock
    may exist per document at any time.  Enforced via a partial unique
    database constraint.

    ``lock_token`` is a per-checkout UUID issued to the client.  The client
    must echo it back on check-in so that stale browser sessions cannot
    accidentally overwrite work from a newer checkout.

CheckoutSession
    Immutable, append-only audit log for checkout lifecycle events.
    One record is written per action: CHECKOUT, CHECKIN, FORCE_RELEASE,
    EXPIRED_RELEASE.  Records are never soft-deleted; they are the audit
    trail.

VersionDiff
    Metadata record modelling a comparison request between two
    DocumentVersions.  ``diff_data`` is a JSONField so that external diff
    backends (text diff, AI summary, structural diff) can populate it
    without schema migrations.  ``status`` tracks async computation state.
"""

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

AUTH_USER = settings.AUTH_USER_MODEL


# ---------------------------------------------------------------------------
# Choices
# ---------------------------------------------------------------------------

class LockStatus(models.TextChoices):
    ACTIVE = "active", _("Active")
    RELEASED = "released", _("Released")
    EXPIRED = "expired", _("Expired")
    FORCE_RELEASED = "force_released", _("Force Released")


class CheckoutAction(models.TextChoices):
    CHECKOUT = "checkout", _("Check-out")
    CHECKIN = "checkin", _("Check-in")
    FORCE_RELEASE = "force_release", _("Force Release")
    EXPIRED_RELEASE = "expired_release", _("Expired Release")


class DiffType(models.TextChoices):
    CONTENT = "content", _("Content")
    METADATA = "metadata", _("Metadata")
    FULL = "full", _("Full")


class DiffStatus(models.TextChoices):
    PENDING = "pending", _("Pending")
    READY = "ready", _("Ready")
    FAILED = "failed", _("Failed")


# ---------------------------------------------------------------------------
# DocumentLock
# ---------------------------------------------------------------------------

class DocumentLock(UUIDModel, TenantScopedModel, AuditedModel, SoftDeleteModel):
    """Exclusive checkout lock on a Document.

    Design decisions
    ----------------
    * ``lock_token`` (per-session UUID) guards against stale-client check-ins:
      the client must present the same token it received at checkout.
    * ``expires_at = None`` means the lock does not auto-expire; non-null
      values are processed by the ``release_expired_locks`` service function,
      which is intended to run as a periodic Celery task.
    * The partial unique constraint ``dms_doclock_unique_active_per_document``
      ensures database-level enforcement of the single-active-lock invariant,
      complementing the service-layer check.
    """

    document = models.ForeignKey(
        "dms.Document",
        on_delete=models.CASCADE,
        related_name="locks",
        verbose_name=_("document"),
    )
    locked_by = models.ForeignKey(
        AUTH_USER,
        on_delete=models.SET_NULL,
        null=True,
        related_name="dms_locks_held",
        verbose_name=_("locked by"),
    )
    lock_token = models.UUIDField(
        _("lock token"),
        unique=True,
        default=uuid.uuid4,
        editable=False,
        help_text=_("Opaque token issued at checkout; must be echoed back at check-in."),
    )
    expires_at = models.DateTimeField(
        _("expires at"),
        null=True,
        blank=True,
        help_text=_("Null means the lock never auto-expires."),
    )
    is_active = models.BooleanField(_("is active"), default=True, db_index=True)
    status = models.CharField(
        _("status"),
        max_length=20,
        choices=LockStatus.choices,
        default=LockStatus.ACTIVE,
        db_index=True,
    )
    notes = models.TextField(_("notes"), blank=True, default="")

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("document lock")
        verbose_name_plural = _("document locks")
        ordering = ("-created_at",)
        constraints = [
            models.UniqueConstraint(
                fields=("document",),
                condition=models.Q(is_active=True, is_deleted=False),
                name="dms_doclock_unique_active_per_document",
            ),
        ]
        indexes = [
            models.Index(
                fields=("document", "is_active", "is_deleted"),
                name="dms_doclock_doc_active_idx",
            ),
            models.Index(
                fields=("locked_by", "is_active", "is_deleted"),
                name="dms_doclock_user_active_idx",
            ),
            models.Index(
                fields=("tenant", "expires_at", "is_active"),
                name="dms_doclock_expires_idx",
            ),
        ]

    @property
    def is_expired(self) -> bool:
        from django.utils import timezone
        return self.expires_at is not None and self.expires_at < timezone.now()

    def __str__(self) -> str:
        return f"Lock({self.document_id}, {self.status})"


# ---------------------------------------------------------------------------
# CheckoutSession (audit log — append-only)
# ---------------------------------------------------------------------------

class CheckoutSession(UUIDModel, TenantScopedModel, AuditedModel):
    """Immutable audit log entry for a single checkout lifecycle event.

    Records are never mutated or deleted.  Together they form a complete
    timeline of who locked/unlocked each document and which version (if any)
    was produced on check-in.

    ``actor`` is the user who triggered the event; ``lock_holder`` is the
    user who held the lock at the time.  For CHECKOUT/CHECKIN they are the
    same person; for FORCE_RELEASE they differ.
    """

    document = models.ForeignKey(
        "dms.Document",
        on_delete=models.CASCADE,
        related_name="checkout_events",
        verbose_name=_("document"),
    )
    actor = models.ForeignKey(
        AUTH_USER,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="dms_checkout_actions",
        verbose_name=_("actor"),
        help_text=_(
            "User who performed this action.  May differ from the lock "
            "holder in FORCE_RELEASE scenarios."
        ),
    )
    lock_holder = models.ForeignKey(
        AUTH_USER,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="dms_checkout_sessions",
        verbose_name=_("lock holder"),
        help_text=_("User who held the lock at the time of this event."),
    )
    action = models.CharField(
        _("action"),
        max_length=20,
        choices=CheckoutAction.choices,
        db_index=True,
    )
    lock_token = models.UUIDField(
        _("lock token"),
        help_text=_("Snapshot of the lock token at the time of this event."),
    )
    version_created = models.ForeignKey(
        "dms.DocumentVersion",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="checkout_events",
        verbose_name=_("version created"),
        help_text=_("DocumentVersion produced during this check-in (if any)."),
    )
    notes = models.TextField(_("notes"), blank=True, default="")

    class Meta:
        verbose_name = _("checkout session")
        verbose_name_plural = _("checkout sessions")
        ordering = ("-created_at",)
        indexes = [
            models.Index(
                fields=("document", "action"),
                name="dms_checkout_doc_action_idx",
            ),
            models.Index(
                fields=("lock_token",),
                name="dms_checkout_token_idx",
            ),
            models.Index(
                fields=("tenant", "actor", "action"),
                name="dms_checkout_actor_idx",
            ),
        ]

    def __str__(self) -> str:
        return f"CheckoutSession({self.document_id}, {self.action})"


# ---------------------------------------------------------------------------
# VersionDiff
# ---------------------------------------------------------------------------

class VersionDiff(UUIDModel, TenantScopedModel, AuditedModel, SoftDeleteModel):
    """Version comparison metadata record.

    Stores inputs and outputs of a diff request between two DocumentVersions.
    ``diff_data`` is an open-ended JSONField so that multiple diff backends
    (text diff, AI summary, structural diff) can be supported without further
    schema migrations.

    Status transitions
    ------------------
    PENDING → READY     (computed successfully)
    PENDING → FAILED    (computation error; diff_summary holds the reason)

    The actual diff computation happens outside this model (async task or
    external service).  ``request_version_diff`` creates the record;
    ``update_version_diff`` populates results.
    """

    from_version = models.ForeignKey(
        "dms.DocumentVersion",
        on_delete=models.CASCADE,
        related_name="diffs_as_source",
        verbose_name=_("from version"),
    )
    to_version = models.ForeignKey(
        "dms.DocumentVersion",
        on_delete=models.CASCADE,
        related_name="diffs_as_target",
        verbose_name=_("to version"),
    )
    diff_type = models.CharField(
        _("diff type"),
        max_length=20,
        choices=DiffType.choices,
        default=DiffType.FULL,
    )
    status = models.CharField(
        _("status"),
        max_length=20,
        choices=DiffStatus.choices,
        default=DiffStatus.PENDING,
        db_index=True,
    )
    diff_summary = models.TextField(
        _("diff summary"),
        blank=True,
        default="",
        help_text=_("Human-readable summary of changes, or error reason on FAILED."),
    )
    diff_data = models.JSONField(
        _("diff data"),
        default=dict,
        blank=True,
        help_text=_("Structured result from the diff backend.  Format is backend-defined."),
    )

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("version diff")
        verbose_name_plural = _("version diffs")
        ordering = ("-created_at",)
        constraints = [
            models.UniqueConstraint(
                fields=("from_version", "to_version", "diff_type"),
                condition=models.Q(is_deleted=False),
                name="dms_versiondiff_unique_per_versions_type",
            ),
        ]
        indexes = [
            models.Index(
                fields=("from_version", "to_version"),
                name="dms_versiondiff_versions_idx",
            ),
            models.Index(
                fields=("tenant", "status", "is_deleted"),
                name="dms_versiondiff_status_idx",
            ),
        ]

    def __str__(self) -> str:
        return (
            f"VersionDiff({self.from_version_id} → {self.to_version_id}, {self.diff_type})"
        )
