"""DMS records — services.

Public API
----------
RetentionPolicy:
    create_retention_policy(...)                       -> RetentionPolicy
    update_retention_policy(policy, ...)               -> RetentionPolicy
    delete_retention_policy(policy)                    -> None  (soft-delete)

LegalHold:
    place_legal_hold(*, ...)                           -> LegalHold
    release_legal_hold(hold, *, released_by)           -> LegalHold

ArchiveRecord (write-once):
    create_archive_record(*, ...)                      -> ArchiveRecord

Design notes
------------
Retention policies
    Deleting a retention policy is a soft-delete.  Existing ArchiveRecords
    referencing the policy retain the FK relationship; the policy row is
    preserved in the DB with ``is_deleted=True``.

    Updating a policy does NOT retroactively recompute ``expires_at`` on
    existing ArchiveRecords — that would break audit immutability.

Legal holds
    Multiple concurrent holds on a single document are allowed.  A document
    is considered "held" when ``has_active_legal_hold()`` returns True.

    Releasing a hold sets ``ended_at`` and ``released_by``.  Attempting to
    release an already-released hold raises ``RecordsError``.

Archive records
    Immutable after creation.  The ``expires_at`` field is computed from
    the retention policy at creation time:
      - ``is_permanent=True``  → expires_at = None  (always)
      - policy with period = 0  → expires_at = None  (keep indefinitely)
      - policy with period > 0  → expires_at = archived_at + period days
      - no policy               → expires_at = None

    The service does NOT enforce that a document lacks a legal hold before
    archiving — archiving while held is permitted (e.g. litigation discovery
    archival).  Destruction is a separate process that must check holds.
"""

from __future__ import annotations

from datetime import timedelta

from django.utils import timezone

from simorgh.apps.dms.records.models import (
    ActionOnExpiry,
    ArchiveRecord,
    LegalHold,
    RetentionPolicy,
)

# Sentinel for "argument not provided" in partial-update helpers
_UNSET = object()


# ---------------------------------------------------------------------------
# Domain errors
# ---------------------------------------------------------------------------

class RecordsError(Exception):
    """Base error for the records bounded context."""


class RetentionPolicyError(RecordsError):
    """Raised for invalid retention policy operations."""


class LegalHoldError(RecordsError):
    """Raised for invalid legal hold operations."""


class ArchiveError(RecordsError):
    """Raised for invalid archive record operations."""


# ---------------------------------------------------------------------------
# RetentionPolicy services
# ---------------------------------------------------------------------------

def create_retention_policy(
    *,
    tenant_id: int,
    organization_node_id: int,
    name: str,
    description: str = "",
    retention_period_days: int = 0,
    action_on_expiry: str = ActionOnExpiry.REVIEW,
    is_active: bool = True,
) -> RetentionPolicy:
    """Create a new retention policy for a tenant.

    Raises ``RetentionPolicyError`` if:
    - ``name`` is blank
    - ``retention_period_days`` is negative
    - ``action_on_expiry`` is not a valid choice
    """
    name = (name or "").strip()
    if not name:
        raise RetentionPolicyError("Retention policy name must not be empty.")

    if retention_period_days < 0:
        raise RetentionPolicyError("retention_period_days must be non-negative.")

    valid_actions = {c[0] for c in ActionOnExpiry.choices}
    if action_on_expiry not in valid_actions:
        raise RetentionPolicyError(
            f"Invalid action_on_expiry {action_on_expiry!r}. "
            f"Valid values: {sorted(valid_actions)}."
        )

    return RetentionPolicy.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        name=name,
        description=description,
        retention_period_days=retention_period_days,
        action_on_expiry=action_on_expiry,
        is_active=is_active,
    )


def update_retention_policy(
    policy: RetentionPolicy,
    *,
    name=_UNSET,
    description=_UNSET,
    retention_period_days=_UNSET,
    action_on_expiry=_UNSET,
    is_active=_UNSET,
) -> RetentionPolicy:
    """Partially update a retention policy.

    Only provided (non-_UNSET) fields are changed.
    Does NOT retroactively recompute expires_at on existing ArchiveRecords.
    """
    update_fields = []

    if name is not _UNSET:
        name = (name or "").strip()
        if not name:
            raise RetentionPolicyError("Retention policy name must not be empty.")
        policy.name = name
        update_fields.append("name")

    if description is not _UNSET:
        policy.description = description
        update_fields.append("description")

    if retention_period_days is not _UNSET:
        if retention_period_days < 0:
            raise RetentionPolicyError("retention_period_days must be non-negative.")
        policy.retention_period_days = retention_period_days
        update_fields.append("retention_period_days")

    if action_on_expiry is not _UNSET:
        valid_actions = {c[0] for c in ActionOnExpiry.choices}
        if action_on_expiry not in valid_actions:
            raise RetentionPolicyError(
                f"Invalid action_on_expiry {action_on_expiry!r}."
            )
        policy.action_on_expiry = action_on_expiry
        update_fields.append("action_on_expiry")

    if is_active is not _UNSET:
        policy.is_active = is_active
        update_fields.append("is_active")

    if update_fields:
        update_fields.append("updated_at")
        policy.save(update_fields=update_fields)

    return policy


def delete_retention_policy(policy: RetentionPolicy) -> None:
    """Soft-delete a retention policy.

    The policy row is retained for audit purposes.  Existing ArchiveRecords
    that reference it are unaffected (FK uses SET_NULL on ArchiveRecord, but
    the policy is only soft-deleted so the FK remains valid).
    """
    policy.is_deleted = True
    policy.save(update_fields=["is_deleted", "updated_at"])


# ---------------------------------------------------------------------------
# LegalHold services
# ---------------------------------------------------------------------------

def place_legal_hold(
    *,
    tenant_id: int,
    organization_node_id: int,
    document,
    name: str,
    placed_by,
    notes: str = "",
) -> LegalHold:
    """Place a legal hold on a document.

    Multiple concurrent holds on a single document are supported.
    ``name`` should be a short description of the hold reason (e.g.
    "Litigation — Case #2024-001").

    Raises ``LegalHoldError`` if ``name`` is blank.
    """
    name = (name or "").strip()
    if not name:
        raise LegalHoldError("Legal hold name must not be empty.")

    return LegalHold.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        document=document,
        name=name,
        notes=notes,
        placed_by=placed_by,
    )


def release_legal_hold(hold: LegalHold, *, released_by) -> LegalHold:
    """Release an active legal hold.

    Sets ``ended_at`` to the current time and records ``released_by``.

    Raises ``LegalHoldError`` if the hold is already released (ended_at != None).
    """
    if hold.ended_at is not None:
        raise LegalHoldError(
            f"Legal hold {hold.public_id!r} is already released "
            f"(ended_at={hold.ended_at})."
        )

    hold.ended_at = timezone.now()
    hold.released_by = released_by
    hold.save(update_fields=["ended_at", "released_by"])
    return hold


# ---------------------------------------------------------------------------
# ArchiveRecord services
# ---------------------------------------------------------------------------

def create_archive_record(
    *,
    tenant_id: int,
    organization_node_id: int,
    document,
    archived_by,
    reason: str = "",
    version=None,
    retention_policy: RetentionPolicy | None = None,
    is_permanent: bool = False,
) -> ArchiveRecord:
    """Create an immutable archive record for a document.

    ``expires_at`` computation:
    - ``is_permanent=True``        → None (no expiry regardless of policy)
    - policy.retention_period_days == 0  → None
    - policy.retention_period_days > 0   → archived_at + period
    - no policy                    → None

    Raises ``ArchiveError`` if:
    - the document already has an archive record that is permanent
      (duplicate permanent archives are not allowed)
    """
    if is_permanent:
        existing_permanent = ArchiveRecord.objects.filter(
            document=document, is_permanent=True
        ).exists()
        if existing_permanent:
            raise ArchiveError(
                f"Document {document.public_id!r} already has a permanent archive record."
            )

    archived_at = timezone.now()
    expires_at = None

    if not is_permanent and retention_policy is not None:
        days = retention_policy.retention_period_days
        if days > 0:
            expires_at = archived_at + timedelta(days=days)

    return ArchiveRecord.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        document=document,
        version=version,
        retention_policy=retention_policy,
        archived_by=archived_by,
        archived_at=archived_at,
        reason=reason,
        expires_at=expires_at,
        is_permanent=is_permanent,
    )
