"""Generic cross-cutting models: Attachment, Comment, Activity, CustomField.

All four are :class:`TenantScopedModel` and use ``GenericForeignKey`` to
point at any other entity in the platform without coupling schemas. This lets
the CRM, Helpdesk, and any future module attach files, comments, activity
records, and custom field values to their entities by adding **zero** new
columns to their own tables.

Conventions
-----------
- ``content_type`` + ``object_id`` are always required and always indexed
  together so reverse lookups (``Attachment.objects.for_entity(obj)``) are
  fast.
- All four models follow soft-delete semantics where useful; ``Activity`` is
  append-only by design.
"""

from __future__ import annotations

from typing import ClassVar

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _

from simorgh.core.models import (
    ScopedSoftDeleteManager,
    SoftDeleteManager,
    SoftDeleteModel,
    TenantScopedModel,
    UUIDModel,
)

# ---------------------------------------------------------------------------
# Attachments
# ---------------------------------------------------------------------------


class AttachmentKind(models.TextChoices):
    INLINE = "inline", _("Inline")
    DOCUMENT = "document", _("Document")
    THUMBNAIL = "thumbnail", _("Thumbnail")
    BACKUP = "backup", _("Backup")
    OTHER = "other", _("Other")


class Attachment(UUIDModel, TenantScopedModel, SoftDeleteModel):
    """A file (``storage.FileMetadata``) bound to an arbitrary entity."""

    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    object_id = models.CharField(_("object id"), max_length=64, db_index=True)
    entity = GenericForeignKey("content_type", "object_id")

    file = models.ForeignKey(
        "storage.FileMetadata",
        on_delete=models.PROTECT,
        related_name="attachments",
    )
    kind = models.CharField(
        _("kind"),
        max_length=16,
        choices=AttachmentKind.choices,
        default=AttachmentKind.DOCUMENT,
    )
    description = models.CharField(_("description"), max_length=512, blank=True)
    sort_order = models.PositiveIntegerField(_("order"), default=0)
    uploaded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()

    class Meta:
        verbose_name = _("attachment")
        verbose_name_plural = _("attachments")
        ordering = ("sort_order", "-created_at")
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "content_type", "object_id")),
            models.Index(fields=("tenant", "kind")),
        ]

    def __str__(self) -> str:
        return f"attachment#{self.pk} → {self.content_type}#{self.object_id}"


# ---------------------------------------------------------------------------
# Comments
# ---------------------------------------------------------------------------


class Comment(UUIDModel, TenantScopedModel, SoftDeleteModel):
    """A threaded comment on any entity."""

    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    object_id = models.CharField(_("object id"), max_length=64, db_index=True)
    entity = GenericForeignKey("content_type", "object_id")

    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    body = models.TextField(_("body"))
    parent = models.ForeignKey(
        "self",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="replies",
    )
    mentions = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        related_name="comment_mentions",
    )
    edited_at = models.DateTimeField(_("edited at"), null=True, blank=True)

    objects: ClassVar[ScopedSoftDeleteManager] = ScopedSoftDeleteManager()
    soft_cascade: ClassVar[tuple[str, ...]] = ("replies",)

    class Meta:
        verbose_name = _("comment")
        verbose_name_plural = _("comments")
        ordering = ("created_at",)
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "content_type", "object_id", "created_at")),
            models.Index(fields=("tenant", "author", "-created_at")),
        ]

    def __str__(self) -> str:
        return f"comment#{self.pk} by {self.author_id} on {self.content_type}#{self.object_id}"


# ---------------------------------------------------------------------------
# Activity feed
# ---------------------------------------------------------------------------


class Activity(UUIDModel, TenantScopedModel):
    """Append-only activity record (the user-visible feed).

    Pattern follows the "actor verb object [target]" form popularised by
    ActivityStreams 2.0 — e.g. *Alice* (*verb*) *commented on* (*object*) *Ticket #42*.
    Optional *target* is a related entity (e.g. "Project X" for "Alice closed
    Ticket #42 in Project X").
    """

    actor = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    verb = models.CharField(_("verb"), max_length=64, db_index=True)

    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
        null=True,
        blank=True,
    )
    object_id = models.CharField(_("object id"), max_length=64, db_index=True, blank=True)
    entity = GenericForeignKey("content_type", "object_id")

    target_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.SET_NULL,
        related_name="+",
        null=True,
        blank=True,
    )
    target_object_id = models.CharField(
        _("target object id"), max_length=64, blank=True, db_index=True
    )
    target = GenericForeignKey("target_content_type", "target_object_id")

    occurred_at = models.DateTimeField(_("occurred at"), db_index=True)
    extra = models.JSONField(_("extra"), default=dict, blank=True)

    class Meta:
        verbose_name = _("activity")
        verbose_name_plural = _("activity")
        ordering = ("-occurred_at",)
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "actor", "-occurred_at")),
            models.Index(fields=("tenant", "content_type", "object_id", "-occurred_at")),
            models.Index(fields=("tenant", "verb", "-occurred_at")),
        ]

    def __str__(self) -> str:
        return f"{self.actor_id} {self.verb} {self.content_type}#{self.object_id}"


# ---------------------------------------------------------------------------
# Custom fields
# ---------------------------------------------------------------------------


class CustomFieldType(models.TextChoices):
    STRING = "string", _("String")
    TEXT = "text", _("Text")
    NUMBER = "number", _("Number")
    BOOLEAN = "boolean", _("Boolean")
    DATE = "date", _("Date")
    DATETIME = "datetime", _("Date & time")
    SELECT = "select", _("Select")
    MULTISELECT = "multiselect", _("Multi-select")
    JSON = "json", _("JSON")


class CustomFieldDefinition(UUIDModel, TenantScopedModel):
    """A custom field schema definition for a specific entity type.

    Each tenant can add their own fields to any entity (e.g., "Industry" on
    ``crm.contact``) without touching the entity's database table.
    """

    entity_type = models.CharField(_("entity type"), max_length=128, db_index=True)
    key = models.CharField(_("key"), max_length=64)
    label_key = models.CharField(_("label i18n key"), max_length=256)
    field_type = models.CharField(
        _("field type"),
        max_length=16,
        choices=CustomFieldType.choices,
    )
    is_required = models.BooleanField(_("required"), default=False)
    sort_order = models.PositiveIntegerField(_("order"), default=0)
    validation = models.JSONField(_("validation rules"), default=dict, blank=True)
    options = models.JSONField(_("options"), default=list, blank=True)
    help_text = models.CharField(_("help text"), max_length=512, blank=True)

    class Meta:
        verbose_name = _("custom field definition")
        verbose_name_plural = _("custom field definitions")
        ordering = ("entity_type", "sort_order", "key")
        constraints: ClassVar[list[models.UniqueConstraint]] = [
            models.UniqueConstraint(
                fields=("tenant", "entity_type", "key"),
                name="custom_field_unique_tenant_entity_key",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type")),
        ]

    def __str__(self) -> str:
        return f"{self.entity_type}.{self.key} ({self.field_type})"


class CustomFieldValue(UUIDModel, TenantScopedModel):
    """A custom-field value bound to a specific entity row."""

    definition = models.ForeignKey(
        CustomFieldDefinition,
        on_delete=models.CASCADE,
        related_name="values",
    )
    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    object_id = models.CharField(_("object id"), max_length=64, db_index=True)
    entity = GenericForeignKey("content_type", "object_id")
    value = models.JSONField(_("value"), null=True, blank=True)

    class Meta:
        verbose_name = _("custom field value")
        verbose_name_plural = _("custom field values")
        constraints: ClassVar[list[models.UniqueConstraint]] = [
            models.UniqueConstraint(
                fields=("definition", "content_type", "object_id"),
                name="custom_field_value_unique_def_target",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "content_type", "object_id")),
        ]

    def __str__(self) -> str:
        return f"{self.definition.key}={self.value!r} @ {self.content_type}#{self.object_id}"


# ---------------------------------------------------------------------------
# Tags (polymorphic)
# ---------------------------------------------------------------------------


class Tag(UUIDModel, TenantScopedModel):
    """A tenant-scoped label that can be attached to any entity.

    ``entity_type`` scopes the tag to a specific entity type
    (e.g. ``"crm.lead"``).  A ``None`` value means the tag is *global* and
    can be applied to any entity.

    ``slug`` is derived from ``name`` and is unique per tenant so that
    programmatic lookups (e.g. automation rules) can reference tags by slug
    without relying on mutable display names.
    """

    name = models.CharField(_("name"), max_length=64)
    slug = models.SlugField(
        _("slug"),
        max_length=64,
        allow_unicode=False,
        help_text=_("URL-friendly identifier, unique per tenant."),
    )
    color = models.CharField(
        _("color"),
        max_length=7,
        blank=True,
        default="#6B7280",
        help_text=_("Hex colour code, e.g. #3B82F6"),
    )
    entity_type = models.CharField(
        _("entity type"),
        max_length=128,
        blank=True,
        default="",
        help_text=_("Dotted app_label.model_name — blank means global."),
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )

    class Meta:
        verbose_name = _("tag")
        verbose_name_plural = _("tags")
        ordering = ("name",)
        constraints: ClassVar[list[models.UniqueConstraint]] = [
            models.UniqueConstraint(
                fields=("tenant", "slug"),
                name="platform_core_tag_unique_slug_per_tenant",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type")),
        ]

    def __str__(self) -> str:
        return self.name


class TagAssignment(UUIDModel, TenantScopedModel):
    """A many-to-many link between a :class:`Tag` and any entity."""

    tag = models.ForeignKey(
        Tag,
        on_delete=models.CASCADE,
        related_name="assignments",
    )
    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    object_id = models.CharField(_("object id"), max_length=64, db_index=True)
    entity = GenericForeignKey("content_type", "object_id")
    assigned_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )

    class Meta:
        verbose_name = _("tag assignment")
        verbose_name_plural = _("tag assignments")
        constraints: ClassVar[list[models.UniqueConstraint]] = [
            models.UniqueConstraint(
                fields=("tag", "content_type", "object_id"),
                name="platform_core_tag_assignment_unique",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "content_type", "object_id")),
            models.Index(fields=("tenant", "tag")),
        ]

    def __str__(self) -> str:
        return f"{self.tag} → {self.content_type}#{self.object_id}"


# ---------------------------------------------------------------------------
# Notes (private / team / workspace)
# ---------------------------------------------------------------------------


class NoteVisibility(models.TextChoices):
    PRIVATE = "private", _("Private")
    TEAM = "team", _("Team")
    WORKSPACE = "workspace", _("Workspace")


class Note(UUIDModel, TenantScopedModel):
    """A private or shared note that can be attached to any entity.

    ``visibility`` controls who can read the note:

    * ``private``   — only the author.
    * ``team``      — all members of the same organisation node.
    * ``workspace`` — all members of the tenant.
    """

    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    object_id = models.CharField(_("object id"), max_length=64, db_index=True)
    entity = GenericForeignKey("content_type", "object_id")

    body = models.TextField(_("body"))
    visibility = models.CharField(
        _("visibility"),
        max_length=16,
        choices=NoteVisibility.choices,
        default=NoteVisibility.PRIVATE,
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    is_pinned = models.BooleanField(_("pinned"), default=False)
    edited_at = models.DateTimeField(_("edited at"), null=True, blank=True)

    class Meta:
        verbose_name = _("note")
        verbose_name_plural = _("notes")
        ordering = ("-is_pinned", "-created_at")
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "content_type", "object_id")),
            models.Index(fields=("tenant", "author", "-created_at")),
        ]

    def __str__(self) -> str:
        return f"note#{self.pk} by {self.author_id} on {self.content_type}#{self.object_id}"


# ---------------------------------------------------------------------------
# Mentions (@user)
# ---------------------------------------------------------------------------


class Mention(UUIDModel, TenantScopedModel):
    """Records a @username mention inside a :class:`Comment` or :class:`Note`.

    Only one of *comment* / *note* is set; the other is NULL.  The *notified*
    flag is set to ``True`` once a notification has been successfully dispatched
    so that re-edits do not trigger duplicate notifications for the same user.
    """

    comment = models.ForeignKey(
        "Comment",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="mention_records",
    )
    note = models.ForeignKey(
        "Note",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="mention_records",
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="+",
        verbose_name=_("mentioned user"),
    )
    notified = models.BooleanField(_("notified"), default=False)

    class Meta:
        verbose_name = _("mention")
        verbose_name_plural = _("mentions")
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("comment", "user"),
                condition=models.Q(comment__isnull=False),
                name="platform_core_mention_unique_comment_user",
            ),
            models.UniqueConstraint(
                fields=("note", "user"),
                condition=models.Q(note__isnull=False),
                name="platform_core_mention_unique_note_user",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "user")),
            models.Index(fields=("tenant", "comment")),
            models.Index(fields=("tenant", "note")),
        ]

    def __str__(self) -> str:
        target = f"comment#{self.comment_id}" if self.comment_id else f"note#{self.note_id}"
        return f"mention of user#{self.user_id} in {target}"


# ---------------------------------------------------------------------------
# Reference Number Sequences
# ---------------------------------------------------------------------------


class ReferenceNumberSequence(UUIDModel, TenantScopedModel):
    """Monotonically-increasing reference-number generator scoped to an entity type.

    Each tenant can configure one sequence per entity type (e.g.
    ``"helpdesk.ticket"`` → ``TK-0001``).  The counter is incremented inside
    a ``SELECT FOR UPDATE`` transaction so concurrent creation never produces
    duplicate references.

    Format string variables
    -----------------------
    * ``{prefix}`` — the :attr:`prefix` value
    * ``{year}``   — 4-digit year (Gregorian by default; Jalali when
                     :attr:`use_jalali_year` is ``True``)
    * ``{seq}``    — current sequence number (supports format spec, e.g.
                     ``{seq:04d}`` for zero-padded 4-digit numbers)

    Examples
    --------
    * ``"{prefix}-{seq:04d}"``        → ``TK-0001``
    * ``"{prefix}-{year}-{seq:04d}"`` → ``TK-2025-0001``
    """

    entity_type = models.CharField(
        _("entity type"),
        max_length=128,
        db_index=True,
        help_text=_("Dotted app_label.model_name, e.g. 'helpdesk.ticket'."),
    )
    prefix = models.CharField(
        _("prefix"),
        max_length=32,
        help_text=_("Short code prepended to the number, e.g. 'TK'."),
    )
    format = models.CharField(
        _("format"),
        max_length=128,
        default="{prefix}-{seq:04d}",
        help_text=_(
            "Python format string. Variables: {prefix}, {year}, {seq}. "
            "Example: '{prefix}-{year}-{seq:04d}'"
        ),
    )
    last_number = models.PositiveIntegerField(_("last number"), default=0)
    reset_yearly = models.BooleanField(
        _("reset yearly"),
        default=False,
        help_text=_("If True, the counter resets to 0 at the start of each year."),
    )
    last_reset_year = models.PositiveSmallIntegerField(
        _("last reset year"),
        null=True,
        blank=True,
        help_text=_("Year in which the counter was last reset (for reset_yearly)."),
    )
    use_jalali_year = models.BooleanField(
        _("use Jalali year"),
        default=False,
        help_text=_("Use the Persian (Jalali/Shamsi) year in the {year} variable."),
    )
    is_active = models.BooleanField(_("active"), default=True)

    class Meta:
        verbose_name = _("reference number sequence")
        verbose_name_plural = _("reference number sequences")
        ordering = ("entity_type",)
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("tenant", "entity_type"),
                name="platform_core_ref_seq_unique_tenant_entity",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type", "is_active")),
        ]

    def __str__(self) -> str:
        return f"{self.prefix} / {self.entity_type} (last={self.last_number})"


# ---------------------------------------------------------------------------
# Dynamic Forms
# ---------------------------------------------------------------------------


class FormFieldType(models.TextChoices):
    TEXT = "text", _("Text")
    TEXTAREA = "textarea", _("Text area")
    NUMBER = "number", _("Number")
    DATE = "date", _("Date")
    DATETIME = "datetime", _("Date & time")
    SELECT = "select", _("Select")
    MULTISELECT = "multiselect", _("Multi-select")
    RADIO = "radio", _("Radio")
    CHECKBOX = "checkbox", _("Checkbox")
    FILE = "file", _("File upload")
    EMAIL = "email", _("Email")
    PHONE = "phone", _("Phone")
    URL = "url", _("URL")
    RATING = "rating", _("Rating")


class FormDefinition(UUIDModel, TenantScopedModel):
    """A tenant-scoped dynamic form schema.

    ``fields`` is a JSON array of field descriptors::

        [
          {
            "id": "full_name",
            "type": "text",
            "label": "Full name",
            "required": true,
            "options": [],
            "validation": {"max_length": 200}
          },
          ...
        ]

    ``submit_action`` controls what happens when the form is submitted:

    * ``"webhook"``      — POST payload to ``submit_config["url"]``
    * ``"create_ticket"``— create a helpdesk ticket (submit_config: category etc.)
    * ``"create_lead"``  — create a CRM lead
    * ``"notify_only"``  — fire an event but take no automated action

    ``public_url_slug`` makes the form accessible via ``GET /f/{slug}/``
    without authentication.  A blank slug means the form is internal-only.
    """

    title = models.CharField(_("title"), max_length=256)
    description = models.TextField(_("description"), blank=True)
    entity_type = models.CharField(
        _("entity type"),
        max_length=128,
        blank=True,
        default="",
        help_text=_("Dotted app_label.model_name this form targets, or blank."),
    )
    fields = models.JSONField(
        _("fields"),
        default=list,
        help_text=_(
            "Ordered list of field definitions: {id, type, label, required, options, validation}."
        ),
    )
    submit_action = models.CharField(
        _("submit action"),
        max_length=64,
        default="notify_only",
        help_text=_("Action taken on submission: webhook, create_ticket, create_lead, notify_only."),
    )
    submit_config = models.JSONField(
        _("submit config"),
        default=dict,
        blank=True,
        help_text=_("Action-specific configuration (e.g. {url} for webhook)."),
    )
    is_active = models.BooleanField(_("active"), default=True)
    public_url_slug = models.SlugField(
        _("public URL slug"),
        max_length=128,
        blank=True,
        default="",
        allow_unicode=False,
        help_text=_("If set, form is publicly accessible at /f/{slug}/. Unique per tenant."),
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )

    class Meta:
        verbose_name = _("form definition")
        verbose_name_plural = _("form definitions")
        ordering = ("-created_at",)
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("tenant", "public_url_slug"),
                condition=models.Q(public_url_slug__gt=""),
                name="platform_core_form_unique_slug_per_tenant",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "is_active")),
            models.Index(fields=("tenant", "public_url_slug")),
        ]

    def __str__(self) -> str:
        return self.title


class FormSubmission(UUIDModel, TenantScopedModel):
    """A single submission of a :class:`FormDefinition`.

    ``submitted_by`` is NULL for anonymous (public) submissions.
    ``ip_address`` is recorded for audit/spam-prevention purposes.
    ``processed`` is set to ``True`` once any configured ``submit_action``
    has been executed successfully.
    """

    form = models.ForeignKey(
        FormDefinition,
        on_delete=models.PROTECT,
        related_name="submissions",
    )
    data = models.JSONField(
        _("data"),
        default=dict,
        help_text=_("Key-value mapping of field id → submitted value."),
    )
    submitted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    ip_address = models.GenericIPAddressField(
        _("IP address"),
        null=True,
        blank=True,
        unpack_ipv4=True,
    )
    processed = models.BooleanField(
        _("processed"),
        default=False,
        help_text=_("True once all post-submit actions have been executed."),
    )

    class Meta:
        verbose_name = _("form submission")
        verbose_name_plural = _("form submissions")
        ordering = ("-created_at",)
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "form", "-created_at")),
            models.Index(fields=("tenant", "submitted_by", "-created_at")),
        ]

    def __str__(self) -> str:
        return f"submission#{self.pk} for form '{self.form_id}'"


# ---------------------------------------------------------------------------
# Import Engine
# ---------------------------------------------------------------------------


class ImportJobStatus(models.TextChoices):
    PENDING = "pending", _("Pending")
    VALIDATING = "validating", _("Validating")
    PROCESSING = "processing", _("Processing")
    DONE = "done", _("Done")
    FAILED = "failed", _("Failed")


class ImportJob(UUIDModel, TenantScopedModel):
    """Tracks a bulk-import of entities from a CSV or Excel file.

    Lifecycle::

        PENDING → VALIDATING → PROCESSING → DONE
                                          ↘ FAILED

    ``error_report`` is a JSON array of per-row errors::

        [{"row": 3, "errors": ["name is required", "age must be a number"]}, ...]

    The actual processing is done by the Celery task
    ``simorgh.apps.platform_core.tasks.process_import_job``.
    """

    entity_type = models.CharField(
        _("entity type"),
        max_length=128,
        db_index=True,
        help_text=_('Dot-notation entity identifier, e.g. "crm.lead".'),
    )
    file = models.ForeignKey(
        "storage.FileMetadata",
        on_delete=models.PROTECT,
        related_name="+",
        verbose_name=_("source file"),
        help_text=_("The uploaded CSV or Excel file."),
    )
    status = models.CharField(
        _("status"),
        max_length=16,
        choices=ImportJobStatus.choices,
        default=ImportJobStatus.PENDING,
        db_index=True,
    )
    total_rows = models.PositiveIntegerField(_("total rows"), default=0)
    processed_rows = models.PositiveIntegerField(_("processed rows"), default=0)
    error_rows = models.PositiveIntegerField(_("error rows"), default=0)
    error_report = models.JSONField(
        _("error report"),
        default=list,
        help_text=_('JSON array of [{row, errors}] describing row-level failures.'),
    )
    started_at = models.DateTimeField(_("started at"), null=True, blank=True)
    finished_at = models.DateTimeField(_("finished at"), null=True, blank=True)
    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 = _("import job")
        verbose_name_plural = _("import jobs")
        ordering = ("-created_at",)
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type", "-created_at")),
            models.Index(fields=("tenant", "status", "-created_at")),
        ]

    def __str__(self) -> str:
        return f"ImportJob#{self.pk} [{self.entity_type}] {self.status}"


# ---------------------------------------------------------------------------
# Export Engine
# ---------------------------------------------------------------------------


class ExportFormat(models.TextChoices):
    CSV = "csv", _("CSV")
    XLSX = "xlsx", _("Excel (XLSX)")
    JSON = "json", _("JSON")


class ExportJobStatus(models.TextChoices):
    PENDING = "pending", _("Pending")
    PROCESSING = "processing", _("Processing")
    DONE = "done", _("Done")
    FAILED = "failed", _("Failed")


class ExportJob(UUIDModel, TenantScopedModel):
    """Tracks an async bulk-export of entities to CSV, Excel, or JSON.

    Lifecycle::

        PENDING → PROCESSING → DONE
                             ↘ FAILED

    When DONE, ``file`` is set to the resulting :class:`~storage.FileMetadata`.
    The Celery task ``platform_core.process_export_job`` drives the lifecycle.
    """

    entity_type = models.CharField(
        _("entity type"),
        max_length=128,
        db_index=True,
        help_text=_('Dot-notation entity identifier, e.g. "crm.lead".'),
    )
    filters = models.JSONField(
        _("filters"),
        default=dict,
        blank=True,
        help_text=_("Arbitrary filter parameters passed to the exporter queryset function."),
    )
    format = models.CharField(
        _("format"),
        max_length=8,
        choices=ExportFormat.choices,
        default=ExportFormat.CSV,
    )
    status = models.CharField(
        _("status"),
        max_length=16,
        choices=ExportJobStatus.choices,
        default=ExportJobStatus.PENDING,
        db_index=True,
    )
    file = models.ForeignKey(
        "storage.FileMetadata",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("result file"),
        help_text=_("Set once the export file has been written to storage."),
    )
    row_count = models.PositiveIntegerField(_("row count"), default=0)
    error_message = models.TextField(_("error message"), blank=True, default="")
    started_at = models.DateTimeField(_("started at"), null=True, blank=True)
    finished_at = models.DateTimeField(_("finished at"), null=True, blank=True)
    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 = _("export job")
        verbose_name_plural = _("export jobs")
        ordering = ("-created_at",)
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type", "-created_at")),
            models.Index(fields=("tenant", "status", "-created_at")),
        ]

    def __str__(self) -> str:
        return f"ExportJob#{self.pk} [{self.entity_type}] {self.status}"


# ---------------------------------------------------------------------------
# Print Engine
# ---------------------------------------------------------------------------


class PaperSize(models.TextChoices):
    A4 = "A4", "A4"
    A5 = "A5", "A5"
    LETTER = "Letter", "Letter"


class PageOrientation(models.TextChoices):
    PORTRAIT = "portrait", _("Portrait")
    LANDSCAPE = "landscape", _("Landscape")


class PrintTemplate(UUIDModel, TenantScopedModel):
    """Jinja2-based HTML template used to render a PDF for a given entity type.

    Each tenant can maintain multiple templates per ``entity_type``
    (e.g., several invoice designs).  The ``is_active`` flag controls
    which templates are offered in the UI.
    """

    entity_type = models.CharField(
        max_length=128,
        db_index=True,
        verbose_name=_("entity type"),
        help_text=_("Dot-notation entity type, e.g. 'crm.invoice'."),
    )
    name = models.CharField(max_length=255, verbose_name=_("name"))
    html_template = models.TextField(
        verbose_name=_("HTML template"),
        help_text=_("Jinja2 template — use {{ entity.field }} syntax."),
    )
    css = models.TextField(
        blank=True,
        default="",
        verbose_name=_("CSS"),
        help_text=_("Extra CSS injected into the rendered HTML."),
    )
    paper_size = models.CharField(
        max_length=16,
        choices=PaperSize.choices,
        default=PaperSize.A4,
        verbose_name=_("paper size"),
    )
    orientation = models.CharField(
        max_length=16,
        choices=PageOrientation.choices,
        default=PageOrientation.PORTRAIT,
        verbose_name=_("orientation"),
    )
    is_active = models.BooleanField(default=True, verbose_name=_("is active"))
    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 = _("print template")
        verbose_name_plural = _("print templates")
        ordering = ("entity_type", "name")
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type", "is_active")),
        ]

    def __str__(self) -> str:
        return f"{self.name} [{self.entity_type}]"


# ---------------------------------------------------------------------------
# Template Engine (Message Templates)
# ---------------------------------------------------------------------------


class MessageChannel(models.TextChoices):
    EMAIL = "email", _("Email")
    SMS = "sms", _("SMS")
    PUSH = "push", _("Push")
    INAPP = "inapp", _("In-App")


class MessageTemplate(UUIDModel, TenantScopedModel):
    """Tenant-scoped message template rendered by Jinja2.

    Each template is identified by a unique *code* within its tenant so
    callers can refer to it by a stable slug rather than a numeric PK.
    """

    code = models.CharField(
        max_length=128,
        db_index=True,
        verbose_name=_("code"),
    )
    name = models.CharField(max_length=255, verbose_name=_("name"))
    channel = models.CharField(
        max_length=16,
        choices=MessageChannel.choices,
        default=MessageChannel.EMAIL,
        db_index=True,
        verbose_name=_("channel"),
    )
    subject = models.CharField(
        max_length=512,
        blank=True,
        default="",
        verbose_name=_("subject"),
        help_text=_("Used for email channel; may contain Jinja2 variables."),
    )
    body = models.TextField(verbose_name=_("body"))
    variables = models.JSONField(
        default=list,
        blank=True,
        verbose_name=_("variables"),
        help_text=_("List of {name, type, description} dicts describing available template variables."),
    )
    language = models.CharField(
        max_length=16,
        default="en",
        db_index=True,
        verbose_name=_("language"),
        help_text=_("ISO 639-1 language code (e.g. en, fa, ar)."),
    )
    is_active = models.BooleanField(default=True, verbose_name=_("is active"))
    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 = _("message template")
        verbose_name_plural = _("message templates")
        ordering = ("code",)
        constraints = [
            models.UniqueConstraint(
                fields=("tenant", "code", "language"),
                name="platform_core_msg_tmpl_tenant_code_lang_uniq",
            )
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "channel", "is_active")),
            models.Index(fields=("tenant", "code", "language")),
        ]

    def __str__(self) -> str:
        return f"{self.code} [{self.channel}][{self.language}]"


# ---------------------------------------------------------------------------
# Document Template (reusable document layouts for PDF generation)
# ---------------------------------------------------------------------------


class DocumentTemplate(UUIDModel, TenantScopedModel):
    """A reusable document template that defines structured document layouts.

    Unlike :class:`PrintTemplate` which is a raw Jinja2 HTML template for a
    specific entity type, a :class:`DocumentTemplate` represents a higher-level
    document definition with named sections, variables, and metadata. Other
    modules register their templates against these document templates.

    Examples: "Standard Invoice", "Employment Contract", "Purchase Order".
    """

    code = models.CharField(
        max_length=128,
        db_index=True,
        verbose_name=_("code"),
        help_text=_("Unique code per tenant, e.g. 'invoice_standard'."),
    )
    name = models.CharField(max_length=255, verbose_name=_("name"))
    description = models.TextField(
        blank=True,
        default="",
        verbose_name=_("description"),
    )
    entity_type = models.CharField(
        max_length=128,
        db_index=True,
        verbose_name=_("entity type"),
        help_text=_("Dot-notation entity type this document template targets."),
    )
    sections = models.JSONField(
        default=list,
        blank=True,
        verbose_name=_("sections"),
        help_text=_(
            "Ordered list of document sections. Each section has: "
            "{id, title_key, type (header/body/footer), content (Jinja2 HTML), "
            "variables, sort_order}."
        ),
    )
    variables = models.JSONField(
        default=list,
        blank=True,
        verbose_name=_("variables"),
        help_text=_("List of {name, type, description, required} dicts describing available variables."),
    )
    css = models.TextField(
        blank=True,
        default="",
        verbose_name=_("CSS"),
        help_text=_("Global CSS injected into the rendered document."),
    )
    paper_size = models.CharField(
        max_length=16,
        choices=PaperSize.choices,
        default=PaperSize.A4,
        verbose_name=_("paper size"),
    )
    orientation = models.CharField(
        max_length=16,
        choices=PageOrientation.choices,
        default=PageOrientation.PORTRAIT,
        verbose_name=_("orientation"),
    )
    is_active = models.BooleanField(default=True, verbose_name=_("is active"))
    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 = _("document template")
        verbose_name_plural = _("document templates")
        ordering = ("entity_type", "code")
        constraints = [
            models.UniqueConstraint(
                fields=("tenant", "code"),
                name="platform_core_doc_tmpl_tenant_code_uniq",
            )
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type", "is_active")),
            models.Index(fields=("tenant", "code")),
        ]

    def __str__(self) -> str:
        return f"{self.name} [{self.entity_type}]"


# ---------------------------------------------------------------------------
# Export Layout (visual formatting for data exports)
# ---------------------------------------------------------------------------


class ExportLayout(UUIDModel, TenantScopedModel):
    """A visual layout configuration for data exports.

    Defines how exported data should be formatted: column ordering, cell
    formatting, grouping, aggregation, and output styling. Used by the
    Export Engine to produce formatted CSV, Excel, and PDF exports.

    Unlike :class:`ExportSpec` in ``export_registry.py`` which defines
    *what* data to export, :class:`ExportLayout` defines *how* the
    exported data should look.
    """

    code = models.CharField(
        max_length=128,
        db_index=True,
        verbose_name=_("code"),
        help_text=_("Unique code per tenant, e.g. 'invoice_detailed'."),
    )
    name = models.CharField(max_length=255, verbose_name=_("name"))
    entity_type = models.CharField(
        max_length=128,
        db_index=True,
        verbose_name=_("entity type"),
        help_text=_("Dot-notation entity type this layout targets."),
    )
    export_format = models.CharField(
        max_length=16,
        choices=ExportFormat.choices,
        default=ExportFormat.CSV,
        verbose_name=_("export format"),
    )
    columns = models.JSONField(
        default=list,
        blank=True,
        verbose_name=_("columns"),
        help_text=_(
            "Ordered list of column definitions: "
            "{key, label, width, align, format, visible, transform}."
        ),
    )
    grouping = models.JSONField(
        default=list,
        blank=True,
        verbose_name=_("grouping"),
        help_text=_("List of field names to group rows by."),
    )
    aggregation = models.JSONField(
        default=dict,
        blank=True,
        verbose_name=_("aggregation"),
        help_text=_('Aggregation rules per column: {column_key: "sum"|"avg"|"count"|"min"|"max"}'),
    )
    styling = models.JSONField(
        default=dict,
        blank=True,
        verbose_name=_("styling"),
        help_text=_(
            "Visual styling: {header_bg, header_font, body_font_size, "
            "zebra_striping, borders, cell_padding}."
        ),
    )
    header_footer = models.JSONField(
        default=dict,
        blank=True,
        verbose_name=_("header/footer"),
        help_text=_(
            "Header and footer definitions: "
            "{header: {title, subtitle, logo_url}, footer: {text, page_numbers}}."
        ),
    )
    page_setup = models.JSONField(
        default=dict,
        blank=True,
        verbose_name=_("page setup"),
        help_text=_(
            "Page setup for PDF exports: {paper_size, orientation, margins, "
            "fit_to_page, repeat_header}."
        ),
    )
    is_default = models.BooleanField(default=False, verbose_name=_("is default"))
    is_active = models.BooleanField(default=True, verbose_name=_("is active"))
    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 = _("export layout")
        verbose_name_plural = _("export layouts")
        ordering = ("entity_type", "code")
        constraints = [
            models.UniqueConstraint(
                fields=("tenant", "code"),
                name="platform_core_export_layout_tenant_code_uniq",
            )
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "entity_type", "is_active")),
            models.Index(fields=("tenant", "export_format")),
        ]

    def __str__(self) -> str:
        return f"{self.name} [{self.entity_type}] ({self.export_format})"
