import os
import uuid

from django.conf import settings
from django.db import models

from .storage import private_storage


def document_upload_path(instance, filename):
    """Random path inside the private store. Not a security boundary (files are
    never served by path — the download view is); the randomness just avoids
    collisions and leaks nothing from the original filename."""
    extension = os.path.splitext(filename)[1].lower()
    return f"documents/{uuid.uuid4().hex}{extension}"


# A calm, distinguishable set. Categories without a colour fall back to one of
# these deterministically, so a new category is never colourless and the same
# category always looks the same.
CATEGORY_PALETTE = [
    "#0f5f6b",  # teal
    "#1f5f8b",  # blue
    "#7a4b8c",  # plum
    "#9a5b12",  # amber
    "#1c7a4e",  # green
    "#a3423c",  # clay
    "#4a5a86",  # slate blue
    "#8a6d1f",  # brass
]


class Category(models.Model):
    """Document category (per instance; e.g. Minutes, Financials, Insurance)."""

    name = models.CharField(max_length=120)
    slug = models.SlugField(max_length=140, unique=True)
    order = models.PositiveIntegerField(default=0)
    color = models.CharField(
        max_length=7, blank=True,
        help_text="Hex colour like #0f5f6b. Left blank, one is chosen for you.",
    )

    class Meta:
        ordering = ["order", "name"]
        verbose_name_plural = "categories"

    def __str__(self):
        return self.name

    @property
    def colour(self):
        """The colour to paint this category, chosen if not set.

        Falls back on the primary key rather than a hash of the name: names
        hash into collisions easily, and four categories sharing one colour
        defeats the point of colouring them at all.
        """
        if self.color:
            return self.color
        return CATEGORY_PALETTE[(self.pk or 0) % len(CATEGORY_PALETTE)]


class Tag(models.Model):
    """Cross-cutting label (a year, "AGM", "proxy") — orthogonal to category."""

    name = models.CharField(max_length=60, unique=True)
    slug = models.SlugField(max_length=70, unique=True)

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name


class Collection(models.Model):
    """A batch of documents that belong together.

    Documents overwhelmingly arrive in groups — the papers from one AGM, a
    year's insurance renewal, the board's bios — and were previously uploaded
    one at a time with no relationship between them. A collection is that
    relationship, and it is what the library browses by.
    """

    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220)
    # Deliberately no category. A batch legitimately holds several kinds of
    # document — an AGM produces minutes, financials and a proxy form — so a
    # category on the group would be a half-truth, and "apply to the documents
    # in this group" would be ambiguous when the group is being viewed from
    # inside one of several categories. The category lives on the document.
    summary = models.TextField(blank=True)
    occurred_on = models.DateField(
        null=True, blank=True,
        help_text="The date these documents relate to (e.g. the AGM date).",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    # Trashed rather than deleted: a group removed by mistake takes its
    # documents with it, and rebuilding one by hand is miserable.
    removed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-occurred_on", "-created_at"]
        unique_together = [("slug",)]

    def __str__(self):
        return self.title

    @property
    def document_count(self):
        return self.documents.filter(published=True, removed_at=None).count()


class Document(models.Model):
    # Source WordPress post ID — the idempotency key for the migration, so an
    # import can be rehearsed, corrected and re-run without duplicating records.
    wp_id = models.CharField(max_length=32, blank=True, null=True, unique=True, db_index=True)
    collection = models.ForeignKey(
        Collection, on_delete=models.SET_NULL, null=True, blank=True,
        related_name="documents",
    )
    tags = models.ManyToManyField(Tag, blank=True, related_name="documents")
    title = models.CharField(max_length=255)
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
        related_name="documents",
    )
    summary = models.TextField(blank=True)
    file = models.FileField(storage=private_storage, upload_to=document_upload_path)
    published = models.BooleanField(
        default=True, help_text="Unpublished documents are visible to staff only."
    )
    published_date = models.DateField(null=True, blank=True)
    uploaded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    # The file stays on disk while a document is in the trash; only emptying
    # the trash removes it for good.
    removed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-published_date", "-created_at"]

    def __str__(self):
        return self.title

    @property
    def file_kind(self):
        """Short label for the file-type badge, so the list sorts by eye."""
        extension = os.path.splitext(self.file.name or "")[1].lower().lstrip(".")
        if extension == "pdf":
            return "pdf"
        if extension in {"doc", "docx", "odt", "rtf"}:
            return "doc"
        if extension in {"xls", "xlsx", "csv", "ods"}:
            return "xls"
        if extension in {"ppt", "pptx"}:
            return "ppt"
        return extension[:4] or "file"


class BoardMember(models.Model):
    """Board of directors entry — editable in the admin, no hand-edited HTML.

    The photo uses the same private storage as documents (the board page is
    behind login), so it is served through the gated photo view, never a public
    media URL. Contact details are deliberately NOT rendered into page markup.
    """

    name = models.CharField(max_length=160)
    title = models.CharField(max_length=160, blank=True)
    bio = models.TextField(blank=True)
    photo = models.ImageField(
        storage=private_storage, upload_to="board/", blank=True, null=True
    )
    contact_email = models.EmailField(
        blank=True, help_text="Staff reference only — never rendered on the site."
    )
    order = models.PositiveIntegerField(default=0)
    active = models.BooleanField(default=True)
    # Removal is reversible on purpose: a board member deleted by mistake is
    # awkward to reconstruct, so they go to a recoverable list instead.
    removed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["order", "name"]

    def __str__(self):
        return self.name

    @property
    def bio_html(self):
        """The bio as safe HTML, whichever way it was stored.

        Imported bios are plain text with blank-line paragraphs; bios edited in
        the staff dialog are already sanitised HTML from clean_html. Escaping
        plus linebreaks turns the former into the latter's shape.
        """
        from django.utils.html import linebreaks
        from django.utils.safestring import mark_safe

        body = (self.bio or "").strip()
        if not body:
            return ""
        if "<p" in body or "<br" in body or "<ul" in body or "<ol" in body:
            return mark_safe(body)
        return mark_safe(linebreaks(body, autoescape=True))

    @property
    def bio_preview(self):
        """First couple of sentences, with any markup stripped."""
        import re as _re
        text = _re.sub(r"<[^>]+>", " ", self.bio or "")
        text = _re.sub(r"\s+", " ", text).strip()
        return text[:220] + ("…" if len(text) > 220 else "")


class DownloadLog(models.Model):
    """One row per served download — the activity trail and rate-limit source."""

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
    )
    document = models.ForeignKey(
        Document, on_delete=models.SET_NULL, null=True, blank=True
    )
    which = models.CharField(max_length=16, default="primary")
    ip = models.GenericIPAddressField(null=True, blank=True)
    downloaded_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-downloaded_at"]
        indexes = [models.Index(fields=["user", "downloaded_at"])]

    def __str__(self):
        return f"{self.user_id} -> doc {self.document_id} @ {self.downloaded_at:%Y-%m-%d %H:%M}"


class LoginPhoto(models.Model):
    """A photograph for the sign-in scene.

    These are the ONLY deliberately public files in the app: the sign-in page
    is unauthenticated, so its backdrop has to be servable without a session.
    They are still kept outside the web root and served by primary key through
    a view, so nothing else in the private store becomes reachable.
    """

    image = models.ImageField(storage=private_storage, upload_to="login/")
    caption = models.CharField(max_length=160, blank=True)
    order = models.PositiveIntegerField(default=0)
    added_at = models.DateTimeField(auto_now_add=True)
    #: May this photograph be the FIRST one a visitor sees? A picture can be
    #: lovely in the rotation and still be an odd first impression (a close-up
    #: of food, say). Opt-in, with a deliberate fallback: when nothing is
    #: marked, any photograph may open — so the scene never depends on someone
    #: having curated it.
    is_starter = models.BooleanField(default=False)

    class Meta:
        ordering = ["order", "pk"]

    @classmethod
    def openers(cls):
        """The photographs allowed to appear first, or all of them if none
        have been marked."""
        starters = cls.objects.filter(is_starter=True)
        return starters if starters.exists() else cls.objects.all()

    def __str__(self):
        return self.caption or self.image.name.rsplit("/", 1)[-1]
