"""DMS documents — service layer.

All mutation logic for DocumentType, Document, and DocumentVersion.

Public surface
--------------
Document types
  create_document_type   — validate code + create
  update_document_type   — rename / re-describe
  delete_document_type   — soft delete (documents retain their type reference)

Documents
  create_document        — shell document (no version yet)
  update_document        — rename / reclassify / move folder
  archive_document       — soft-archive (no new versions accepted)
  delete_document        — soft delete

Versions
  add_version            — append an immutable version snapshot
  publish_version        — DRAFT → PUBLISHED; updates document.status
  rollback_to_version    — create a new version copying a prior version's file
"""

from __future__ import annotations

from django.db import transaction
from django.utils.translation import gettext_lazy as _

from simorgh.apps.dms.documents.models import (
    Document,
    DocumentStatus,
    DocumentType,
    DocumentVersion,
    DocumentVersionStatus,
)


# ---------------------------------------------------------------------------
# Custom domain errors
# ---------------------------------------------------------------------------

class DocumentTypeError(Exception):
    """Raised for invalid DocumentType operations."""


class DocumentError(Exception):
    """Raised for invalid Document operations."""


class VersionError(Exception):
    """Raised for invalid DocumentVersion operations."""


# ---------------------------------------------------------------------------
# DocumentType services
# ---------------------------------------------------------------------------

def create_document_type(
    *,
    name: str,
    code: str,
    tenant_id: int,
    organization_node_id: int,
    description: str = "",
    icon: str = "",
    color: str = "",
    is_active: bool = True,
) -> DocumentType:
    code = code.strip().lower()
    if DocumentType.objects.filter(tenant_id=tenant_id, code=code, is_deleted=False).exists():
        raise DocumentTypeError(
            _("A document type with code '%(code)s' already exists.") % {"code": code}
        )
    return DocumentType.objects.create(
        name=name.strip(),
        code=code,
        description=description,
        icon=icon,
        color=color,
        is_active=is_active,
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
    )


def update_document_type(
    doc_type: DocumentType,
    *,
    name: str | None = None,
    description: str | None = None,
    icon: str | None = None,
    color: str | None = None,
    is_active: bool | None = None,
) -> DocumentType:
    fields = ["updated_at"]
    if name is not None:
        doc_type.name = name.strip()
        fields.append("name")
    if description is not None:
        doc_type.description = description
        fields.append("description")
    if icon is not None:
        doc_type.icon = icon
        fields.append("icon")
    if color is not None:
        doc_type.color = color
        fields.append("color")
    if is_active is not None:
        doc_type.is_active = is_active
        fields.append("is_active")
    doc_type.save(update_fields=fields)
    return doc_type


def delete_document_type(doc_type: DocumentType) -> None:
    doc_type.delete()


# ---------------------------------------------------------------------------
# Document services
# ---------------------------------------------------------------------------

def create_document(
    *,
    title: str,
    repository,  # repositories.models.Repository
    tenant_id: int,
    organization_node_id: int,
    folder=None,
    document_type: DocumentType | None = None,
    code: str = "",
    workflow_status: str = "",
    extra: dict | None = None,
) -> Document:
    """Create and return a new shell document (no version yet)."""
    code = code.strip()
    if code:
        if Document.objects.filter(tenant_id=tenant_id, code=code, is_deleted=False).exists():
            raise DocumentError(
                _("A document with code '%(code)s' already exists.") % {"code": code}
            )
    elif not code:
        # Auto-generate code from reference sequence if one is configured.
        try:
            from simorgh.apps.platform_core.services import get_next_reference
            code = get_next_reference(tenant_id, "dms.document")
        except Exception:  # noqa: BLE001
            pass  # No sequence configured — leave code blank

    doc = Document.objects.create(
        title=title.strip(),
        code=code,
        repository=repository,
        folder=folder,
        document_type=document_type,
        status=DocumentStatus.DRAFT,
        workflow_status=workflow_status,
        extra=extra or {},
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
    )
    from simorgh.apps.dms.registry import fire_hook
    from simorgh.apps.dms.hooks import HOOK_POST_DOCUMENT_CREATE
    fire_hook(HOOK_POST_DOCUMENT_CREATE, document=doc)
    return doc


def update_document(
    doc: Document,
    *,
    title: str | None = None,
    folder=...,  # use ... sentinel to mean "unchanged"
    document_type=...,
    workflow_status: str | None = None,
    extra: dict | None = None,
) -> Document:
    if doc.status == DocumentStatus.ARCHIVED:
        raise DocumentError(_("Cannot update an archived document."))

    fields = ["updated_at"]
    if title is not None:
        doc.title = title.strip()
        fields.append("title")
    if folder is not ...:
        doc.folder = folder
        fields.append("folder")
    if document_type is not ...:
        doc.document_type = document_type
        fields.append("document_type")
    if workflow_status is not None:
        doc.workflow_status = workflow_status
        fields.append("workflow_status")
    if extra is not None:
        doc.extra = extra
        fields.append("extra")
    doc.save(update_fields=fields)
    from simorgh.apps.dms.registry import fire_hook
    from simorgh.apps.dms.hooks import HOOK_POST_DOCUMENT_UPDATE
    fire_hook(HOOK_POST_DOCUMENT_UPDATE, document=doc)
    return doc


def archive_document(doc: Document) -> Document:
    """Mark a document as archived. No new versions can be added."""
    if doc.status == DocumentStatus.ARCHIVED:
        return doc
    doc.status = DocumentStatus.ARCHIVED
    doc.save(update_fields=["status", "updated_at"])
    from simorgh.apps.dms.registry import fire_hook
    from simorgh.apps.dms.hooks import HOOK_POST_DOCUMENT_ARCHIVE
    fire_hook(HOOK_POST_DOCUMENT_ARCHIVE, document=doc)
    return doc


def delete_document(doc: Document) -> None:
    from simorgh.apps.dms.registry import fire_hook
    from simorgh.apps.dms.hooks import HOOK_PRE_DOCUMENT_DELETE, HOOK_POST_DOCUMENT_DELETE
    fire_hook(HOOK_PRE_DOCUMENT_DELETE, document=doc)
    with transaction.atomic():
        DocumentVersion.objects.filter(document=doc, is_deleted=False).update(is_deleted=True)
        doc.delete()
    fire_hook(HOOK_POST_DOCUMENT_DELETE, document=doc)


# ---------------------------------------------------------------------------
# Version services
# ---------------------------------------------------------------------------

def _next_version(
    document: Document, bump: str
) -> tuple[int, int]:
    """Compute the next (major, minor) version numbers."""
    existing = (
        DocumentVersion.objects.filter(document=document, is_deleted=False)
        .order_by("-version_major", "-version_minor")
        .values("version_major", "version_minor")
        .first()
    )
    if existing is None:
        return (1, 0)

    major, minor = existing["version_major"], existing["version_minor"]
    if bump == "major":
        return (major + 1, 0)
    return (major, minor + 1)


def add_version(
    *,
    document: Document,
    file_asset=None,  # storage.FileMetadata or None
    bump: str = "minor",
    label: str = "",
    change_summary: str = "",
    tenant_id: int,
    organization_node_id: int,
) -> DocumentVersion:
    """Append a new immutable version to a document.

    The new version starts in DRAFT status.  Call ``publish_version`` to
    promote it to PUBLISHED.

    ``bump`` must be "minor" or "major".  First version is always 1.0.
    """
    if document.status == DocumentStatus.ARCHIVED:
        raise DocumentError(_("Cannot add a version to an archived document."))

    if bump not in ("minor", "major"):
        raise VersionError(_("Invalid bump value. Use 'minor' or 'major'."))

    major, minor = _next_version(document, bump)

    # Mirror file fields for display efficiency.
    file_size = None
    content_type = ""
    checksum = ""
    if file_asset is not None:
        file_size = getattr(file_asset, "size_bytes", None)
        content_type = getattr(file_asset, "content_type", "")
        checksum = getattr(file_asset, "checksum_sha256", "")

    with transaction.atomic():
        version = DocumentVersion.objects.create(
            document=document,
            file_asset=file_asset,
            version_major=major,
            version_minor=minor,
            status=DocumentVersionStatus.DRAFT,
            label=label,
            change_summary=change_summary,
            file_size_bytes=file_size,
            content_type=content_type,
            checksum_sha256=checksum,
            is_current=True,
            tenant_id=tenant_id,
            organization_node_id=organization_node_id,
        )

        # Demote previous current version (if any).
        DocumentVersion.objects.filter(
            document=document, is_current=True, is_deleted=False
        ).exclude(pk=version.pk).update(is_current=False)

        # Point document at the new version.
        document.current_version = version
        # Keep document status as DRAFT until explicitly published.
        document.save(update_fields=["current_version", "updated_at"])

    return version


def publish_version(version: DocumentVersion) -> DocumentVersion:
    """Promote a DRAFT version to PUBLISHED.

    Side effects:
    * Previous PUBLISHED versions of the same document become SUPERSEDED.
    * ``document.status`` is set to PUBLISHED.
    * ``document.current_version`` is updated to this version.
    """
    if version.status != DocumentVersionStatus.DRAFT:
        raise VersionError(
            _("Only DRAFT versions can be published (current status: %(status)s).") % {"status": version.status}
        )

    document = version.document

    with transaction.atomic():
        # Supersede any existing published versions.
        DocumentVersion.objects.filter(
            document=document,
            status=DocumentVersionStatus.PUBLISHED,
            is_deleted=False,
        ).update(status=DocumentVersionStatus.SUPERSEDED, is_current=False)

        version.status = DocumentVersionStatus.PUBLISHED
        version.is_current = True
        version.save(update_fields=["status", "is_current", "updated_at"])

        document.current_version = version
        document.status = DocumentStatus.PUBLISHED
        document.save(update_fields=["current_version", "status", "updated_at"])

    from simorgh.apps.dms.registry import fire_hook
    from simorgh.apps.dms.hooks import HOOK_POST_DOCUMENT_PUBLISH
    fire_hook(HOOK_POST_DOCUMENT_PUBLISH, version=version, document=version.document)
    return version


def rollback_to_version(
    *,
    document: Document,
    target_version: DocumentVersion,
    tenant_id: int,
    organization_node_id: int,
    change_summary: str = "",
) -> DocumentVersion:
    """Create a new version whose file_asset is copied from ``target_version``.

    This is a non-destructive rollback: the history is preserved and a new
    version entry is created pointing to the same file asset.
    """
    if document.status == DocumentStatus.ARCHIVED:
        raise DocumentError(_("Cannot roll back an archived document."))
    if target_version.document_id != document.pk:
        raise VersionError(_("Target version does not belong to this document."))
    if target_version.file_asset is None:
        raise VersionError(_("Cannot roll back to a version without a file asset."))

    # Rollback always creates a new minor version on the current major line.
    return add_version(
        document=document,
        file_asset=target_version.file_asset,
        bump="minor",
        label=f"Rollback to v{target_version.version_label}",
        change_summary=change_summary or f"Rolled back to v{target_version.version_label}",
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
    )
