import logging
from dataclasses import dataclass
from functools import cached_property
from typing import Optional
from uuid import uuid4

from django.conf import settings
from django.db import models
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from scrobbles.dataclasses import BaseLogData, WithPeopleLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
from scrobbles.models import BaseFileImportMixin
from workouts.utils import display_weight

logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}


@dataclass
class WorkoutSetEntry(BaseLogData):
    exercise_id: Optional[int] = None
    sets: Optional[int] = None
    reps: Optional[int] = None
    weight_kg: Optional[float] = None
    notes: Optional[str] = None

    @property
    def exercise(self) -> Optional["Exercise"]:
        if not self.exercise_id:
            return None
        return Exercise.objects.filter(id=self.exercise_id).first()

    def __str__(self) -> str:
        name = self.exercise.name if self.exercise else "Unknown"
        parts = [name]
        if self.sets and self.reps:
            parts.append(f"{self.sets} x {self.reps}")
        elif self.reps:
            parts.append(f"{self.reps} reps")
        if self.weight_kg:
            parts.append(display_weight(self.weight_kg))
        if self.notes:
            parts.append(f"({self.notes})")
        return " ".join(parts)


@dataclass
class WorkoutLogData(BaseLogData, WithPeopleLogData):
    workouts: Optional[list[WorkoutSetEntry]] = None
    duration_minutes: Optional[int] = None
    bodyweight_kg: Optional[float] = None
    rpe: Optional[float] = None

    _excluded_fields = {}

    @cached_property
    def workout_list(self) -> str:
        if self.workouts:
            return ", ".join([str(WorkoutSetEntry(**w)) for w in self.workouts])
        return ""

    def as_html(self, scrobble_id: Optional[int] = None, units: str = "metric") -> str:
        if not self.workouts:
            return ""
        html_parts = []
        for workout_data in self.workouts:
            entry = WorkoutSetEntry(**workout_data)
            exercise = entry.exercise
            name = exercise.name if exercise else "Unknown"
            parts = []
            if entry.sets and entry.reps:
                parts.append(f"{entry.sets} x {entry.reps}")
            elif entry.reps:
                parts.append(f"{entry.reps} reps")
            if entry.weight_kg:
                parts.append(display_weight(entry.weight_kg, units))
            info = " ".join(parts)
            extra = f" \u2014 {entry.notes}" if entry.notes else ""
            html_parts.append(
                f'<div class="workout-set"><strong>{name}</strong>'
                f'{f" <span>{info}</span>" if info else ""}{extra}</div>'
            )
        return f'<div class="workout-sets">{"".join(html_parts)}</div>'

    @classmethod
    def override_fields(cls) -> dict:
        from workouts.forms import WorkoutSetsField

        fields = {}
        for base in cls.mro()[1:]:
            if hasattr(base, "override_fields"):
                base_fields = base.override_fields()
                fields.update(base_fields)
        custom_fields = {
            "workouts": WorkoutSetsField(required=False),
        }
        fields.update(custom_fields)
        return fields

    @classmethod
    def prepare_form(cls, form, units: str = "metric"):
        from workouts.utils import weight_label

        workouts_field = form.fields.get("workouts")
        if workouts_field:
            workouts_field.units = units
            if hasattr(workouts_field.widget, "units"):
                workouts_field.widget.units = units

        bodyweight_field = form.fields.get("bodyweight_kg")
        if bodyweight_field is not None:
            bodyweight_field.label = f"Bodyweight ({weight_label(units)})"
            if units == "imperial" and form.initial.get("bodyweight_kg") is not None:
                from workouts.utils import kg_to_lbs

                form.fields["bodyweight_kg"].initial = kg_to_lbs(
                    form.initial["bodyweight_kg"]
                )

    @classmethod
    def normalize_form_data(cls, data: dict, units: str = "metric"):
        from workouts.utils import lbs_to_kg

        if (
            units == "imperial"
            and data.get("bodyweight_kg") is not None
            and data.get("bodyweight_kg") != ""
        ):
            data["bodyweight_kg"] = lbs_to_kg(data["bodyweight_kg"])


class Exercise(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255, unique=True, db_index=True)
    force = models.CharField(max_length=64, **BNULL)
    level = models.CharField(max_length=64, **BNULL)
    mechanic = models.CharField(max_length=64, **BNULL)
    equipment = models.CharField(max_length=64, **BNULL)
    category = models.CharField(max_length=64, **BNULL)
    primary_muscles = models.JSONField(default=list, **BNULL)
    secondary_muscles = models.JSONField(default=list, **BNULL)
    instructions = models.JSONField(default=list, **BNULL)
    photo = models.ImageField(upload_to="workouts/photos/", **BNULL)
    photo_small = ImageSpecField(
        source="photo",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    photo_medium = ImageSpecField(
        source="photo",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse("workouts:exercise_detail", kwargs={"slug": self.uuid})

    @classmethod
    def find_or_create(cls, name: str) -> "Exercise":
        exercise = cls.objects.filter(name__iexact=name).first()
        if not exercise:
            exercise = cls.objects.create(name=name)
        return exercise


class WorkoutRoutine(ScrobblableMixin):
    media_type_label = "Workout"

    description = models.TextField(**BNULL)

    def get_absolute_url(self):
        return reverse("workouts:workout_routine_detail", kwargs={"slug": self.uuid})

    @property
    def strings(self) -> ScrobblableConstants:
        return ScrobblableConstants(verb="Lifting", tags="workout")

    @property
    def logdata_cls(self):
        return WorkoutLogData

    def primary_image_url(self) -> str:
        return ""

    def fix_metadata(self) -> None:
        pass

    @classmethod
    def find_or_create(cls, title: str) -> "WorkoutRoutine":
        routine = cls.objects.filter(title__iexact=title).first()
        if not routine:
            routine = cls.objects.create(title=title)
        return routine


class WorkoutRoutineExercise(TimeStampedModel):
    routine = models.ForeignKey(
        WorkoutRoutine, related_name="routine_exercises", on_delete=models.CASCADE
    )
    exercise = models.ForeignKey(
        Exercise, related_name="routine_exercises", on_delete=models.CASCADE
    )
    sequence = models.PositiveIntegerField(default=0)
    target_sets = models.PositiveIntegerField(**BNULL)
    target_reps = models.PositiveIntegerField(**BNULL)
    target_weight_kg = models.FloatField(**BNULL)
    enabled = models.BooleanField(default=True)

    class Meta:
        ordering = ["routine", "sequence"]

    def __str__(self):
        return f"{self.routine.title}: {self.exercise.name}"


class WorkoutImport(BaseFileImportMixin):
    class Meta:
        verbose_name = "Workout Import"

    def get_path(instance, filename):
        extension = filename.split(".")[-1]
        uuid = instance.uuid
        return f"workout-imports/{uuid}.{extension}"

    @property
    def import_type(self) -> str:
        return "Flexify"

    @property
    def upload_file_path(self):
        if getattr(settings, "USE_S3_STORAGE"):
            return self.sqlite_file.url
        return self.sqlite_file.path

    sqlite_file = models.FileField(upload_to=get_path, **BNULL)
    csv_file = models.FileField(upload_to=get_path, **BNULL)
    original_filename = models.CharField(max_length=255, **BNULL)
    file_hash = models.CharField(max_length=32, **BNULL)

    def process(self, force=False):
        from workouts.importer import import_flexify

        if self.processed_finished and not force:
            logger.info(f"{self} already processed on {self.processed_finished}")
            return

        self.mark_started()
        try:
            result = import_flexify(
                sqlite_path=self.upload_file_path,
                csv_path=self.csv_file.path if self.csv_file else None,
                user_id=self.user_id,
            )
            self.record_log(result["scrobbles"])
        except Exception as exc:
            self.record_error(f"Import failed: {exc}")
            logger.exception(f"Import failed for {self}")
        finally:
            self.mark_finished()
