"""Support for Habitica sensors."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
import logging
from typing import Any
from uuid import UUID

from habiticalib import ContentData, GroupData, HabiticaClass, TaskData, UserData, ha

from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util

from . import HABITICA_KEY
from .const import ASSETS_URL
from .coordinator import HabiticaConfigEntry
from .entity import HabiticaBase, HabiticaPartyBase, HabiticaPartyMemberBase
from .util import (
    collected_quest_items,
    get_attribute_points,
    get_attributes_total,
    inventory_list,
    pending_damage,
    pending_quest_items,
    quest_attributes,
    quest_boss,
    rage_attributes,
)

_LOGGER = logging.getLogger(__name__)

SVG_CLASS = {
    HabiticaClass.WARRIOR: ha.WARRIOR,
    HabiticaClass.ROGUE: ha.ROGUE,
    HabiticaClass.MAGE: ha.WIZARD,
    HabiticaClass.HEALER: ha.HEALER,
}


PARALLEL_UPDATES = 1


@dataclass(kw_only=True, frozen=True)
class HabiticaSensorEntityDescription(SensorEntityDescription):
    """Habitica Sensor Description."""

    value_fn: Callable[[UserData, ContentData], StateType | datetime]
    attributes_fn: Callable[[UserData, ContentData], dict[str, Any] | None] | None = (
        None
    )
    entity_picture: str | None = None


@dataclass(kw_only=True, frozen=True)
class HabiticaPartySensorEntityDescription(SensorEntityDescription):
    """Habitica Party Sensor Description."""

    value_fn: Callable[[GroupData, ContentData], StateType]
    entity_picture: Callable[[GroupData], str | None] | str | None = None
    attributes_fn: Callable[[GroupData, ContentData], dict[str, Any] | None] | None = (
        None
    )


@dataclass(kw_only=True, frozen=True)
class HabiticaTaskSensorEntityDescription(SensorEntityDescription):
    """Habitica Task Sensor Description."""

    value_fn: Callable[[list[TaskData]], list[TaskData]]


class HabiticaSensorEntity(StrEnum):
    """Habitica Entities."""

    DISPLAY_NAME = "display_name"
    HEALTH = "health"
    MANA = "mana"
    MANA_MAX = "mana_max"
    EXPERIENCE = "experience"
    EXPERIENCE_MAX = "experience_max"
    LEVEL = "level"
    GOLD = "gold"
    CLASS = "class"
    HABITS = "habits"
    REWARDS = "rewards"
    GEMS = "gems"
    TRINKETS = "trinkets"
    STRENGTH = "strength"
    INTELLIGENCE = "intelligence"
    CONSTITUTION = "constitution"
    PERCEPTION = "perception"
    EGGS_TOTAL = "eggs_total"
    HATCHING_POTIONS_TOTAL = "hatching_potions_total"
    FOOD_TOTAL = "food_total"
    SADDLE = "saddle"
    QUEST_SCROLLS = "quest_scrolls"
    PENDING_DAMAGE = "pending_damage"
    PENDING_QUEST_ITEMS = "pending_quest_items"
    MEMBER_COUNT = "member_count"
    GROUP_LEADER = "group_leader"
    QUEST = "quest"
    BOSS = "boss"
    BOSS_HP = "boss_hp"
    BOSS_HP_REMAINING = "boss_hp_remaining"
    COLLECTED_ITEMS = "collected_items"
    BOSS_RAGE = "boss_rage"
    BOSS_RAGE_LIMIT = "boss_rage_limit"
    LAST_CHECKIN = "last_checkin"


SENSOR_DESCRIPTIONS_COMMON: tuple[HabiticaSensorEntityDescription, ...] = (
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.DISPLAY_NAME,
        translation_key=HabiticaSensorEntity.DISPLAY_NAME,
        value_fn=lambda user, _: user.profile.name,
        attributes_fn=lambda user, _: {
            "username": f"@{user.auth.local.username}",
            "blurb": user.profile.blurb,
            "joined": (
                dt_util.as_local(joined).date()
                if (joined := user.auth.timestamps.created)
                else None
            ),
            "last_login": (
                dt_util.as_local(last).date()
                if (last := user.auth.timestamps.loggedin)
                else None
            ),
            "total_logins": user.loginIncentives,
        },
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.HEALTH,
        translation_key=HabiticaSensorEntity.HEALTH,
        suggested_display_precision=0,
        value_fn=lambda user, _: user.stats.hp,
        entity_picture=ha.HP,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.MANA,
        translation_key=HabiticaSensorEntity.MANA,
        suggested_display_precision=0,
        value_fn=lambda user, _: user.stats.mp,
        entity_picture=ha.MP,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.MANA_MAX,
        translation_key=HabiticaSensorEntity.MANA_MAX,
        value_fn=lambda user, _: user.stats.maxMP,
        entity_picture=ha.MP,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.EXPERIENCE,
        translation_key=HabiticaSensorEntity.EXPERIENCE,
        value_fn=lambda user, _: user.stats.exp,
        entity_picture=ha.XP,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.EXPERIENCE_MAX,
        translation_key=HabiticaSensorEntity.EXPERIENCE_MAX,
        value_fn=lambda user, _: user.stats.toNextLevel,
        entity_picture=ha.XP,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.LEVEL,
        translation_key=HabiticaSensorEntity.LEVEL,
        value_fn=lambda user, _: user.stats.lvl,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.CLASS,
        translation_key=HabiticaSensorEntity.CLASS,
        value_fn=lambda user, _: user.stats.Class.value if user.stats.Class else None,
        device_class=SensorDeviceClass.ENUM,
        options=[item.value for item in HabiticaClass],
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.STRENGTH,
        translation_key=HabiticaSensorEntity.STRENGTH,
        value_fn=lambda user, content: get_attributes_total(user, content, "Str"),
        attributes_fn=lambda user, content: get_attribute_points(user, content, "Str"),
        suggested_display_precision=0,
        native_unit_of_measurement="STR",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.INTELLIGENCE,
        translation_key=HabiticaSensorEntity.INTELLIGENCE,
        value_fn=lambda user, content: get_attributes_total(user, content, "Int"),
        attributes_fn=lambda user, content: get_attribute_points(user, content, "Int"),
        suggested_display_precision=0,
        native_unit_of_measurement="INT",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.PERCEPTION,
        translation_key=HabiticaSensorEntity.PERCEPTION,
        value_fn=lambda user, content: get_attributes_total(user, content, "per"),
        attributes_fn=lambda user, content: get_attribute_points(user, content, "per"),
        suggested_display_precision=0,
        native_unit_of_measurement="PER",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.CONSTITUTION,
        translation_key=HabiticaSensorEntity.CONSTITUTION,
        value_fn=lambda user, content: get_attributes_total(user, content, "con"),
        attributes_fn=lambda user, content: get_attribute_points(user, content, "con"),
        suggested_display_precision=0,
        native_unit_of_measurement="CON",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.LAST_CHECKIN,
        translation_key=HabiticaSensorEntity.LAST_CHECKIN,
        value_fn=(
            lambda user, _: dt_util.as_local(last)
            if (last := user.auth.timestamps.loggedin)
            else None
        ),
        device_class=SensorDeviceClass.TIMESTAMP,
    ),
)
SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.GOLD,
        translation_key=HabiticaSensorEntity.GOLD,
        suggested_display_precision=2,
        value_fn=lambda user, _: user.stats.gp,
        entity_picture=ha.GP,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.GEMS,
        translation_key=HabiticaSensorEntity.GEMS,
        value_fn=lambda user, _: None if (b := user.balance) is None else round(b * 4),
        suggested_display_precision=0,
        entity_picture="shop_gem.png",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.TRINKETS,
        translation_key=HabiticaSensorEntity.TRINKETS,
        value_fn=lambda user, _: user.purchased.plan.consecutive.trinkets,
        suggested_display_precision=0,
        native_unit_of_measurement="⧖",
        entity_picture="notif_subscriber_reward.png",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.EGGS_TOTAL,
        translation_key=HabiticaSensorEntity.EGGS_TOTAL,
        value_fn=lambda user, _: sum(n for n in user.items.eggs.values()),
        entity_picture="Pet_Egg_Egg.png",
        attributes_fn=lambda user, content: inventory_list(user, content, "eggs"),
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.HATCHING_POTIONS_TOTAL,
        translation_key=HabiticaSensorEntity.HATCHING_POTIONS_TOTAL,
        value_fn=lambda user, _: sum(n for n in user.items.hatchingPotions.values()),
        entity_picture="Pet_HatchingPotion_RoyalPurple.png",
        attributes_fn=(
            lambda user, content: inventory_list(user, content, "hatchingPotions")
        ),
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.FOOD_TOTAL,
        translation_key=HabiticaSensorEntity.FOOD_TOTAL,
        value_fn=(
            lambda user, _: sum(n for k, n in user.items.food.items() if k != "Saddle")
        ),
        entity_picture=ha.FOOD,
        attributes_fn=lambda user, content: inventory_list(user, content, "food"),
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.SADDLE,
        translation_key=HabiticaSensorEntity.SADDLE,
        value_fn=lambda user, _: user.items.food.get("Saddle", 0),
        entity_picture="Pet_Food_Saddle.png",
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.QUEST_SCROLLS,
        translation_key=HabiticaSensorEntity.QUEST_SCROLLS,
        value_fn=(lambda user, _: sum(n for n in user.items.quests.values())),
        entity_picture="inventory_quest_scroll_dustbunnies.png",
        attributes_fn=lambda user, content: inventory_list(user, content, "quests"),
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.PENDING_DAMAGE,
        translation_key=HabiticaSensorEntity.PENDING_DAMAGE,
        value_fn=pending_damage,
        suggested_display_precision=1,
        entity_picture=ha.DAMAGE,
    ),
    HabiticaSensorEntityDescription(
        key=HabiticaSensorEntity.PENDING_QUEST_ITEMS,
        translation_key=HabiticaSensorEntity.PENDING_QUEST_ITEMS,
        value_fn=pending_quest_items,
    ),
)


SENSOR_DESCRIPTIONS_PARTY: tuple[HabiticaPartySensorEntityDescription, ...] = (
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.MEMBER_COUNT,
        translation_key=HabiticaSensorEntity.MEMBER_COUNT,
        value_fn=lambda party, _: party.memberCount,
        entity_picture=ha.PARTY,
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.GROUP_LEADER,
        translation_key=HabiticaSensorEntity.GROUP_LEADER,
        value_fn=lambda party, _: party.leader.profile.name,
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.QUEST,
        translation_key=HabiticaSensorEntity.QUEST,
        value_fn=lambda p, c: c.quests[p.quest.key].text if p.quest.key else None,
        attributes_fn=quest_attributes,
        entity_picture=(
            lambda party: f"inventory_quest_scroll_{party.quest.key}.png"
            if party.quest.key
            else None
        ),
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.BOSS,
        translation_key=HabiticaSensorEntity.BOSS,
        value_fn=lambda p, c: boss.name if (boss := quest_boss(p, c)) else None,
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.BOSS_HP,
        translation_key=HabiticaSensorEntity.BOSS_HP,
        value_fn=lambda p, c: boss.hp if (boss := quest_boss(p, c)) else None,
        entity_picture=ha.HP,
        suggested_display_precision=0,
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.BOSS_HP_REMAINING,
        translation_key=HabiticaSensorEntity.BOSS_HP_REMAINING,
        value_fn=lambda p, _: p.quest.progress.hp,
        entity_picture=ha.HP,
        suggested_display_precision=2,
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.COLLECTED_ITEMS,
        translation_key=HabiticaSensorEntity.COLLECTED_ITEMS,
        value_fn=(
            lambda p, _: sum(n for n in p.quest.progress.collect.values())
            if p.quest.progress.collect
            else None
        ),
        attributes_fn=collected_quest_items,
        entity_picture=(
            lambda p: f"quest_{p.quest.key}_{k}.png"
            if p.quest.progress.collect
            and (k := next(iter(p.quest.progress.collect), None))
            else None
        ),
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.BOSS_RAGE,
        translation_key=HabiticaSensorEntity.BOSS_RAGE,
        value_fn=lambda p, _: p.quest.progress.rage,
        entity_picture=ha.RAGE,
        suggested_display_precision=2,
    ),
    HabiticaPartySensorEntityDescription(
        key=HabiticaSensorEntity.BOSS_RAGE_LIMIT,
        translation_key=HabiticaSensorEntity.BOSS_RAGE_LIMIT,
        value_fn=(
            lambda p, c: boss.rage.value
            if (boss := quest_boss(p, c)) and boss.rage
            else None
        ),
        entity_picture=ha.RAGE,
        suggested_display_precision=0,
        attributes_fn=rage_attributes,
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: HabiticaConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the habitica sensors."""

    coordinator = config_entry.runtime_data

    async_add_entities(
        HabiticaSensor(coordinator, description)
        for description in SENSOR_DESCRIPTIONS + SENSOR_DESCRIPTIONS_COMMON
    )

    if party := coordinator.data.user.party.id:
        party_coordinator = hass.data[HABITICA_KEY][party]
        async_add_entities(
            HabiticaPartySensor(
                party_coordinator,
                config_entry,
                description,
                coordinator.content,
            )
            for description in SENSOR_DESCRIPTIONS_PARTY
        )
        for subentry_id, subentry in config_entry.subentries.items():
            if (
                subentry.unique_id
                and UUID(subentry.unique_id) in party_coordinator.data.members
            ):
                async_add_entities(
                    [
                        HabiticaPartyMemberSensor(
                            coordinator,
                            party_coordinator,
                            description,
                            subentry,
                        )
                        for description in SENSOR_DESCRIPTIONS_COMMON
                    ],
                    config_subentry_id=subentry_id,
                )


class HabiticaSensor(HabiticaBase, SensorEntity):
    """A generic Habitica sensor."""

    entity_description: HabiticaSensorEntityDescription

    @property
    def native_value(self) -> StateType | datetime:
        """Return the state of the device."""

        return (
            self.entity_description.value_fn(self.user, self.coordinator.content)
            if self.user is not None
            else None
        )

    @property
    def extra_state_attributes(self) -> dict[str, float | None] | None:
        """Return entity specific state attributes."""
        if self.user is not None and (func := self.entity_description.attributes_fn):
            return func(self.user, self.coordinator.content)
        return None

    @property
    def entity_picture(self) -> str | None:
        """Return the entity picture to use in the frontend, if any."""
        if (
            self.entity_description.key is HabiticaSensorEntity.CLASS
            and self.user is not None
            and (_class := self.user.stats.Class)
        ):
            return SVG_CLASS[_class]

        if (
            self.entity_description.key is HabiticaSensorEntity.DISPLAY_NAME
            and self.user is not None
            and (img_url := self.user.profile.imageUrl)
        ):
            return img_url

        if entity_picture := self.entity_description.entity_picture:
            return (
                entity_picture
                if entity_picture.startswith("data:image")
                else f"{ASSETS_URL}{entity_picture}"
            )

        return None


class HabiticaPartyMemberSensor(HabiticaSensor, HabiticaPartyMemberBase):
    """Habitica party member sensor."""


class HabiticaPartySensor(HabiticaPartyBase, SensorEntity):
    """Habitica party sensor."""

    entity_description: HabiticaPartySensorEntityDescription

    @property
    def native_value(self) -> StateType | datetime:
        """Return the state of the device."""

        return self.entity_description.value_fn(
            self.coordinator.data.party, self.content
        )

    @property
    def entity_picture(self) -> str | None:
        """Return the entity picture to use in the frontend, if any."""
        pic = self.entity_description.entity_picture

        entity_picture = (
            pic
            if isinstance(pic, str) or pic is None
            else pic(self.coordinator.data.party)
        )

        return (
            None
            if not entity_picture
            else entity_picture
            if entity_picture.startswith("data:image")
            else f"{ASSETS_URL}{entity_picture}"
        )

    @property
    def extra_state_attributes(self) -> dict[str, Any] | None:
        """Return entity specific state attributes."""
        if func := self.entity_description.attributes_fn:
            return func(self.coordinator.data.party, self.content)
        return None
