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

import requests
from django.apps import apps
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 drinks.cellartracker import get_wine_from_cellartracker_id
from drinks.roastdb import get_coffee_from_slug, search_coffee_on_roastdb
from drinks.untappd import get_beer_from_untappd_id
from drinks.vivino import get_wine_from_vivino_id
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from scrobbles.dataclasses import BaseLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin

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


class WineType(models.TextChoices):
    RED = "red", "Red"
    WHITE = "white", "White"
    ROSE = "rosé", "Rosé"
    SPARKLING = "sparkling", "Sparkling"
    DESSERT = "dessert", "Dessert"
    FORTIFIED = "fortified", "Fortified"
    ORANGE = "orange", "Orange"


class DrinkFormat(models.TextChoices):
    ALUMINUM_CAN = "aluminum_can", "Aluminum Can"
    GLASS_BOTTLE = "glass_bottle", "Glass Bottle"
    PLASTIC_BOTTLE = "plastic_bottle", "Plastic Bottle"
    METAL_BOTTLE = "metal_bottle", "Metal Bottle"
    KEG = "keg", "Keg"
    BY_THE_GLASS = "by_the_glass", "By the Glass"
    DRAFT = "draft", "Draft"
    GROWLER = "growler", "Growler"
    BOX = "box", "Box"
    MUG = "mug", "Mug"


class RoastLevel(models.TextChoices):
    CINNAMON = "cinnamon", "Cinnamon"
    CITY = "city", "City"
    CITY_PLUS = "city_plus", "City+"
    FULL_CITY = "full_city", "Full City"
    FULL_CITY_PLUS = "full_city_plus", "Full City+"
    VIENNA = "vienna", "Vienna"
    FRENCH = "french", "French"
    SPANISH = "spanish", "Spanish"


class BrewingMethod(models.TextChoices):
    POUROVER = "pourover", "Pour-over"
    DRIP = "drip", "Drip"
    FRENCH_PRESS = "french_press", "French Press"
    ESPRESSO = "espresso", "Espresso"
    AEROPRESS = "aeropress", "AeroPress"
    KEURIG = "keurig", "Keurig"
    NESCAPRESSO = "nespresso", "Nespresso"
    COLD_BREW = "cold_brew", "Cold Brew"
    MOKA_POT = "moka_pot", "Moka Pot"
    SIPHON = "siphon", "Siphon"
    TURKISH = "turkish", "Turkish"
    CHEMEX = "chemex", "Chemex"


ML_PER_OZ = 29.5735


@dataclass
class DrinkLogData(BaseLogData):
    rating: Optional[str] = None
    format: Optional[str] = None
    size_ml: Optional[int] = None

    @property
    def size_oz(self) -> Optional[float]:
        if self.size_ml:
            return round(float(self.size_ml) / ML_PER_OZ, 1)
        return None

    @property
    def format_display(self) -> str:
        if self.format:
            return DrinkFormat(self.format).label
        return ""

    @classmethod
    def override_fields(cls) -> dict:
        from django import forms

        return {
            "format": forms.ChoiceField(
                choices=[("", "---------")] + list(DrinkFormat.choices),
                required=False,
            ),
        }


@dataclass
class BeerLogData(DrinkLogData):
    pass


@dataclass
class WineLogData(DrinkLogData):
    pass


@dataclass
class CoffeeLogData(DrinkLogData):
    roast_level: Optional[str] = None
    brewing_method: Optional[str] = None
    single_origin_or_blend: Optional[str] = None

    @property
    def roast_level_display(self) -> str:
        if self.roast_level:
            return RoastLevel(self.roast_level).label
        return ""

    @property
    def brewing_method_display(self) -> str:
        if self.brewing_method:
            return BrewingMethod(self.brewing_method).label
        return ""

    @classmethod
    def override_fields(cls) -> dict:
        from django import forms

        fields = super().override_fields()
        fields.update(
            {
                "roast_level": forms.ChoiceField(
                    choices=[("", "---------")] + list(RoastLevel.choices),
                    required=False,
                ),
                "brewing_method": forms.ChoiceField(
                    choices=[("", "---------")] + list(BrewingMethod.choices),
                    required=False,
                ),
            }
        )
        return fields


class Drink(ScrobblableMixin):
    calories = models.PositiveIntegerField(**BNULL)
    is_alcoholic = models.BooleanField(default=False)
    off_code = models.CharField(max_length=255, unique=True, **BNULL)
    source_site = models.CharField(max_length=100, **BNULL)
    description = models.TextField(**BNULL)
    ingredients = models.TextField(**BNULL)
    protein = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
    fat = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
    carbohydrates = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
    fiber = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
    sugar = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
    sodium = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
    drink_image = models.ImageField(upload_to="drinks/image/", **BNULL)
    drink_image_small = ImageSpecField(
        source="drink_image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    drink_image_medium = ImageSpecField(
        source="drink_image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )

    class Meta:
        db_table = "drinks_drink"
        indexes = [
            models.Index(fields=["off_code"]),
        ]

    def __str__(self):
        return self.title or "Drink"

    @classmethod
    def find_or_create_water(cls) -> "Drink":
        water, _ = cls.objects.get_or_create(
            title="Water", defaults={"calories": 0, "base_run_time_seconds": 120}
        )
        return water

    @classmethod
    def find_or_create_from_search(cls, product_data: dict) -> "Drink":
        code = product_data.get("code", "")
        product_name = product_data.get("product_name", "")
        brands = product_data.get("brands", "")

        drink = None
        if code:
            drink = cls.objects.filter(off_code=code).first()
        if not drink and product_name:
            drink = cls.objects.filter(
                title=product_name, source_site="Open Food Facts"
            ).first()

        if drink:
            return drink

        nutriments = product_data.get("nutriments", {})
        categories = product_data.get("categories", [])
        is_alcoholic = any(
            "alcoholic" in cat.lower() or "beer" in cat.lower() or "wine" in cat.lower()
            for cat in categories
        )

        drink = cls.objects.create(
            title=product_name,
            description=f"{brands} - {product_name}" if brands else product_name,
            source_site="Open Food Facts",
            ingredients=product_data.get("ingredients_text", ""),
            calories=nutriments.get("calories"),
            is_alcoholic=is_alcoholic,
            protein=nutriments.get("protein"),
            fat=nutriments.get("fat"),
            carbohydrates=nutriments.get("carbohydrates"),
            fiber=nutriments.get("fiber"),
            sugar=nutriments.get("sugar"),
            sodium=nutriments.get("sodium"),
            off_code=code,
        )

        image_url = product_data.get("image_url", "")
        if image_url:
            drink.save_image_from_url(image_url)

        return drink

    def save_image_from_url(self, url: str):
        headers = {"User-Agent": "Vrobbler/1.0"}
        try:
            r = requests.get(url, headers=headers, timeout=10)
            if r.status_code == 200:
                fname = f"{self.title}_{self.uuid}.jpg"
                self.drink_image.save(fname, ContentFile(r.content), save=True)
        except requests.RequestException:
            pass

    def get_absolute_url(self) -> str:
        return reverse("drinks:drink_detail", kwargs={"slug": self.uuid})

    @property
    def logdata_cls(self):
        return DrinkLogData


class BeerStyle(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    description = models.TextField(**BNULL)

    def __str__(self):
        return self.name

    class Meta:
        db_table = "beers_beerstyle"


class BeerProducer(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    description = models.TextField(**BNULL)
    location = models.CharField(max_length=255, **BNULL)
    beeradvocate_id = models.CharField(max_length=255, **BNULL)
    untappd_id = models.CharField(max_length=255, **BNULL)

    def find_or_create(cls, title: str) -> "BeerProducer":
        return cls.objects.filter(title=title).first()

    def __str__(self):
        return self.name

    class Meta:
        db_table = "beers_beerproducer"


class Beer(ScrobblableMixin):
    description = models.TextField(**BNULL)
    ibu = models.SmallIntegerField(**BNULL)
    abv = models.FloatField(**BNULL)
    styles = models.ManyToManyField(BeerStyle, related_name="styles")
    calories = models.PositiveIntegerField(**BNULL)
    non_alcoholic = models.BooleanField(default=False)
    beeradvocate_id = models.CharField(max_length=255, **BNULL)
    beeradvocate_score = models.SmallIntegerField(**BNULL)
    untappd_image = models.ImageField(upload_to="drinks/untappd/", **BNULL)
    untappd_image_small = ImageSpecField(
        source="untappd_image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    untappd_image_medium = ImageSpecField(
        source="untappd_image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )
    untappd_id = models.CharField(max_length=255, **BNULL)
    untappd_rating = models.FloatField(**BNULL)
    producer = models.ForeignKey(BeerProducer, on_delete=models.DO_NOTHING, **BNULL)

    def get_absolute_url(self) -> str:
        return reverse("drinks:beer_detail", kwargs={"slug": self.uuid})

    def __str__(self):
        return f"{self.title} by {self.producer}"

    @property
    def subtitle(self):
        return self.producer.name

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

    @property
    def beeradvocate_link(self) -> str:
        link = ""
        if self.producer and self.beeradvocate_id:
            if self.beeradvocate_id:
                link = f"https://www.beeradvocate.com/beer/profile/{self.producer.beeradvocate_id}/{self.beeradvocate_id}/"
        return link

    @property
    def untappd_link(self) -> str:
        link = ""
        if self.untappd_id:
            link = f"https://www.untappd.com/beer/{self.untappd_id}/"
        return link

    @property
    def primary_image_url(self) -> str:
        url = ""
        if self.untappd_image:
            url = self.untappd_image.url
        return url

    @property
    def logdata_cls(self):
        return BeerLogData

    @classmethod
    def find_or_create(cls, untappd_id: str) -> "Beer":
        beer = cls.objects.filter(untappd_id=untappd_id).first()

        if not beer:
            beer_dict = get_beer_from_untappd_id(untappd_id)
            producer_dict = {}
            style_ids = []
            for key in list(beer_dict.keys()):
                if "producer__" in key:
                    pkey = key.replace("producer__", "")
                    producer_dict[pkey] = beer_dict.pop(key)
                if "styles" in key:
                    for style in beer_dict.pop("styles"):
                        style_inst, created = BeerStyle.objects.get_or_create(
                            name=style
                        )
                        style_ids.append(style_inst.id)

            producer, _created = BeerProducer.objects.get_or_create(**producer_dict)
            beer_dict["producer_id"] = producer.id
            beer = Beer.objects.create(**beer_dict)
            for style_id in style_ids:
                beer.styles.add(style_id)

        return beer

    def scrobbles(self, user_id):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        return Scrobble.objects.filter(user_id=user_id, beer=self).order_by(
            "-timestamp"
        )

    class Meta:
        db_table = "beers_beer"


# --- Wine models ---


class WineGrape(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class WineRegion(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    country = models.CharField(max_length=255, **BNULL)

    def __str__(self):
        return self.name


class WineProducer(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    description = models.TextField(**BNULL)
    location = models.CharField(max_length=255, **BNULL)
    vivino_id = models.CharField(max_length=255, **BNULL)
    cellartracker_id = models.CharField(max_length=255, **BNULL)

    def __str__(self):
        return self.name


class Wine(ScrobblableMixin):
    description = models.TextField(**BNULL)
    calories = models.PositiveIntegerField(**BNULL)
    non_alcoholic = models.BooleanField(default=False)
    wine_type = models.CharField(max_length=20, choices=WineType.choices, **BNULL)
    grapes = models.ManyToManyField(WineGrape, related_name="wines", blank=True)
    region = models.ForeignKey(WineRegion, on_delete=models.DO_NOTHING, **BNULL)
    producer = models.ForeignKey(WineProducer, on_delete=models.DO_NOTHING, **BNULL)
    vintage = models.CharField(max_length=255, **BNULL)
    vivino_id = models.CharField(max_length=255, **BNULL)
    vivino_rating = models.FloatField(**BNULL)
    vivino_image = models.ImageField(upload_to="drinks/vivino/", **BNULL)
    vivino_image_small = ImageSpecField(
        source="vivino_image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    vivino_image_medium = ImageSpecField(
        source="vivino_image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )
    cellartracker_id = models.CharField(max_length=255, **BNULL)
    cellartracker_rating = models.FloatField(**BNULL)
    cellartracker_image = models.ImageField(upload_to="drinks/cellartracker/", **BNULL)
    abv = models.FloatField(**BNULL)
    aroma = models.TextField(**BNULL)
    food_pairing = models.TextField(**BNULL)
    serving_temperature_min_celsius = models.FloatField(**BNULL)
    serving_temperature_max_celsius = models.FloatField(**BNULL)
    decanting_time_minutes = models.SmallIntegerField(**BNULL)

    def get_absolute_url(self) -> str:
        return reverse("drinks:wine_detail", kwargs={"slug": self.uuid})

    def __str__(self):
        return f"{self.title} by {self.producer}"

    @property
    def subtitle(self):
        return self.producer.name

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

    @property
    def vivino_link(self) -> str:
        if self.vivino_id:
            return f"https://www.vivino.com/{self.vivino_id}"
        return ""

    @property
    def cellartracker_link(self) -> str:
        if self.cellartracker_id:
            return (
                f"https://www.cellartracker.com/wine.asp?iWine={self.cellartracker_id}"
            )
        return ""

    @property
    def primary_image_url(self) -> str:
        if self.vivino_image:
            return self.vivino_image.url
        if self.cellartracker_image:
            return self.cellartracker_image.url
        return ""

    @property
    def logdata_cls(self):
        return WineLogData

    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}_{self.uuid}.jpg"
            self.vivino_image.save(fname, ContentFile(r.content), save=True)

    @classmethod
    def find_or_create(
        cls, vivino_id: str = None, cellartracker_id: str = None
    ) -> "Wine":
        wine = None
        if vivino_id:
            wine = cls.objects.filter(vivino_id=vivino_id).first()
        if not wine and cellartracker_id:
            wine = cls.objects.filter(cellartracker_id=cellartracker_id).first()

        if wine:
            return wine

        wine_dict = None
        if vivino_id:
            wine_dict = get_wine_from_vivino_id(vivino_id)
        elif cellartracker_id:
            wine_dict = get_wine_from_cellartracker_id(cellartracker_id)

        if not wine_dict or not wine_dict.get("title"):
            return None

        producer_name = wine_dict.pop("producer__name", "")
        region_name = wine_dict.pop("region", "")
        grape_names = wine_dict.pop("grapes", [])
        image_url = wine_dict.pop("image_url", "")

        producer = None
        if producer_name:
            producer, _ = WineProducer.objects.get_or_create(name=producer_name)

        region = None
        if region_name:
            region, _ = WineRegion.objects.get_or_create(name=region_name)

        wine = cls.objects.create(
            title=wine_dict.get("title", ""),
            description=wine_dict.get("description", ""),
            wine_type=wine_dict.get("wine_type", ""),
            vintage=wine_dict.get("vintage", ""),
            vivino_id=wine_dict.get("vivino_id", vivino_id),
            vivino_rating=wine_dict.get("vivino_rating"),
            cellartracker_id=wine_dict.get("cellartracker_id", cellartracker_id),
            cellartracker_rating=wine_dict.get("cellartracker_rating"),
            producer=producer,
            region=region,
        )

        for grape_name in grape_names:
            grape, _ = WineGrape.objects.get_or_create(name=grape_name)
            wine.grapes.add(grape)

        if image_url:
            try:
                wine.save_image_from_url(image_url)
            except Exception:
                pass

        return wine

    @classmethod
    def find_or_create_from_search(cls, wine_data: dict) -> "Wine":
        title = wine_data.get("full_wine_name", "")
        producer_name = wine_data.get("winery", "")

        wine = None
        if title and producer_name:
            wine = cls.objects.filter(title=title, producer__name=producer_name).first()
        elif title:
            wine = cls.objects.filter(title=title).first()

        if wine:
            return wine

        producer = None
        if producer_name:
            producer, _ = WineProducer.objects.get_or_create(
                name=producer_name,
                defaults={
                    "description": wine_data.get("winery_description", ""),
                },
            )
            if not producer.description and wine_data.get("winery_description"):
                producer.description = wine_data["winery_description"]
                producer.save()

        region = None
        region_name = wine_data.get("region", "")
        if region_name:
            region, _ = WineRegion.objects.get_or_create(name=region_name)

        wine = cls.objects.create(
            title=title,
            description=wine_data.get("tasting_notes", ""),
            wine_type=wine_data.get("wine_type", "").lower(),
            vintage=wine_data.get("vintage", ""),
            producer=producer,
            region=region,
            abv=wine_data.get("alc_percentage"),
            aroma=wine_data.get("aroma", ""),
            food_pairing=wine_data.get("food_pairing", ""),
            serving_temperature_min_celsius=wine_data.get(
                "serving_temperature_celcius_range", {}
            ).get("min_temp"),
            serving_temperature_max_celsius=wine_data.get(
                "serving_temperature_celcius_range", {}
            ).get("max_temp"),
            decanting_time_minutes=wine_data.get("decanting_time_minutes"),
        )

        grape_variety = wine_data.get("grape_variety", "")
        if grape_variety:
            for grape_name in grape_variety.split(","):
                grape_name = grape_name.strip()
                if grape_name:
                    grape, _ = WineGrape.objects.get_or_create(name=grape_name)
                    wine.grapes.add(grape)

        return wine

    def scrobbles(self, user_id):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        return Scrobble.objects.filter(user_id=user_id, wine=self).order_by(
            "-timestamp"
        )


# --- Coffee models ---


class CoffeeRoaster(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    description = models.TextField(**BNULL)
    location = models.CharField(max_length=255, **BNULL)
    roastdb_id = models.CharField(max_length=255, **BNULL)

    def __str__(self):
        return self.name


class Coffee(ScrobblableMixin):
    description = models.TextField(**BNULL)
    calories = models.PositiveIntegerField(**BNULL)
    origin = models.CharField(max_length=255, **BNULL)
    roaster = models.ForeignKey(CoffeeRoaster, on_delete=models.DO_NOTHING, **BNULL)
    roastdb_id = models.CharField(max_length=255, **BNULL)
    roastdb_image = models.ImageField(upload_to="drinks/roastdb/", **BNULL)
    roastdb_image_small = ImageSpecField(
        source="roastdb_image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    roastdb_image_medium = ImageSpecField(
        source="roastdb_image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )

    def get_absolute_url(self) -> str:
        return reverse("drinks:coffee_detail", kwargs={"slug": self.uuid})

    def __str__(self):
        return f"{self.title} by {self.roaster}"

    @property
    def subtitle(self):
        return self.roaster.name

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

    @property
    def primary_image_url(self) -> str:
        if self.roastdb_image:
            return self.roastdb_image.url
        return ""

    @property
    def logdata_cls(self):
        return CoffeeLogData

    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}_{self.uuid}.jpg"
            self.roastdb_image.save(fname, ContentFile(r.content), save=True)

    @classmethod
    def find_or_create(cls, roastdb_id: str = None, slug: str = None) -> "Coffee":
        coffee = None
        if roastdb_id:
            coffee = cls.objects.filter(roastdb_id=roastdb_id).first()
        if not coffee and slug:
            coffee = cls.objects.filter(roastdb_id=slug).first()
        if coffee:
            return coffee

        coffee_dict = None
        if slug:
            coffee_dict = get_coffee_from_slug(slug)
        elif roastdb_id:
            coffee_dict = get_coffee_from_slug(roastdb_id)

        if not coffee_dict or not coffee_dict.get("title"):
            return None

        roaster_name = coffee_dict.pop("roaster__name", "")
        image_url = coffee_dict.pop("image_url", "")

        roaster = None
        if roaster_name:
            roaster, _ = CoffeeRoaster.objects.get_or_create(name=roaster_name)

        coffee = cls.objects.create(
            title=coffee_dict.get("title", ""),
            description=coffee_dict.get("description", ""),
            origin=coffee_dict.get("origin", ""),
            roastdb_id=slug or roastdb_id,
            roaster=roaster,
        )

        if image_url:
            try:
                coffee.save_image_from_url(image_url)
            except Exception:
                pass

        return coffee

    def scrobbles(self, user_id):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        return Scrobble.objects.filter(user_id=user_id, coffee=self).order_by(
            "-timestamp"
        )
