import logging
from dataclasses import dataclass
from datetime import datetime
from functools import cached_property
from typing import Any, Optional
from uuid import uuid4

import requests
from boardgames.sources.bgg import lookup_boardgame_from_bgg
from django import forms
from django.conf import settings
from django.core.files.base import ContentFile
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 locations.models import GeoLocation
from people.models import Person
from scrobbles.dataclasses import BaseLogData, LongPlayLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin

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


@dataclass
class BoardGameScoreLogData(BaseLogData):
    person_id: Optional[int] = None
    bgg_username: Optional[str] = None
    color: Optional[str] = None
    character: Optional[str] = None
    team: Optional[str] = None
    score: Optional[int] = None
    win: Optional[bool] = None
    new: Optional[bool] = None
    rank: Optional[int] = None
    seat_order: Optional[int] = None
    role: Optional[str] = None
    rank: Optional[int] = None
    seat_order: Optional[int] = None
    role: Optional[str] = None
    lichess_username: Optional[str] = None

    @property
    def person(self) -> Optional[Person]:
        return Person.objects.filter(id=self.person_id).first()

    @property
    def name(self) -> str:
        name = ""
        if self.person:
            name = self.person.name
        return name

    def __str__(self) -> str:
        out = self.name
        if self.score:
            out += f" {self.score}"
        if self.color:
            out += f" ({self.color})"
        if self.win:
            out += f" [W]"
        return out


@dataclass
class BoardGameLogData(BaseLogData, LongPlayLogData):
    players: Optional[list[BoardGameScoreLogData]] = None
    location_id: Optional[int] = None
    difficulty: Optional[int] = None
    solo: Optional[bool] = None
    two_handed: Optional[bool] = None
    expansion_ids: Optional[list[int]] = None
    moves: Optional[list] = None
    rated: Optional[str] = None
    speed: Optional[str] = None
    variant: Optional[str] = None
    variant_ids: Optional[list[int]] = None
    lichess_id: Optional[int] = None
    board: Optional[str] = None
    rounds: Optional[int] = None
    details: Optional[str] = None
    raw_data: Optional[dict] = None

    _excluded_fields = {
        "lichess_id",
        "speed",
        "rated",
        "moves",
        "variant",
    }

    @classmethod
    def override_fields(cls) -> dict:
        from boardgames.widgets import VariantSelectWidget
        from scrobbles.forms import NotesDictField

        fields = {}
        for base in cls.mro()[1:]:
            if hasattr(base, "override_fields"):
                base_fields = base.override_fields()
                fields.update(base_fields)
        custom_fields = {
            "notes": NotesDictField(required=False),
            "location_id": forms.ModelChoiceField(
                queryset=BoardGameLocation.objects.all(),
                required=False,
                widget=forms.Select(),
            ),
            "variant_ids": forms.ModelMultipleChoiceField(
                queryset=BoardGameVariant.objects.all(),
                required=False,
                widget=VariantSelectWidget(attrs={"size": 5}),
            ),
            "expansion_ids": forms.ModelMultipleChoiceField(
                queryset=BoardGame.objects.filter(
                    expansion_for_boardgame__isnull=False
                ),
                required=False,
                widget=forms.SelectMultiple(attrs={"size": 5}),
            ),
        }
        fields.update(custom_fields)
        return fields

    @cached_property
    def variants(self) -> list["BoardGameVariant"]:
        if not self.variant_ids:
            return []
        return list(
            BoardGameVariant.objects.filter(id__in=self.variant_ids)
        )

    @cached_property
    def location(self):
        if not self.location_id:
            return
        return BoardGameLocation.objects.filter(id=self.location_id).first()

    @cached_property
    def player_log(self) -> str:
        if self.players:
            return ", ".join(
                [
                    BoardGameScoreLogData(
                        **{k: v for k, v in player.items() if k in BoardGameScoreLogData.__dataclass_fields__}
                    ).__str__()
                    for player in self.players
                ]
            )
        return ""

    def as_html(self) -> str:
        html_parts = []

        if self.board:
            html_parts.append(f'<div class="boardgame-board">{self.board}</div>')

        if self.variants:
            variant_names = ", ".join(v.name for v in self.variants)
            html_parts.append(
                f'<div class="boardgame-variants">Variants: {variant_names}</div>'
            )

        if self.location:
            html_parts.append(f'<div class="boardgame-location">{self.location}</div>')

        if self.players:
            players_html = []
            for player_data in self.players:
                player = BoardGameScoreLogData(
                    **{k: v for k, v in player_data.items() if k in BoardGameScoreLogData.__dataclass_fields__}
                )
                player_info = player.name
                if player.score:
                    player_info += f" ({player.score})"
                if player.win:
                    player_info += " 🏆"
                if player.new:
                    player_info += " (new)"
                players_html.append(
                    f'<div class="boardgame-player">{player_info}</div>'
                )
            html_parts.append(
                f'<div class="boardgame-players">{"".join(players_html)}</div>'
            )

        return "".join(html_parts)


class BoardGamePublisher(TimeStampedModel):
    name = models.CharField(max_length=255)
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    logo = models.ImageField(upload_to="games/platform-logos/", **BNULL)
    bgg_id = models.IntegerField(**BNULL)

    def __str__(self):
        return self.name

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


class BoardGameDesigner(TimeStampedModel):
    name = models.CharField(max_length=255)
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    bgg_id = models.IntegerField(**BNULL)
    bio = models.TextField(**BNULL)

    def __str__(self) -> str:
        return str(self.name)

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


class BoardGameLocation(TimeStampedModel):
    name = models.CharField(max_length=255)
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    bgstats_id = models.UUIDField(**BNULL)
    description = models.TextField(**BNULL)
    geo_location = models.ForeignKey(GeoLocation, **BNULL, on_delete=models.DO_NOTHING)

    def __str__(self) -> str:
        return str(self.name)

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


class BoardGame(ScrobblableMixin):
    COMPLETION_PERCENT = getattr(settings, "BOARD_GAME_COMPLETION_PERCENT", 100)

    FIELDS_FROM_BGGEEK = [
        "igdb_id",
        "alternative_name",
        "rating",
        "rating_count",
        "release_date",
        "cover",
        "screenshot",
    ]

    title = models.CharField(max_length=255)
    publisher = models.ForeignKey(
        BoardGamePublisher, **BNULL, on_delete=models.DO_NOTHING
    )
    publishers = models.ManyToManyField(
        BoardGamePublisher,
        related_name="board_games",
    )
    designers = models.ManyToManyField(
        BoardGameDesigner,
        related_name="board_games",
    )
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    description = models.TextField(**BNULL)
    cover = models.ImageField(upload_to="boardgames/covers/", **BNULL)
    cover_small = ImageSpecField(
        source="cover",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    cover_medium = ImageSpecField(
        source="cover",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )
    layout_image = models.ImageField(upload_to="boardgames/layouts/", **BNULL)
    layout_image_small = ImageSpecField(
        source="layout_image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    layout_image_medium = ImageSpecField(
        source="layout_image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )
    rating = models.FloatField(**BNULL)
    bgg_rank = models.IntegerField(**BNULL)
    max_players = models.PositiveSmallIntegerField(**BNULL)
    min_players = models.PositiveSmallIntegerField(**BNULL)
    published_date = models.DateField(**BNULL)
    published_year = models.IntegerField(**BNULL)
    recommended_age = models.PositiveSmallIntegerField(**BNULL)
    bggeek_id = models.CharField(max_length=255, **BNULL)
    bgstats_id = models.UUIDField(**BNULL)
    uses_teams = models.BooleanField(default=False, **BNULL)
    cooperative = models.BooleanField(default=False, **BNULL)
    highest_wins = models.BooleanField(default=True, **BNULL)
    no_points = models.BooleanField(default=False, **BNULL)
    min_play_time = models.IntegerField(**BNULL)
    max_play_time = models.IntegerField(**BNULL)
    expansion_for_boardgame = models.ForeignKey(
        "self", **BNULL, on_delete=models.DO_NOTHING
    )
    skip_expansions = models.BooleanField(default=False)

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

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

    @property
    def logdata_cls(self):
        return BoardGameLogData

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

    def primary_image_url(self) -> str:
        url = ""
        if self.cover:
            url = self.cover.url
        return url

    def bggeek_link(self):
        link = ""
        if self.bggeek_id:
            link = f"https://boardgamegeek.com/boardgame/{self.bggeek_id}"
        return link

    def fix_metadata(self, data: dict = {}, force_update=False) -> None:

        if not self.published_date or force_update:
            if not data:
                data = lookup_boardgame_from_bgg(str(self.bggeek_id))

            expansions = data.pop("expansions", [])
            cover_url = data.pop("cover_url", "")
            year_published = data.pop("year_published", None)
            if year_published is None:
                year_published = data.pop("published_year", None)
            publisher_name = data.pop("publisher_name", "")

            if year_published:
                data["published_year"] = int(year_published)

            if not data["min_players"]:
                data.pop("min_players")
            if not data["min_players"]:
                data.pop("max_players")

            # Pop extra BGG metadata that isn't a model field
            categories = data.pop("categories", [])
            families = data.pop("families", [])
            data.pop("mechanics", None)
            data.pop("designers", None)
            data.pop("publishers", None)
            data.pop("publisher", None)

            # Fun trick for updating all fields at once
            BoardGame.objects.filter(pk=self.id).update(**data)
            self.refresh_from_db()

            # Add publishers
            (
                self.publisher,
                _created,
            ) = BoardGamePublisher.objects.get_or_create(name=publisher_name)
            self.save()

            for cat in categories:
                self.genre.add(cat.strip())
            for fam in families:
                self.tags.add(fam.strip())

            # Go get cover image if the URL is present
            if cover_url and not self.cover:
                self.save_image_from_url(cover_url)

            from boardgames.utils import fetch_and_link_expansions

            if not self.skip_expansions:
                fetch_and_link_expansions(self, expansions)

    def save_image_from_url(self, url):
        headers = {"User-Agent": "Vrobbler 0.11.12"}
        r = requests.get(url, headers=headers)
        if r.status_code == 200:
            fname = f"{self.title}_cover_{self.uuid}.jpg"
            self.cover.save(fname, ContentFile(r.content), save=True)

    @classmethod
    def find_or_create(
        cls,
        lookup_id: str,
        data: dict[str, Any] = {},
        defer_expansions: bool = False,
    ) -> "BoardGame":
        """Given a Lookup ID (either BGG or BGA ID), return a board game object"""
        game = cls.objects.filter(bggeek_id=lookup_id).first()
        if not game:
            game = cls.objects.filter(title=lookup_id).first()

        if game:
            logger.info(
                "Board game exists in database.",
                extra={"lookup_id": lookup_id, "data": data},
            )
            return game

        if data.get("bggId"):
            bgg_data = lookup_boardgame_from_bgg(lookup_id=data.get("bggId"))
        elif data.get("name"):
            bgg_data = lookup_boardgame_from_bgg(title=data.get("name"))
        else:
            if int(lookup_id):
                bgg_data = lookup_boardgame_from_bgg(lookup_id=lookup_id)
            else:
                bgg_data = lookup_boardgame_from_bgg(title=lookup_id)

        expansions = bgg_data.pop("expansions", [])
        mechanics = bgg_data.pop("mechanics", [])
        designers = bgg_data.pop("designers", [])
        categories = bgg_data.pop("categories", [])
        families = bgg_data.pop("families", [])
        publishers = bgg_data.pop("publishers", [])
        publisher = bgg_data.pop("publisher", [])
        cover_url = bgg_data.pop("cover_url") or ""

        game = cls.objects.create(**bgg_data)

        if cover_url:
            game.save_image_from_url(cover_url)
        game.cooperative = data.get("cooperative", False)
        game.highest_wins = data.get("highestWins", True)
        game.no_points = data.get("noPoints", False)
        game.uses_teams = data.get("useTeams", False)
        game.bgstats_id = data.get("uuid", None)
        if publisher:
            publisher, _ = BoardGamePublisher.objects.get_or_create(name=publisher)
            game.publisher = publisher

        skip_expansions = (
            game.bggeek_id is not None
            and str(game.bggeek_id).isdigit()
            and int(game.bggeek_id) in settings.SKIP_AUTO_EXPANSION_DOWNLOAD
        ) or any(
            c == "Collectible Card Games" for c in categories
        )

        if skip_expansions:
            game.skip_expansions = True

        game.save()

        if designers:
            for designer_name in designers:
                designer, created = BoardGameDesigner.objects.get_or_create(
                    name=designer_name
                )
                game.designers.add(designer.id)

        if publishers:
            for name in publishers:
                publisher, _ = BoardGamePublisher.objects.get_or_create(name=name)
                game.publishers.add(publisher)

        for cat in categories:
            game.genre.add(cat.strip())
        for fam in families:
            game.tags.add(fam.strip())

        if expansions and not game.skip_expansions:
            if defer_expansions:
                from boardgames.tasks import fetch_board_game_expansions

                fetch_board_game_expansions.delay(game.id, expansions)
            else:
                from boardgames.utils import fetch_and_link_expansions

                fetch_and_link_expansions(game, expansions)

        return game


class BoardGameVariant(TimeStampedModel):
    name = models.CharField(max_length=255)
    board_game = models.ForeignKey(
        BoardGame,
        on_delete=models.CASCADE,
        related_name="variants",
    )
    description = models.TextField(**BNULL)
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)

    def __str__(self) -> str:
        return f"{self.name} ({self.board_game.title})"
