"""
PhotoMetadata and MetadataFile models for structured metadata handling.

This module provides proper models for:
1. PhotoMetadata - Structured EXIF/metadata extracted from photos
2. MetadataFile - XMP sidecars and other metadata files

Benefits over current approach:
- Typed fields instead of JSON blob (exif_json)
- Proper foreign key relationships
- Versioning for metadata changes
- Support for multiple metadata sources (embedded, sidecar, user-edited)
- Clear separation between camera metadata and derived metadata
"""

import numbers
import uuid
from fractions import Fraction

from django.db import models

from api.metadata.reader import get_metadata
from api.metadata.tags import Tags
from api.models.user import User, get_deleted_user


class MetadataFile(models.Model):
    """
    Represents a metadata sidecar file (XMP, JSON, etc.) associated with a photo.

    A photo can have multiple metadata files:
    - XMP sidecar from camera/software
    - Adobe Lightroom .xmp
    - Darktable .xmp
    - User-created metadata file
    """

    class FileType(models.TextChoices):
        XMP = "xmp", "XMP Sidecar"
        JSON = "json", "JSON Metadata"
        EXIF = "exif", "EXIF Extract"
        OTHER = "other", "Other"

    class Source(models.TextChoices):
        # Sidecar that came with the photo (same directory)
        ORIGINAL = "original", "Original Sidecar"
        # Generated by photo editing software
        SOFTWARE = "software", "Software Generated"
        # Created by LibrePhotos
        LIBREPHOTOS = "librephotos", "LibrePhotos Generated"
        # User manually created/uploaded
        USER = "user", "User Created"

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

    # Link to the File model for actual file storage
    file = models.OneToOneField(
        "File",
        on_delete=models.CASCADE,
        related_name="metadata_info",
    )

    # The photo this metadata belongs to
    photo = models.ForeignKey(
        "Photo",
        on_delete=models.CASCADE,
        related_name="metadata_files",
    )

    file_type = models.CharField(
        max_length=10,
        choices=FileType.choices,
        default=FileType.XMP,
    )

    source = models.CharField(
        max_length=20,
        choices=Source.choices,
        default=Source.ORIGINAL,
    )

    # Priority for metadata resolution (higher = more authoritative)
    # User edits > Software sidecars > Original sidecars > Embedded
    priority = models.IntegerField(default=0)

    # Software that created this file (if known)
    creator_software = models.CharField(max_length=100, blank=True, null=True)

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

    class Meta:
        ordering = ["-priority", "-updated_at"]
        verbose_name = "Metadata File"
        verbose_name_plural = "Metadata Files"

    def __str__(self):
        return f"{self.file_type} sidecar for {self.photo_id}"


class PhotoMetadata(models.Model):
    """
    Structured metadata for a photo.

    This model stores extracted and computed metadata in typed fields,
    making it queryable and more maintainable than a JSON blob.

    Metadata Sources (in priority order):
    1. User edits (highest priority)
    2. XMP sidecar files
    3. Embedded EXIF/IPTC/XMP
    4. Computed/derived values (lowest priority)
    """

    class Source(models.TextChoices):
        # Embedded in the file (EXIF, IPTC, embedded XMP)
        EMBEDDED = "embedded", "Embedded in File"
        # From XMP sidecar file
        SIDECAR = "sidecar", "XMP Sidecar"
        # User manually edited
        USER_EDIT = "user_edit", "User Edit"
        # Computed by LibrePhotos (AI, geocoding, etc.)
        COMPUTED = "computed", "Computed"

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

    photo = models.OneToOneField(
        "Photo",
        on_delete=models.CASCADE,
        related_name="metadata",
    )

    # ==================== Camera Settings ====================
    # Aperture (f-stop), e.g., 2.8
    aperture = models.FloatField(null=True, blank=True, db_index=True)

    # Shutter speed as a string fraction, e.g., "1/250"
    shutter_speed = models.CharField(max_length=20, null=True, blank=True)

    # Shutter speed in seconds (for queries), e.g., 0.004
    shutter_speed_seconds = models.FloatField(null=True, blank=True)

    # ISO sensitivity
    iso = models.IntegerField(null=True, blank=True, db_index=True)

    # Focal length in mm
    focal_length = models.FloatField(null=True, blank=True)

    # 35mm equivalent focal length
    focal_length_35mm = models.IntegerField(null=True, blank=True)

    # Exposure compensation in EV
    exposure_compensation = models.FloatField(null=True, blank=True)

    # Flash fired
    flash_fired = models.BooleanField(null=True, blank=True)

    # Metering mode
    metering_mode = models.CharField(max_length=50, null=True, blank=True)

    # White balance
    white_balance = models.CharField(max_length=50, null=True, blank=True)

    # ==================== Camera/Lens Info ====================
    camera_make = models.CharField(max_length=100, null=True, blank=True, db_index=True)
    camera_model = models.CharField(
        max_length=100, null=True, blank=True, db_index=True
    )
    lens_make = models.CharField(max_length=100, null=True, blank=True)
    lens_model = models.CharField(max_length=100, null=True, blank=True, db_index=True)
    serial_number = models.CharField(max_length=100, null=True, blank=True)

    # ==================== Image Properties ====================
    width = models.IntegerField(null=True, blank=True)
    height = models.IntegerField(null=True, blank=True)
    orientation = models.IntegerField(null=True, blank=True)
    color_space = models.CharField(max_length=50, null=True, blank=True)
    bit_depth = models.IntegerField(null=True, blank=True)

    # ==================== Timestamps ====================
    # Original capture time from EXIF
    date_taken = models.DateTimeField(null=True, blank=True, db_index=True)

    # Sub-second precision (from EXIF:SubSecTimeOriginal)
    date_taken_subsec = models.CharField(max_length=10, null=True, blank=True)

    # When the file was last modified
    date_modified = models.DateTimeField(null=True, blank=True)

    # Timezone offset if available
    timezone_offset = models.CharField(max_length=10, null=True, blank=True)

    # ==================== Location ====================
    gps_latitude = models.FloatField(null=True, blank=True, db_index=True)
    gps_longitude = models.FloatField(null=True, blank=True, db_index=True)
    gps_altitude = models.FloatField(null=True, blank=True)

    # Location from reverse geocoding
    location_country = models.CharField(
        max_length=100, null=True, blank=True, db_index=True
    )
    location_state = models.CharField(max_length=100, null=True, blank=True)
    location_city = models.CharField(
        max_length=100, null=True, blank=True, db_index=True
    )
    location_address = models.TextField(null=True, blank=True)

    # ==================== Content Description ====================
    title = models.CharField(max_length=500, null=True, blank=True)
    caption = models.TextField(null=True, blank=True)
    keywords = models.JSONField(null=True, blank=True)  # List of strings

    # Rating (0-5 stars)
    rating = models.IntegerField(null=True, blank=True, db_index=True)

    # Copyright info
    copyright = models.TextField(null=True, blank=True)
    creator = models.CharField(max_length=200, null=True, blank=True)

    # ==================== Tracking ====================
    # Which source this metadata came from
    source = models.CharField(
        max_length=20,
        choices=Source.choices,
        default=Source.EMBEDDED,
    )

    # Raw EXIF/XMP data for fields we don't have explicit columns for
    raw_exif = models.JSONField(null=True, blank=True)
    raw_xmp = models.JSONField(null=True, blank=True)
    raw_iptc = models.JSONField(null=True, blank=True)

    # Version tracking
    version = models.IntegerField(default=1)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "Photo Metadata"
        verbose_name_plural = "Photo Metadata"
        indexes = [
            models.Index(fields=["camera_make", "camera_model"]),
            models.Index(fields=["date_taken"]),
            models.Index(fields=["location_country", "location_city"]),
        ]

    def __str__(self):
        return f"Metadata for {self.photo_id}"

    @property
    def resolution(self):
        """Returns resolution as 'WxH' string."""
        if self.width and self.height:
            return f"{self.width}x{self.height}"
        return None

    @property
    def megapixels(self):
        """Returns megapixel count."""
        if self.width and self.height:
            return round((self.width * self.height) / 1_000_000, 1)
        return None

    @property
    def has_location(self):
        """Check if GPS coordinates are available."""
        return self.gps_latitude is not None and self.gps_longitude is not None

    @property
    def camera_display(self):
        """Returns a human-readable camera name."""
        if self.camera_make and self.camera_model:
            # Avoid duplicating make in model
            if self.camera_model.startswith(self.camera_make):
                return self.camera_model
            return f"{self.camera_make} {self.camera_model}"
        return self.camera_model or self.camera_make

    @property
    def lens_display(self):
        """Returns a human-readable lens name."""
        if self.lens_make and self.lens_model:
            if self.lens_model.startswith(self.lens_make):
                return self.lens_model
            return f"{self.lens_make} {self.lens_model}"
        return self.lens_model or self.lens_make

    @classmethod
    def extract_exif_data(cls, photo, commit=True):
        """
        Extract EXIF data from a photo's main file and update PhotoMetadata.

        This method extracts metadata from the photo's main file and:
        1. Updates Photo fields (size, video_length, rating, exif_timestamp_subsec, image_sequence_number)
        2. Gets or creates PhotoMetadata and populates it with extracted data

        Args:
            photo: Photo instance to extract metadata from
            commit: Whether to save Photo and PhotoMetadata after extraction

        Returns:
            PhotoMetadata instance
        """
        if not photo.main_file:
            return None

        (
            size,
            fstop,
            focal_length,
            iso,
            shutter_speed,
            camera,
            lens,
            width,
            height,
            focalLength35Equivalent,
            subjectDistance,
            digitalZoomRatio,
            video_length,
            rating,
            subsec_time_original,
            image_number,
            xmp_subject,
            iptc_keywords,
        ) = get_metadata(  # noqa: E501
            photo.main_file.path,
            tags=[
                Tags.FILE_SIZE,
                Tags.FSTOP,
                Tags.FOCAL_LENGTH,
                Tags.ISO,
                Tags.EXPOSURE_TIME,
                Tags.CAMERA,
                Tags.LENS,
                Tags.IMAGE_WIDTH,
                Tags.IMAGE_HEIGHT,
                Tags.FOCAL_LENGTH_35MM,
                Tags.SUBJECT_DISTANCE,
                Tags.DIGITAL_ZOOM_RATIO,
                Tags.QUICKTIME_DURATION,
                Tags.RATING,
                Tags.SUBSEC_TIME_ORIGINAL,
                Tags.IMAGE_NUMBER,
                Tags.SUBJECT,
                Tags.IPTC_KEYWORDS,
            ],
            try_sidecar=True,
        )

        # Fields still on Photo model
        if size and isinstance(size, numbers.Number):
            photo.size = size
        if video_length and isinstance(video_length, numbers.Number):
            photo.video_length = video_length
        if rating is not None and isinstance(rating, numbers.Number):
            photo.rating = rating

        # Burst/sequence detection fields
        if subsec_time_original:
            # SubSecTimeOriginal is typically a string like "123" representing milliseconds
            photo.exif_timestamp_subsec = str(subsec_time_original)[:10]
        if image_number is not None and isinstance(image_number, numbers.Number):
            photo.image_sequence_number = int(image_number)

        if commit:
            photo.save()

        # Store metadata in PhotoMetadata model
        metadata, created = cls.objects.get_or_create(
            photo=photo, defaults={"source": cls.Source.EMBEDDED}
        )

        if fstop and isinstance(fstop, numbers.Number):
            metadata.aperture = fstop
        if focal_length and isinstance(focal_length, numbers.Number):
            metadata.focal_length = focal_length
        if iso and isinstance(iso, numbers.Number):
            metadata.iso = iso
        if shutter_speed and isinstance(shutter_speed, numbers.Number):
            metadata.shutter_speed = str(
                Fraction(shutter_speed).limit_denominator(1000)
            )
        if camera and isinstance(camera, str):
            metadata.camera_model = camera
        if lens and isinstance(lens, str):
            metadata.lens_model = lens
        if width and isinstance(width, numbers.Number):
            metadata.width = width
        if height and isinstance(height, numbers.Number):
            metadata.height = height
        if focalLength35Equivalent and isinstance(
            focalLength35Equivalent, numbers.Number
        ):
            metadata.focal_length_35mm = focalLength35Equivalent
        if rating is not None and isinstance(rating, numbers.Number):
            metadata.rating = rating
        if subsec_time_original:
            metadata.date_taken_subsec = str(subsec_time_original)[:10]

        # Merge keywords from XMP:Subject and IPTC:Keywords (deduplicated)
        merged_keywords = set()
        if xmp_subject:
            if isinstance(xmp_subject, list):
                merged_keywords.update(xmp_subject)
            elif isinstance(xmp_subject, str):
                merged_keywords.add(xmp_subject)
        if iptc_keywords:
            if isinstance(iptc_keywords, list):
                merged_keywords.update(iptc_keywords)
            elif isinstance(iptc_keywords, str):
                merged_keywords.add(iptc_keywords)
        if merged_keywords:
            metadata.keywords = sorted(merged_keywords)

        if commit:
            metadata.save()

        return metadata


class MetadataEdit(models.Model):
    """
    Tracks user edits to photo metadata for history/undo functionality.

    Each edit records what field was changed, old value, new value,
    and who made the change.
    """

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

    photo = models.ForeignKey(
        "Photo",
        on_delete=models.CASCADE,
        related_name="metadata_edits",
    )

    user = models.ForeignKey(
        User,
        on_delete=models.SET(get_deleted_user),
        related_name="metadata_edits",
    )

    # Which field was edited
    field_name = models.CharField(max_length=100)

    # JSON-serialized old and new values
    old_value = models.JSONField(null=True, blank=True)
    new_value = models.JSONField(null=True, blank=True)

    # Whether this edit has been written back to the file
    synced_to_file = models.BooleanField(default=False)
    synced_at = models.DateTimeField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]
        verbose_name = "Metadata Edit"
        verbose_name_plural = "Metadata Edits"
        indexes = [
            models.Index(fields=["photo", "-created_at"]),
        ]

    def __str__(self):
        return f"Edit {self.field_name} on {self.photo_id}"
