from __future__ import annotations

from typing import ClassVar

from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from simorgh.core.models import (
    ScopedSoftDeleteManager,
    SoftDeleteModel,
    TenantScopedModel,
    TimeStampedModel,
    UUIDModel,
)


# ---------------------------------------------------------------------------
# Enumerations
# ---------------------------------------------------------------------------

class FileUploadStatus(models.TextChoices):
    """Tracks where in the upload lifecycle a FileMetadata record is.

    PENDING   — row created, file not yet written to the provider.
    UPLOADING — partial / chunk write in progress (e.g. via UploadSession).
    READY     — file fully persisted and accessible.
    FAILED    — upload aborted or provider error; record kept for audit.
    """

    PENDING = "pending", _("Pending")
    UPLOADING = "uploading", _("Uploading")
    READY = "ready", _("Ready")
    FAILED = "failed", _("Failed")


class VirusScanStatus(models.TextChoices):
    """Result of the antivirus / malware scan pipeline."""

    PENDING = "pending", _("Pending")
    CLEAN = "clean", _("Clean")
    INFECTED = "infected", _("Infected")
    SKIPPED = "skipped", _("Skipped")
    ERROR = "error", _("Scan Error")


class FileProcessingStatus(models.TextChoices):
    """Async post-upload processing state (thumbnail extraction, OCR, etc.)."""

    PENDING = "pending", _("Pending")
    PROCESSING = "processing", _("Processing")
    DONE = "done", _("Done")
    FAILED = "failed", _("Failed")
    SKIPPED = "skipped", _("Skipped")


class UploadSessionStatus(models.TextChoices):
    """Lifecycle of a multi-part / resumable upload session."""

    INITIATED = "initiated", _("Initiated")
    UPLOADING = "uploading", _("Uploading")
    ASSEMBLING = "assembling", _("Assembling chunks")
    COMPLETED = "completed", _("Completed")
    EXPIRED = "expired", _("Expired")
    FAILED = "failed", _("Failed")


class FileVariantKind(models.TextChoices):
    """Derived representation of the original file."""

    THUMBNAIL = "thumbnail", _("Thumbnail")
    PREVIEW = "preview", _("Preview")
    WATERMARKED = "watermarked", _("Watermarked")
    COMPRESSED = "compressed", _("Compressed")
    TRANSCODED = "transcoded", _("Transcoded")


class FileVariantStatus(models.TextChoices):
    PENDING = "pending", _("Pending")
    PROCESSING = "processing", _("Processing")
    READY = "ready", _("Ready")
    FAILED = "failed", _("Failed")


# ---------------------------------------------------------------------------
# StorageFolder
# ---------------------------------------------------------------------------

class StorageFolder(UUIDModel, TimeStampedModel):
    """A logical folder that groups :class:`FileMetadata` records.

    Folders are tenant-scoped and support unlimited nesting via a self-FK.
    System folders (``is_system=True``) are auto-created by modules such as
    ``chat`` or ``helpdesk`` and should not be renamed by users.
    """

    tenant = models.ForeignKey(
        "tenants.Tenant",
        on_delete=models.CASCADE,
        related_name="+",
        verbose_name=_("tenant"),
    )
    parent = models.ForeignKey(
        "self",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="children",
        verbose_name=_("parent folder"),
    )
    name = models.CharField(_("name"), max_length=255)
    is_system = models.BooleanField(
        _("system folder"),
        default=False,
        help_text=_("Auto-managed by a module; should not be renamed by users."),
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("created by"),
    )

    class Meta:
        verbose_name = _("folder")
        verbose_name_plural = _("folders")
        ordering = ("name",)
        constraints = [
            models.UniqueConstraint(
                fields=("tenant", "parent", "name"),
                name="storage_folder_unique_name_per_parent",
                condition=models.Q(parent__isnull=False),
            ),
            models.UniqueConstraint(
                fields=("tenant", "name"),
                name="storage_folder_unique_root_name",
                condition=models.Q(parent__isnull=True),
            ),
        ]

    def __str__(self) -> str:
        return self.full_path

    @property
    def full_path(self) -> str:
        """Return the absolute path, e.g. ``/Chat/Attachments``."""
        parts: list[str] = []
        node: StorageFolder | None = self
        while node is not None:
            parts.append(node.name)
            node = node.parent
        return "/" + "/".join(reversed(parts))


# ---------------------------------------------------------------------------
# FileMetadata  (platform-level FileAsset abstraction)
# ---------------------------------------------------------------------------

class FileMetadata(UUIDModel, TenantScopedModel, SoftDeleteModel):
    """Pointer to a blob held by a ``StorageProvider``.

    This is the platform-level **FileAsset** abstraction.  Not every
    ``FileMetadata`` is a DMS document — chat attachments and helpdesk
    uploads share the same table.  The DMS Document entity will reference
    this model through ``DocumentVersion``.

    Immutable storage key
    ---------------------
    ``path`` is set once by ``store_file`` using ``safe_tenant_path()``
    (UUID-based sub-directory, so collisions are impossible) and must
    never be changed.  Moving a file means creating a new record.

    Status fields
    -------------
    ``upload_status``     — where in the upload lifecycle the blob is.
    ``virus_scan_status`` — result of the AV pipeline (async, pluggable).
    ``processing_status`` — post-upload async work (thumbnails, OCR, …).
    """

    # --- Storage key (immutable after creation) ---------------------------
    storage_backend = models.CharField(_("backend"), max_length=32, default="local")
    path = models.CharField(
        _("storage path"),
        max_length=1024,
        help_text=_(
            "Immutable storage key, e.g. tenants/1/abc123/report.pdf. "
            "Set once by store_file; never changed afterwards."
        ),
    )

    # --- Human-readable metadata ------------------------------------------
    filename = models.CharField(_("filename"), max_length=256)
    content_type = models.CharField(_("content type"), max_length=128, blank=True)
    size_bytes = models.PositiveBigIntegerField(_("size"))
    checksum_sha256 = models.CharField(_("sha256"), max_length=64, db_index=True)

    # --- Upload lifecycle -------------------------------------------------
    upload_status = models.CharField(
        _("upload status"),
        max_length=16,
        choices=FileUploadStatus.choices,
        default=FileUploadStatus.PENDING,
        db_index=True,
    )

    # --- Security ---------------------------------------------------------
    virus_scan_status = models.CharField(
        _("virus scan status"),
        max_length=16,
        choices=VirusScanStatus.choices,
        default=VirusScanStatus.PENDING,
        db_index=True,
    )
    virus_scan_at = models.DateTimeField(_("scanned at"), null=True, blank=True)

    # --- Async processing -------------------------------------------------
    processing_status = models.CharField(
        _("processing status"),
        max_length=16,
        choices=FileProcessingStatus.choices,
        default=FileProcessingStatus.PENDING,
        db_index=True,
    )

    # --- Ownership --------------------------------------------------------
    uploaded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )

    # --- Organisation -----------------------------------------------------
    folder = models.ForeignKey(
        StorageFolder,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="files",
        verbose_name=_("folder"),
    )
    app_context = models.CharField(
        _("app context"),
        max_length=64,
        blank=True,
        default="",
        db_index=True,
        help_text=_("Module that created this file, e.g. 'chat', 'helpdesk'."),
    )

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("file")
        verbose_name_plural = _("files")
        ordering = ("-created_at",)
        constraints = (
            models.UniqueConstraint(
                fields=("storage_backend", "path"),
                name="storage_file_unique_backend_path",
            ),
        )
        indexes = (
            models.Index(fields=("tenant", "-created_at")),
            models.Index(fields=("tenant", "upload_status")),
            models.Index(fields=("tenant", "checksum_sha256")),
            models.Index(fields=("app_context",)),
        )

    def __str__(self) -> str:
        return f"{self.filename} ({self.size_bytes}b)"

    def mark_ready(self) -> None:
        """Transition upload_status → READY and processing_status → PENDING."""
        self.upload_status = FileUploadStatus.READY
        self.processing_status = FileProcessingStatus.PENDING
        self.save(update_fields=["upload_status", "processing_status", "updated_at"])

    def mark_upload_failed(self) -> None:
        self.upload_status = FileUploadStatus.FAILED
        self.save(update_fields=["upload_status", "updated_at"])

    def mark_scan_clean(self) -> None:
        self.virus_scan_status = VirusScanStatus.CLEAN
        self.virus_scan_at = timezone.now()
        self.save(update_fields=["virus_scan_status", "virus_scan_at", "updated_at"])

    def mark_scan_infected(self) -> None:
        self.virus_scan_status = VirusScanStatus.INFECTED
        self.virus_scan_at = timezone.now()
        self.save(update_fields=["virus_scan_status", "virus_scan_at", "updated_at"])


# ---------------------------------------------------------------------------
# FileVariant  (derived representations: thumbnails, previews, …)
# ---------------------------------------------------------------------------

class FileVariant(UUIDModel, TimeStampedModel):
    """A derived version of a :class:`FileMetadata` blob.

    Variants are generated asynchronously by the processing pipeline
    (Celery tasks).  Examples: 100×100 JPEG thumbnail, PDF preview page,
    watermarked copy, transcoded video.

    Variants are NOT soft-deleted — they are hard-deleted when the source
    FileMetadata is deleted.  They carry no tenant FK because tenancy is
    resolved through the ``source`` relation.
    """

    source = models.ForeignKey(
        FileMetadata,
        on_delete=models.CASCADE,
        related_name="variants",
        verbose_name=_("source file"),
    )
    kind = models.CharField(
        _("kind"),
        max_length=16,
        choices=FileVariantKind.choices,
        db_index=True,
    )
    status = models.CharField(
        _("status"),
        max_length=16,
        choices=FileVariantStatus.choices,
        default=FileVariantStatus.PENDING,
        db_index=True,
    )

    # --- Storage ----------------------------------------------------------
    storage_backend = models.CharField(_("backend"), max_length=32, default="local")
    path = models.CharField(_("storage path"), max_length=1024, blank=True, default="")
    content_type = models.CharField(_("content type"), max_length=128, blank=True)
    size_bytes = models.PositiveBigIntegerField(_("size"), default=0)

    # --- Image / video dimensions -----------------------------------------
    width = models.PositiveIntegerField(_("width"), null=True, blank=True)
    height = models.PositiveIntegerField(_("height"), null=True, blank=True)

    # --- Generator info + extensibility -----------------------------------
    generator = models.CharField(
        _("generator"),
        max_length=64,
        blank=True,
        default="",
        help_text=_("E.g. 'pillow', 'ffmpeg', 'external_service'."),
    )
    extra = models.JSONField(
        _("extra"),
        default=dict,
        blank=True,
        help_text=_("Provider-specific metadata (page number, bitrate, etc.)."),
    )
    error_message = models.TextField(_("error message"), blank=True, default="")

    class Meta:
        verbose_name = _("file variant")
        verbose_name_plural = _("file variants")
        ordering = ("kind",)
        constraints = (
            models.UniqueConstraint(
                fields=("source", "kind"),
                name="storage_file_variant_unique_kind_per_source",
            ),
        )
        indexes = (
            models.Index(fields=("source", "kind", "status")),
        )

    def __str__(self) -> str:
        return f"{self.get_kind_display()} of FileMetadata#{self.source_id}"


# ---------------------------------------------------------------------------
# UploadSession  (resumable / multi-chunk upload coordination)
# ---------------------------------------------------------------------------

class UploadSession(UUIDModel, TenantScopedModel):
    """Coordinates a multi-part or resumable upload operation.

    A session is created before the first chunk is sent.  As chunks arrive
    they increment ``received_chunks``.  When all chunks have been received
    the service layer assembles them into a :class:`FileMetadata` record and
    sets ``completed_file``.

    Expired sessions (``expires_at < now()``) are cleaned up by a periodic
    Celery task.  The assembled partial data on the provider is also deleted
    during cleanup.

    Single-request uploads do NOT need a session — use ``store_file``
    directly.
    """

    # --- Identification ---------------------------------------------------
    filename = models.CharField(_("filename"), max_length=256)
    content_type = models.CharField(_("content type"), max_length=128, blank=True)
    total_size_bytes = models.PositiveBigIntegerField(_("total size"))
    total_chunks = models.PositiveIntegerField(
        _("total chunks"),
        default=1,
        help_text=_("Expected number of chunks. 1 = single-part."),
    )
    received_chunks = models.PositiveIntegerField(_("received chunks"), default=0)

    # --- Lifecycle --------------------------------------------------------
    status = models.CharField(
        _("status"),
        max_length=16,
        choices=UploadSessionStatus.choices,
        default=UploadSessionStatus.INITIATED,
        db_index=True,
    )
    expires_at = models.DateTimeField(
        _("expires at"),
        db_index=True,
        help_text=_("Session and any partial data are cleaned up after this time."),
    )

    # --- Target storage key (pre-computed, stable across chunks) ----------
    storage_key = models.CharField(
        _("storage key"),
        max_length=1024,
        help_text=_(
            "Pre-computed UUID-based path where the assembled file will be stored. "
            "Passed to safe_tenant_path as sub_key."
        ),
    )

    # --- Optional checksum for integrity verification on completion -------
    expected_checksum_sha256 = models.CharField(
        _("expected sha256"),
        max_length=64,
        blank=True,
        default="",
        help_text=_("If provided, the assembled file checksum is verified against this."),
    )

    # --- Ownership / routing ----------------------------------------------
    uploaded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("uploaded by"),
    )
    folder = models.ForeignKey(
        StorageFolder,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("target folder"),
    )
    app_context = models.CharField(
        _("app context"),
        max_length=64,
        blank=True,
        default="",
    )

    # --- Result -----------------------------------------------------------
    completed_file = models.ForeignKey(
        FileMetadata,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="upload_sessions",
        verbose_name=_("completed file"),
    )

    class Meta:
        verbose_name = _("upload session")
        verbose_name_plural = _("upload sessions")
        ordering = ("-created_at",)
        indexes = (
            models.Index(fields=("tenant", "status", "expires_at")),
            models.Index(fields=("tenant", "uploaded_by", "status")),
        )

    def __str__(self) -> str:
        return f"UploadSession {self.public_id} ({self.status})"

    @property
    def is_expired(self) -> bool:
        return timezone.now() >= self.expires_at

    @property
    def all_chunks_received(self) -> bool:
        return self.received_chunks >= self.total_chunks

