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

from django.apps import apps
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django_extensions.db.models import TimeStampedModel
from scrobbles.utils import get_scrobbles_for_media
from taggit.managers import TaggableManager
from taggit.models import GenericTaggedItemBase, TagBase

BNULL = {"blank": True, "null": True}

logger = logging.getLogger(__name__)


@dataclass
class ScrobblableConstants:
    verb: str
    tags: str
    priority: str

    def __init__(
        self,
        verb: str = "Scrobbling",
        tags: str = "green_square",
        priority: str = "default",
    ):
        self.verb = verb
        self.tags = tags
        self.priority = priority


class Genre(TagBase):
    source = models.CharField(max_length=255, **BNULL)

    class Meta:
        verbose_name = "Genre"
        verbose_name_plural = "Genres"


class ObjectWithGenres(GenericTaggedItemBase):
    tag = models.ForeignKey(
        Genre,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
        **BNULL,
    )


class ScrobblableItem(TimeStampedModel):
    SECONDS_TO_STALE = 1600
    COMPLETION_PERCENT = 100

    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    title = models.CharField(max_length=255, **BNULL)
    description = models.TextField(**BNULL)
    base_run_time_seconds = models.IntegerField(**BNULL)

    # Cache for subclass related names
    _subclass_related_names_cache = None

    @classmethod
    def get_subclass_related_names(cls):
        """Return cached related names for all subclasses."""
        if cls._subclass_related_names_cache is not None:
            return cls._subclass_related_names_cache

        related_names = []
        for model in apps.get_models():
            if issubclass(model, cls) and model is not cls:
                for field in model._meta.fields:
                    if (
                        isinstance(field, models.OneToOneField)
                        and field.remote_field.model == cls
                    ):
                        if field.remote_field.related_name:
                            related_names.append(field.remote_field.related_name)
                        else:
                            related_names.append(f"{model._meta.model_name}_ptr")
        cls._subclass_related_names_cache = related_names
        return related_names

    def get_concrete_instance(self):
        """Return the actual subclass instance (the "media object")."""
        for name in self.get_subclass_related_names():
            try:
                return getattr(self, name)
            except (AttributeError, self.__class__.DoesNotExist):
                continue
        return self

    def get_media_type(self):
        """Return the media type string from the concrete subclass."""
        concrete = self.get_concrete_instance()
        if concrete is None:
            return "ScrobblableItem"
        # Access class-level MEDIA_TYPE
        return getattr(concrete.__class__, "MEDIA_TYPE", concrete.__class__.__name__)

    @property
    def run_time_seconds(self) -> int:
        if self.base_run_time_seconds:
            return self.base_run_time_seconds
        return 900  # TODO this should be a constant

    @classmethod
    def find_or_create(cls):
        logger.warning("find_or_create() not implemented yet")

    def __str__(self) -> str:
        if self.title:
            return str(self.title)
        return str(self.uuid)

    def scrobble_for_user(
        self,
        user_id,
        source: str = "Vrobbler",
        playback_position_seconds: int = 0,
        status: str = "started",
        log: Optional[dict] = None,
    ):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        scrobble_data = {
            "user_id": user_id,
            "timestamp": timezone.now(),
            "source": source,
            "status": status,
            "playback_position_seconds": playback_position_seconds,
        }

        if log:
            scrobble_data["log"] = log

        logger.info(
            "[scrobble_for_user] called",
            extra={
                "item_id": self.id,
                "media_type": self.__class__.__name__,
                "user_id": user_id,
                "scrobble_data": scrobble_data,
            },
        )
        return Scrobble.create_or_update(self, user_id, scrobble_data)

    @property
    def start_url(self):
        return reverse("scrobbles:start", kwargs={"uuid": self.uuid})

    @property
    def strings(self) -> ScrobblableConstants:
        return ScrobblableConstants()

    @property
    def primary_image_url(self) -> str:
        logger.warning(f"Not implemented yet")
        return ""

    @property
    def logdata_cls(self) -> None:
        from scrobbles.dataclasses import BaseLogData

        return BaseLogData

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

    @property
    def run_time_seconds(self) -> int:
        run_time = 900
        if self.base_run_time_seconds:
            run_time = self.base_run_time_seconds
        return run_time

    @property
    def is_long_play_media(self) -> bool:
        if not self.logdata_cls:
            return False
        return "long_play_complete" in self.logdata_cls.__dataclass_fields__


# DEPRECATED
class ScrobblableMixin(TimeStampedModel):
    SECONDS_TO_STALE = 1600
    COMPLETION_PERCENT = 100

    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    title = models.CharField(max_length=255, **BNULL)
    base_run_time_seconds = models.IntegerField(**BNULL)

    genre = TaggableManager(through=ObjectWithGenres, blank=True)

    class Meta:
        abstract = True

    @property
    def run_time_seconds(self) -> int:
        run_time = 900
        if self.base_run_time_seconds:
            run_time = self.base_run_time_seconds
        return run_time

    @classmethod
    def is_long_play_media(cls) -> bool:
        return False

    def scrobble_for_user(
        self,
        user_id,
        source: str = "Vrobbler",
        playback_position_seconds: int = 0,
        status: str = "started",
        log: Optional[dict] = None,
    ):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        scrobble_data = {
            "user_id": user_id,
            "timestamp": timezone.now(),
            "source": source,
            "status": status,
            "playback_position_seconds": playback_position_seconds,
        }

        if log:
            scrobble_data["log"] = log

        logger.info(
            "[scrobble_for_user] called",
            extra={
                "id": self.id,
                "media_type": self.media_type,
                "user_id": user_id,
                "scrobble_data": scrobble_data,
            },
        )
        return Scrobble.create_or_update(self, user_id, scrobble_data)

    @property
    def start_url(self):
        return reverse("scrobbles:start", kwargs={"uuid": self.uuid})

    @property
    def strings(self) -> ScrobblableConstants:
        return ScrobblableConstants()

    @property
    def primary_image_url(self) -> str:
        logger.warning(f"Not implemented yet")
        return ""

    @property
    def logdata_cls(self) -> None:
        from scrobbles.dataclasses import BaseLogData

        return BaseLogData

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

    def fix_metadata(self) -> None:
        logger.warning("fix_metadata() not implemented yet")

    @classmethod
    def find_or_create(cls):
        logger.warning("find_or_create() not implemented yet")

    def __str__(self) -> str:
        if self.title:
            return str(self.title)
        return str(self.uuid)


class LongPlayScrobblableMixin(ScrobblableMixin):
    class Meta:
        abstract = True

    @classmethod
    def is_long_play_media(cls) -> bool:
        return True

    def is_complete(self) -> bool:
        if self.log:
            return bool(self.log.get("long_play_complete", None))
        return False

    def get_longplay_finish_url(self):
        return reverse("scrobbles:longplay-finish", kwargs={"uuid": self.uuid})

    def first_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]:
        return (
            get_scrobbles_for_media(self, user)
            .filter(
                log__long_play_complete=False,
                log__serial_scrobble_id__isnull=True,
            )
            .order_by("timestamp")
            .first()
        )

    def last_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]:
        return (
            get_scrobbles_for_media(self, user)
            .filter(
                log__long_play_complete=False,
                log__serial_scrobble_id__isnull=False,
            )
            .order_by("timestamp")
            .last()
        )
