"""Habitica button platform."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any

from aiohttp import ClientError
from habiticalib import (
    HabiticaClass,
    HabiticaException,
    NotAuthorizedError,
    Skill,
    TaskType,
    TooManyRequestsError,
)

from homeassistant.components.button import (
    DOMAIN as BUTTON_DOMAIN,
    ButtonEntity,
    ButtonEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import ASSETS_URL, DOMAIN
from .coordinator import (
    HabiticaConfigEntry,
    HabiticaData,
    HabiticaDataUpdateCoordinator,
)
from .entity import HabiticaBase

PARALLEL_UPDATES = 1


@dataclass(kw_only=True, frozen=True)
class HabiticaButtonEntityDescription(ButtonEntityDescription):
    """Describes Habitica button entity."""

    press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
    available_fn: Callable[[HabiticaData], bool]
    class_needed: HabiticaClass | None = None
    entity_picture: str | None = None


class HabiticaButtonEntity(StrEnum):
    """Habitica button entities."""

    RUN_CRON = "run_cron"
    BUY_HEALTH_POTION = "buy_health_potion"
    ALLOCATE_ALL_STAT_POINTS = "allocate_all_stat_points"
    REVIVE = "revive"
    MPHEAL = "mpheal"
    EARTH = "earth"
    FROST = "frost"
    DEFENSIVE_STANCE = "defensive_stance"
    VALOROUS_PRESENCE = "valorous_presence"
    INTIMIDATE = "intimidate"
    TOOLS_OF_TRADE = "tools_of_trade"
    STEALTH = "stealth"
    HEAL = "heal"
    PROTECT_AURA = "protect_aura"
    BRIGHTNESS = "brightness"
    HEAL_ALL = "heal_all"


BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.RUN_CRON,
        translation_key=HabiticaButtonEntity.RUN_CRON,
        press_fn=lambda coordinator: coordinator.habitica.run_cron(),
        available_fn=lambda data: data.user.needsCron is True,
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.BUY_HEALTH_POTION,
        translation_key=HabiticaButtonEntity.BUY_HEALTH_POTION,
        press_fn=lambda coordinator: coordinator.habitica.buy_health_potion(),
        available_fn=(
            lambda data: (data.user.stats.gp or 0) >= 25
            and (data.user.stats.hp or 0) < 50
        ),
        entity_picture="shop_potion.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS,
        translation_key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS,
        press_fn=lambda coordinator: coordinator.habitica.allocate_stat_points(),
        available_fn=(
            lambda data: data.user.preferences.automaticAllocation is True
            and (data.user.stats.points or 0) > 0
        ),
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.REVIVE,
        translation_key=HabiticaButtonEntity.REVIVE,
        press_fn=lambda coordinator: coordinator.habitica.revive(),
        available_fn=lambda data: data.user.stats.hp == 0,
    ),
)


CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.MPHEAL,
        translation_key=HabiticaButtonEntity.MPHEAL,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.ETHEREAL_SURGE)
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 12
            and (data.user.stats.mp or 0) >= 30
        ),
        class_needed=HabiticaClass.MAGE,
        entity_picture="shop_mpheal.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.EARTH,
        translation_key=HabiticaButtonEntity.EARTH,
        press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.EARTHQUAKE),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 13
            and (data.user.stats.mp or 0) >= 35
        ),
        class_needed=HabiticaClass.MAGE,
        entity_picture="shop_earth.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.FROST,
        translation_key=HabiticaButtonEntity.FROST,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.CHILLING_FROST)
        ),
        # chilling frost can only be cast once per day (streaks buff is false)
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 14
            and (data.user.stats.mp or 0) >= 40
            and not data.user.stats.buffs.streaks
        ),
        class_needed=HabiticaClass.MAGE,
        entity_picture="shop_frost.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.DEFENSIVE_STANCE,
        translation_key=HabiticaButtonEntity.DEFENSIVE_STANCE,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.DEFENSIVE_STANCE)
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 12
            and (data.user.stats.mp or 0) >= 25
        ),
        class_needed=HabiticaClass.WARRIOR,
        entity_picture="shop_defensiveStance.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.VALOROUS_PRESENCE,
        translation_key=HabiticaButtonEntity.VALOROUS_PRESENCE,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.VALOROUS_PRESENCE)
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 13
            and (data.user.stats.mp or 0) >= 20
        ),
        class_needed=HabiticaClass.WARRIOR,
        entity_picture="shop_valorousPresence.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.INTIMIDATE,
        translation_key=HabiticaButtonEntity.INTIMIDATE,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.INTIMIDATING_GAZE)
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 14
            and (data.user.stats.mp or 0) >= 15
        ),
        class_needed=HabiticaClass.WARRIOR,
        entity_picture="shop_intimidate.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.TOOLS_OF_TRADE,
        translation_key=HabiticaButtonEntity.TOOLS_OF_TRADE,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(
                Skill.TOOLS_OF_THE_TRADE
            )
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 13
            and (data.user.stats.mp or 0) >= 25
        ),
        class_needed=HabiticaClass.ROGUE,
        entity_picture="shop_toolsOfTrade.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.STEALTH,
        translation_key=HabiticaButtonEntity.STEALTH,
        press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.STEALTH),
        # Stealth buffs stack and it can only be cast if the amount of
        # buffs is smaller than the amount of unfinished dailies
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 14
            and (data.user.stats.mp or 0) >= 45
            and (data.user.stats.buffs.stealth or 0)
            < len(
                [
                    r
                    for r in data.tasks
                    if r.Type is TaskType.DAILY
                    and r.isDue is True
                    and r.completed is False
                ]
            )
        ),
        class_needed=HabiticaClass.ROGUE,
        entity_picture="shop_stealth.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.HEAL,
        translation_key=HabiticaButtonEntity.HEAL,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.HEALING_LIGHT)
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 11
            and (data.user.stats.mp or 0) >= 15
            and (data.user.stats.hp or 0) < 50
        ),
        class_needed=HabiticaClass.HEALER,
        entity_picture="shop_heal.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.BRIGHTNESS,
        translation_key=HabiticaButtonEntity.BRIGHTNESS,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(
                Skill.SEARING_BRIGHTNESS
            )
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 12
            and (data.user.stats.mp or 0) >= 15
        ),
        class_needed=HabiticaClass.HEALER,
        entity_picture="shop_brightness.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.PROTECT_AURA,
        translation_key=HabiticaButtonEntity.PROTECT_AURA,
        press_fn=(
            lambda coordinator: coordinator.habitica.cast_skill(Skill.PROTECTIVE_AURA)
        ),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 13
            and (data.user.stats.mp or 0) >= 30
        ),
        class_needed=HabiticaClass.HEALER,
        entity_picture="shop_protectAura.png",
    ),
    HabiticaButtonEntityDescription(
        key=HabiticaButtonEntity.HEAL_ALL,
        translation_key=HabiticaButtonEntity.HEAL_ALL,
        press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.BLESSING),
        available_fn=(
            lambda data: (data.user.stats.lvl or 0) >= 14
            and (data.user.stats.mp or 0) >= 25
        ),
        class_needed=HabiticaClass.HEALER,
        entity_picture="shop_healAll.png",
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    entry: HabiticaConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up buttons from a config entry."""

    coordinator = entry.runtime_data
    skills_added: set[str] = set()

    @callback
    def add_entities() -> None:
        """Add or remove a skillset based on the player's class."""

        nonlocal skills_added
        buttons = []
        entity_registry = er.async_get(hass)

        for description in CLASS_SKILLS:
            if (
                (coordinator.data.user.stats.lvl or 0) >= 10
                and coordinator.data.user.flags.classSelected
                and not coordinator.data.user.preferences.disableClasses
                and description.class_needed is coordinator.data.user.stats.Class
            ):
                if description.key not in skills_added:
                    buttons.append(HabiticaButton(coordinator, description))
                    skills_added.add(description.key)
            elif description.key in skills_added:
                if entity_id := entity_registry.async_get_entity_id(
                    BUTTON_DOMAIN,
                    DOMAIN,
                    f"{coordinator.config_entry.unique_id}_{description.key}",
                ):
                    entity_registry.async_remove(entity_id)
                skills_added.remove(description.key)

        if buttons:
            async_add_entities(buttons)

    coordinator.async_add_listener(add_entities)
    add_entities()

    async_add_entities(
        HabiticaButton(coordinator, description) for description in BUTTON_DESCRIPTIONS
    )


class HabiticaButton(HabiticaBase, ButtonEntity):
    """Representation of a Habitica button."""

    entity_description: HabiticaButtonEntityDescription

    async def async_press(self) -> None:
        """Handle the button press."""
        try:
            await self.entity_description.press_fn(self.coordinator)
        except TooManyRequestsError as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="setup_rate_limit_exception",
                translation_placeholders={"retry_after": str(e.retry_after)},
            ) from e
        except NotAuthorizedError as e:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="service_call_unallowed",
            ) from e
        except HabiticaException as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": e.error.message},
            ) from e
        except ClientError as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": str(e)},
            ) from e
        else:
            await self.coordinator.async_request_refresh()

    @property
    def available(self) -> bool:
        """Is entity available."""

        return super().available and self.entity_description.available_fn(
            self.coordinator.data
        )

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