"""Base models with common fields, validation, and versioning."""

from __future__ import annotations

import hashlib
import json
import uuid
from typing import TYPE_CHECKING, Any

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

if TYPE_CHECKING:
    from django.db.models import QuerySet


class BaseModel(models.Model):
    """
    Base model that includes default created / updated timestamps.

    All LightWave Django models should extend this class.

    Validation is enforced on every save() by calling full_clean().
    This ensures Django's built-in field validators (EmailField, URLField, etc.)
    always run, not just when using ModelForms.
    """

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

    def save(self, *args: Any, **kwargs: Any) -> None:
        """Override save to enforce validation on every save."""
        self.full_clean()
        super().save(*args, **kwargs)


class VersionedContentMixin(models.Model):
    """
    Mixin for models that need version history tracking.

    Provides git-like versioning:
    - Immutable snapshots created on each save
    - Content hash for change detection
    - Version numbering
    - Audit trail with who made changes

    Usage:
        class Document(VersionedContentMixin, BaseModel):
            title = models.CharField(max_length=255)
            content = models.JSONField(default=dict)

            def get_versioned_fields(self) -> dict:
                return {
                    "title": self.title,
                    "content": self.content,
                }

    The mixin requires a related ContentVersion model to store snapshots.
    """

    # Current version number (increments on each content change)
    version_number = models.PositiveIntegerField(default=1)

    # Hash of versioned content for change detection
    content_hash = models.CharField(max_length=64, blank=True, db_index=True)

    # Who last modified
    last_modified_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="%(class)s_modifications",
    )

    class Meta:
        abstract = True

    def get_versioned_fields(self) -> dict[str, Any]:
        """
        Override this to specify which fields should be versioned.

        Returns:
            Dictionary of field names to values that should be tracked.
        """
        raise NotImplementedError("Subclasses must implement get_versioned_fields()")

    def compute_content_hash(self) -> str:
        """Compute SHA-256 hash of versioned content."""
        content = self.get_versioned_fields()
        # Sort keys for consistent hashing
        content_str = json.dumps(content, sort_keys=True, default=str)
        return hashlib.sha256(content_str.encode()).hexdigest()

    def has_content_changed(self) -> bool:
        """Check if versioned content has changed since last save."""
        new_hash = self.compute_content_hash()
        return new_hash != self.content_hash

    def save(self, *args: Any, **kwargs: Any) -> None:
        """Save with version tracking."""
        # Check if this is a new instance
        is_new = self.pk is None

        # Compute new content hash
        new_hash = self.compute_content_hash()

        # Determine if we need to create a new version
        should_version = is_new or (new_hash != self.content_hash)

        if should_version and not is_new:
            self.version_number += 1

        # Update content hash
        self.content_hash = new_hash

        # Save the model
        super().save(*args, **kwargs)

        # Create version snapshot if content changed
        if should_version:
            self._create_version_snapshot()

    def _create_version_snapshot(self) -> None:
        """
        Create an immutable version snapshot.

        Override this method to customize snapshot creation,
        or implement a related ContentVersion model.
        """
        # Default implementation stores in a generic way
        # Subclasses should override for custom version storage
        pass

    def get_version_history(self) -> "QuerySet":
        """
        Get version history for this content.

        Override to return the related version queryset.
        """
        raise NotImplementedError("Subclasses must implement get_version_history()")

    def restore_version(self, version_number: int) -> None:
        """
        Restore content to a previous version.

        Args:
            version_number: The version number to restore to.
        """
        raise NotImplementedError("Subclasses must implement restore_version()")


class ContentVersion(BaseModel):
    """
    Generic content version snapshot.

    This is a standalone model for storing version history.
    Can be used with VersionedContentMixin or independently.

    For type-specific versioning, create a dedicated version model
    (e.g., DocumentVersion) that references the parent model.
    """

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

    # Generic foreign key info (for flexibility)
    content_type = models.CharField(max_length=100, db_index=True)
    object_id = models.CharField(max_length=100, db_index=True)

    # Version info
    version_number = models.PositiveIntegerField()
    content_hash = models.CharField(max_length=64)

    # Snapshot data (immutable once created)
    snapshot = models.JSONField(default=dict)

    # Audit info
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="content_versions_created",
    )

    # Change metadata
    change_summary = models.TextField(blank=True)
    change_type = models.CharField(
        max_length=20,
        choices=[
            ("create", "Create"),
            ("minor", "Minor"),
            ("major", "Major"),
        ],
        default="minor",
    )

    class Meta:
        abstract = True  # Make abstract - concrete implementations in apps
        ordering = ["-version_number"]
        indexes = [
            models.Index(fields=["content_type", "object_id", "version_number"]),
        ]

    def __str__(self) -> str:
        return f"{self.content_type}:{self.object_id} v{self.version_number}"

    def save(self, *args: Any, **kwargs: Any) -> None:
        """Prevent modification of existing versions (immutability)."""
        if self.pk and ContentVersion.objects.filter(pk=self.pk).exists():
            raise ValueError("Content versions are immutable and cannot be modified.")
        super().save(*args, **kwargs)


class SoftDeleteMixin(models.Model):
    """
    Mixin for soft-delete functionality.

    Models with this mixin are never truly deleted - they're marked
    as deleted and filtered out of default querysets.

    Essential for compliance (legal docs can't be hard-deleted).
    """

    is_deleted = models.BooleanField(default=False, db_index=True)
    deleted_at = models.DateTimeField(null=True, blank=True)
    deleted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="%(class)s_deletions",
    )

    class Meta:
        abstract = True

    def soft_delete(self, user: Any = None) -> None:
        """Mark as deleted without removing from database."""
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.deleted_by = user
        self.save(update_fields=["is_deleted", "deleted_at", "deleted_by", "updated_at"])

    def restore(self) -> None:
        """Restore a soft-deleted record."""
        self.is_deleted = False
        self.deleted_at = None
        self.deleted_by = None
        self.save(update_fields=["is_deleted", "deleted_at", "deleted_by", "updated_at"])

    def hard_delete(self, force: bool = False) -> None:
        """
        Permanently delete the record.

        Args:
            force: Must be True to confirm hard deletion.

        Raises:
            ValueError: If force is not True.
        """
        if not force:
            raise ValueError("Hard delete requires force=True. Use soft_delete() for normal deletion.")
        super().delete()
