"""Actions for the Habitica integration."""

from __future__ import annotations

from dataclasses import asdict
from datetime import UTC, date, datetime, time
import logging
from typing import TYPE_CHECKING, Any, cast
from uuid import UUID, uuid4

from aiohttp import ClientError
from habiticalib import (
    Checklist,
    Direction,
    Frequency,
    HabiticaException,
    NotAuthorizedError,
    NotFoundError,
    Reminders,
    Repeat,
    Skill,
    Task,
    TaskData,
    TaskPriority,
    TaskType,
    TooManyRequestsError,
)
import voluptuous as vol

from homeassistant.components.todo import ATTR_RENAME
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DATE, ATTR_NAME, CONF_NAME
from homeassistant.core import (
    HomeAssistant,
    ServiceCall,
    ServiceResponse,
    SupportsResponse,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.util import dt as dt_util

from .const import (
    ATTR_ADD_CHECKLIST_ITEM,
    ATTR_ALIAS,
    ATTR_ARGS,
    ATTR_CLEAR_DATE,
    ATTR_CLEAR_REMINDER,
    ATTR_CONFIG_ENTRY,
    ATTR_COST,
    ATTR_COUNTER_DOWN,
    ATTR_COUNTER_UP,
    ATTR_DATA,
    ATTR_DIRECTION,
    ATTR_FREQUENCY,
    ATTR_INTERVAL,
    ATTR_ITEM,
    ATTR_KEYWORD,
    ATTR_NOTES,
    ATTR_PATH,
    ATTR_PRIORITY,
    ATTR_REMINDER,
    ATTR_REMOVE_CHECKLIST_ITEM,
    ATTR_REMOVE_REMINDER,
    ATTR_REMOVE_TAG,
    ATTR_REPEAT,
    ATTR_REPEAT_MONTHLY,
    ATTR_SCORE_CHECKLIST_ITEM,
    ATTR_SKILL,
    ATTR_START_DATE,
    ATTR_STREAK,
    ATTR_TAG,
    ATTR_TARGET,
    ATTR_TASK,
    ATTR_TYPE,
    ATTR_UNSCORE_CHECKLIST_ITEM,
    ATTR_UP_DOWN,
    DOMAIN,
    EVENT_API_CALL_SUCCESS,
    SERVICE_ABORT_QUEST,
    SERVICE_ACCEPT_QUEST,
    SERVICE_API_CALL,
    SERVICE_CANCEL_QUEST,
    SERVICE_CAST_SKILL,
    SERVICE_CREATE_DAILY,
    SERVICE_CREATE_HABIT,
    SERVICE_CREATE_REWARD,
    SERVICE_CREATE_TODO,
    SERVICE_GET_TASKS,
    SERVICE_LEAVE_QUEST,
    SERVICE_REJECT_QUEST,
    SERVICE_SCORE_HABIT,
    SERVICE_SCORE_REWARD,
    SERVICE_START_QUEST,
    SERVICE_TRANSFORMATION,
    SERVICE_UPDATE_DAILY,
    SERVICE_UPDATE_HABIT,
    SERVICE_UPDATE_REWARD,
    SERVICE_UPDATE_TODO,
    WEEK_DAYS,
)
from .coordinator import HabiticaConfigEntry

_LOGGER = logging.getLogger(__name__)


SERVICE_API_CALL_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_NAME): str,
        vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_ARGS): dict,
    }
)

SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
        vol.Required(ATTR_SKILL): cv.string,
        vol.Optional(ATTR_TASK): cv.string,
    }
)

SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
    }
)
SERVICE_SCORE_TASK_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
        vol.Required(ATTR_TASK): cv.string,
        vol.Optional(ATTR_DIRECTION): cv.string,
    }
)

SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
        vol.Required(ATTR_ITEM): cv.string,
        vol.Required(ATTR_TARGET): cv.string,
    }
)

BASE_TASK_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
        vol.Optional(ATTR_RENAME): cv.string,
        vol.Optional(ATTR_NOTES): cv.string,
        vol.Optional(ATTR_TAG): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_ALIAS): vol.All(
            cv.string, cv.matches_regex("^[a-zA-Z0-9-_]*$")
        ),
        vol.Optional(ATTR_COST): vol.All(vol.Coerce(float), vol.Range(0)),
        vol.Optional(ATTR_PRIORITY): vol.All(
            vol.Upper, vol.In(TaskPriority._member_names_)
        ),
        vol.Optional(ATTR_UP_DOWN): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_COUNTER_UP): vol.All(int, vol.Range(0)),
        vol.Optional(ATTR_COUNTER_DOWN): vol.All(int, vol.Range(0)),
        vol.Optional(ATTR_FREQUENCY): vol.Coerce(Frequency),
        vol.Optional(ATTR_DATE): cv.date,
        vol.Optional(ATTR_CLEAR_DATE): cv.boolean,
        vol.Optional(ATTR_REMINDER): vol.All(
            cv.ensure_list, [vol.Any(cv.datetime, cv.time)]
        ),
        vol.Optional(ATTR_REMOVE_REMINDER): vol.All(
            cv.ensure_list, [vol.Any(cv.datetime, cv.time)]
        ),
        vol.Optional(ATTR_CLEAR_REMINDER): cv.boolean,
        vol.Optional(ATTR_ADD_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_REMOVE_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_SCORE_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_UNSCORE_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_START_DATE): cv.date,
        vol.Optional(ATTR_INTERVAL): vol.All(int, vol.Range(0)),
        vol.Optional(ATTR_REPEAT): vol.All(cv.ensure_list, [vol.In(WEEK_DAYS)]),
        vol.Optional(ATTR_REPEAT_MONTHLY): vol.All(
            cv.string, vol.In({"day_of_month", "day_of_week"})
        ),
        vol.Optional(ATTR_STREAK): vol.All(int, vol.Range(0)),
    }
)

SERVICE_UPDATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
    {
        vol.Required(ATTR_TASK): cv.string,
        vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
    }
)

SERVICE_CREATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
    {
        vol.Required(ATTR_NAME): cv.string,
    }
)

SERVICE_DAILY_SCHEMA = {
    vol.Optional(ATTR_REMINDER): vol.All(cv.ensure_list, [cv.time]),
    vol.Optional(ATTR_REMOVE_REMINDER): vol.All(cv.ensure_list, [cv.time]),
}


SERVICE_GET_TASKS_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
        vol.Optional(ATTR_TYPE): vol.All(
            cv.ensure_list, [vol.All(vol.Upper, vol.In({x.name for x in TaskType}))]
        ),
        vol.Optional(ATTR_PRIORITY): vol.All(
            cv.ensure_list, [vol.All(vol.Upper, vol.In({x.name for x in TaskPriority}))]
        ),
        vol.Optional(ATTR_TASK): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_TAG): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_KEYWORD): cv.string,
    }
)

SKILL_MAP = {
    "pickpocket": Skill.PICKPOCKET,
    "backstab": Skill.BACKSTAB,
    "smash": Skill.BRUTAL_SMASH,
    "fireball": Skill.BURST_OF_FLAMES,
}
COST_MAP = {
    "pickpocket": "10 MP",
    "backstab": "15 MP",
    "smash": "10 MP",
    "fireball": "10 MP",
}
ITEMID_MAP = {
    "snowball": Skill.SNOWBALL,
    "spooky_sparkles": Skill.SPOOKY_SPARKLES,
    "seafoam": Skill.SEAFOAM,
    "shiny_seed": Skill.SHINY_SEED,
}

SERVICE_TASK_TYPE_MAP = {
    SERVICE_UPDATE_REWARD: TaskType.REWARD,
    SERVICE_CREATE_REWARD: TaskType.REWARD,
    SERVICE_UPDATE_HABIT: TaskType.HABIT,
    SERVICE_CREATE_HABIT: TaskType.HABIT,
    SERVICE_UPDATE_TODO: TaskType.TODO,
    SERVICE_CREATE_TODO: TaskType.TODO,
    SERVICE_UPDATE_DAILY: TaskType.DAILY,
    SERVICE_CREATE_DAILY: TaskType.DAILY,
}


def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
    """Return config entry or raise if not found or not loaded."""
    if not (entry := hass.config_entries.async_get_entry(entry_id)):
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key="entry_not_found",
        )
    if entry.state is not ConfigEntryState.LOADED:
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key="entry_not_loaded",
        )
    return entry


def async_setup_services(hass: HomeAssistant) -> None:  # noqa: C901
    """Set up services for Habitica integration."""

    async def handle_api_call(call: ServiceCall) -> None:
        async_create_issue(
            hass,
            DOMAIN,
            "deprecated_api_call",
            breaks_in_ha_version="2025.6.0",
            is_fixable=False,
            severity=IssueSeverity.WARNING,
            translation_key="deprecated_api_call",
        )
        _LOGGER.warning(
            "Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0"
        )

        name = call.data[ATTR_NAME]
        path = call.data[ATTR_PATH]
        entries: list[HabiticaConfigEntry] = hass.config_entries.async_entries(DOMAIN)

        api = None
        for entry in entries:
            if entry.data[CONF_NAME] == name:
                api = await entry.runtime_data.habitica.habitipy()
                break
        if api is None:
            _LOGGER.error("API_CALL: User '%s' not configured", name)
            return
        try:
            for element in path:
                api = api[element]
        except KeyError:
            _LOGGER.error(
                "API_CALL: Path %s is invalid for API on '{%s}' element", path, element
            )
            return
        kwargs = call.data.get(ATTR_ARGS, {})
        data = await api(**kwargs)
        hass.bus.async_fire(
            EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
        )

    async def cast_skill(call: ServiceCall) -> ServiceResponse:
        """Skill action."""
        entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
        coordinator = entry.runtime_data

        skill = SKILL_MAP[call.data[ATTR_SKILL]]
        cost = COST_MAP[call.data[ATTR_SKILL]]

        try:
            task_id = next(
                task.id
                for task in coordinator.data.tasks
                if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
            )
        except StopIteration as e:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="task_not_found",
                translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
            ) from e

        try:
            response = await coordinator.habitica.cast_skill(skill, task_id)
        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="not_enough_mana",
                translation_placeholders={
                    "cost": cost,
                    "mana": f"{int(coordinator.data.user.stats.mp or 0)} MP",
                },
            ) from e
        except NotFoundError as e:
            # could also be task not found, but the task is looked up
            # before the request, so most likely wrong skill selected
            # or the skill hasn't been unlocked yet.
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="skill_not_found",
                translation_placeholders={"skill": call.data[ATTR_SKILL]},
            ) from e
        except HabiticaException as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": str(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 coordinator.async_request_refresh()
            return asdict(response.data)

    async def manage_quests(call: ServiceCall) -> ServiceResponse:
        """Accept, reject, start, leave or cancel quests."""
        entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
        coordinator = entry.runtime_data

        FUNC_MAP = {
            SERVICE_ABORT_QUEST: coordinator.habitica.abort_quest,
            SERVICE_ACCEPT_QUEST: coordinator.habitica.accept_quest,
            SERVICE_CANCEL_QUEST: coordinator.habitica.cancel_quest,
            SERVICE_LEAVE_QUEST: coordinator.habitica.leave_quest,
            SERVICE_REJECT_QUEST: coordinator.habitica.reject_quest,
            SERVICE_START_QUEST: coordinator.habitica.start_quest,
        }

        func = FUNC_MAP[call.service]

        try:
            response = await func()
        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="quest_action_unallowed"
            ) from e
        except NotFoundError as e:
            raise ServiceValidationError(
                translation_domain=DOMAIN, translation_key="quest_not_found"
            ) from e
        except HabiticaException as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": str(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:
            return asdict(response.data)

    for service in (
        SERVICE_ABORT_QUEST,
        SERVICE_ACCEPT_QUEST,
        SERVICE_CANCEL_QUEST,
        SERVICE_LEAVE_QUEST,
        SERVICE_REJECT_QUEST,
        SERVICE_START_QUEST,
    ):
        hass.services.async_register(
            DOMAIN,
            service,
            manage_quests,
            schema=SERVICE_MANAGE_QUEST_SCHEMA,
            supports_response=SupportsResponse.ONLY,
        )

    async def score_task(call: ServiceCall) -> ServiceResponse:
        """Score a task action."""
        entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
        coordinator = entry.runtime_data

        direction = (
            Direction.DOWN if call.data.get(ATTR_DIRECTION) == "down" else Direction.UP
        )
        try:
            task_id, task_value = next(
                (task.id, task.value)
                for task in coordinator.data.tasks
                if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
            )
        except StopIteration as e:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="task_not_found",
                translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
            ) from e

        if TYPE_CHECKING:
            assert task_id
        try:
            response = await coordinator.habitica.update_score(task_id, direction)
        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:
            if task_value is not None:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="not_enough_gold",
                    translation_placeholders={
                        "gold": f"{(coordinator.data.user.stats.gp or 0):.2f} GP",
                        "cost": f"{task_value:.2f} GP",
                    },
                ) from e
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": e.error.message},
            ) from e
        except HabiticaException as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": str(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 coordinator.async_request_refresh()
            return asdict(response.data)

    async def transformation(call: ServiceCall) -> ServiceResponse:
        """User a transformation item on a player character."""

        entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
        coordinator = entry.runtime_data

        item = ITEMID_MAP[call.data[ATTR_ITEM]]
        # check if target is self
        if call.data[ATTR_TARGET] in (
            str(coordinator.data.user.id),
            coordinator.data.user.profile.name,
            coordinator.data.user.auth.local.username,
        ):
            target_id = coordinator.data.user.id
        else:
            # check if target is a party member
            try:
                party = await coordinator.habitica.get_group_members(public_fields=True)
            except NotFoundError as e:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="party_not_found",
                ) from e
            except HabiticaException as e:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="service_call_exception",
                    translation_placeholders={"reason": str(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
            try:
                target_id = next(
                    member.id
                    for member in party.data
                    if member.id
                    and call.data[ATTR_TARGET].lower()
                    in (
                        str(member.id),
                        str(member.auth.local.username).lower(),
                        str(member.profile.name).lower(),
                    )
                )
            except StopIteration as e:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="target_not_found",
                    translation_placeholders={"target": f"'{call.data[ATTR_TARGET]}'"},
                ) from e
        try:
            response = await coordinator.habitica.cast_skill(item, target_id)
        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="item_not_found",
                translation_placeholders={"item": call.data[ATTR_ITEM]},
            ) from e
        except HabiticaException as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": str(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:
            return asdict(response.data)

    async def get_tasks(call: ServiceCall) -> ServiceResponse:
        """Get tasks action."""

        entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
        coordinator = entry.runtime_data
        response: list[TaskData] = coordinator.data.tasks

        if types := {TaskType[x] for x in call.data.get(ATTR_TYPE, [])}:
            response = [task for task in response if task.Type in types]

        if priority := {TaskPriority[x] for x in call.data.get(ATTR_PRIORITY, [])}:
            response = [task for task in response if task.priority in priority]

        if tasks := call.data.get(ATTR_TASK):
            response = [
                task
                for task in response
                if str(task.id) in tasks or task.alias in tasks or task.text in tasks
            ]

        if tags := call.data.get(ATTR_TAG):
            tag_ids = {
                tag.id
                for tag in coordinator.data.user.tags
                if (tag.name and tag.name.lower())
                in (tag.lower() for tag in tags)  # Case-insensitive matching
                and tag.id
            }

            response = [
                task
                for task in response
                if any(tag_id in task.tags for tag_id in tag_ids if task.tags)
            ]
        if keyword := call.data.get(ATTR_KEYWORD):
            keyword = keyword.lower()
            response = [
                task
                for task in response
                if (task.text and keyword in task.text.lower())
                or (task.notes and keyword in task.notes.lower())
                or any(keyword in item.text.lower() for item in task.checklist)
            ]
        result: dict[str, Any] = {
            "tasks": [task.to_dict(omit_none=False) for task in response]
        }

        return result

    async def create_or_update_task(call: ServiceCall) -> ServiceResponse:  # noqa: C901
        """Create or update task action."""
        entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
        coordinator = entry.runtime_data
        await coordinator.async_refresh()
        is_update = call.service in (
            SERVICE_UPDATE_HABIT,
            SERVICE_UPDATE_REWARD,
            SERVICE_UPDATE_TODO,
            SERVICE_UPDATE_DAILY,
        )
        task_type = SERVICE_TASK_TYPE_MAP[call.service]
        current_task = None

        if is_update:
            try:
                current_task = next(
                    task
                    for task in coordinator.data.tasks
                    if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
                    and task.Type is task_type
                )
            except StopIteration as e:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="task_not_found",
                    translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
                ) from e

        data = Task()

        if not is_update:
            data["type"] = task_type

        if (text := call.data.get(ATTR_RENAME)) or (text := call.data.get(ATTR_NAME)):
            data["text"] = text

        if (notes := call.data.get(ATTR_NOTES)) is not None:
            data["notes"] = notes

        tags = cast(list[str], call.data.get(ATTR_TAG))
        remove_tags = cast(list[str], call.data.get(ATTR_REMOVE_TAG))

        if tags or remove_tags:
            update_tags = set(current_task.tags) if current_task else set()
            user_tags = {
                tag.name.lower(): tag.id
                for tag in coordinator.data.user.tags
                if tag.id and tag.name
            }

            if tags:
                # Creates new tag if it doesn't exist
                async def create_tag(tag_name: str) -> UUID:
                    tag_id = (await coordinator.habitica.create_tag(tag_name)).data.id
                    if TYPE_CHECKING:
                        assert tag_id
                    return tag_id

                try:
                    update_tags.update(
                        {
                            user_tags.get(tag_name.lower())
                            or (await create_tag(tag_name))
                            for tag_name in tags
                        }
                    )
                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 HabiticaException as e:
                    raise HomeAssistantError(
                        translation_domain=DOMAIN,
                        translation_key="service_call_exception",
                        translation_placeholders={"reason": str(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

            if remove_tags:
                update_tags.difference_update(
                    {
                        user_tags[tag_name.lower()]
                        for tag_name in remove_tags
                        if tag_name.lower() in user_tags
                    }
                )

            data["tags"] = list(update_tags)

        if (alias := call.data.get(ATTR_ALIAS)) is not None:
            data["alias"] = alias

        if (cost := call.data.get(ATTR_COST)) is not None:
            data["value"] = cost

        if priority := call.data.get(ATTR_PRIORITY):
            data["priority"] = TaskPriority[priority]

        if frequency := call.data.get(ATTR_FREQUENCY):
            data["frequency"] = frequency
        else:
            frequency = current_task.frequency if current_task else Frequency.WEEKLY

        if up_down := call.data.get(ATTR_UP_DOWN):
            data["up"] = "up" in up_down
            data["down"] = "down" in up_down

        if counter_up := call.data.get(ATTR_COUNTER_UP):
            data["counterUp"] = counter_up

        if counter_down := call.data.get(ATTR_COUNTER_DOWN):
            data["counterDown"] = counter_down

        if due_date := call.data.get(ATTR_DATE):
            data["date"] = datetime.combine(due_date, time())

        if call.data.get(ATTR_CLEAR_DATE):
            data["date"] = None

        checklist = current_task.checklist if current_task else []

        if add_checklist_item := call.data.get(ATTR_ADD_CHECKLIST_ITEM):
            checklist.extend(
                Checklist(completed=False, id=uuid4(), text=item)
                for item in add_checklist_item
                if not any(i.text == item for i in checklist)
            )
        if remove_checklist_item := call.data.get(ATTR_REMOVE_CHECKLIST_ITEM):
            checklist = [
                item for item in checklist if item.text not in remove_checklist_item
            ]

        if score_checklist_item := call.data.get(ATTR_SCORE_CHECKLIST_ITEM):
            for item in checklist:
                if item.text in score_checklist_item:
                    item.completed = True

        if unscore_checklist_item := call.data.get(ATTR_UNSCORE_CHECKLIST_ITEM):
            for item in checklist:
                if item.text in unscore_checklist_item:
                    item.completed = False
        if (
            add_checklist_item
            or remove_checklist_item
            or score_checklist_item
            or unscore_checklist_item
        ):
            data["checklist"] = checklist

        reminders = current_task.reminders if current_task else []

        if add_reminders := call.data.get(ATTR_REMINDER):
            if task_type is TaskType.TODO:
                existing_reminder_datetimes = {
                    r.time.replace(tzinfo=None) for r in reminders
                }

                reminders.extend(
                    Reminders(id=uuid4(), time=r)
                    for r in add_reminders
                    if r not in existing_reminder_datetimes
                )
            if task_type is TaskType.DAILY:
                existing_reminder_times = {
                    r.time.time().replace(microsecond=0, second=0) for r in reminders
                }

                reminders.extend(
                    Reminders(
                        id=uuid4(),
                        time=datetime.combine(date.today(), r, tzinfo=UTC),
                    )
                    for r in add_reminders
                    if r not in existing_reminder_times
                )

        if remove_reminder := call.data.get(ATTR_REMOVE_REMINDER):
            if task_type is TaskType.TODO:
                reminders = list(
                    filter(
                        lambda r: r.time.replace(tzinfo=None) not in remove_reminder,
                        reminders,
                    )
                )
            if task_type is TaskType.DAILY:
                reminders = list(
                    filter(
                        lambda r: r.time.time().replace(second=0, microsecond=0)
                        not in remove_reminder,
                        reminders,
                    )
                )

        if clear_reminders := call.data.get(ATTR_CLEAR_REMINDER):
            reminders = []

        if add_reminders or remove_reminder or clear_reminders:
            data["reminders"] = reminders

        if start_date := call.data.get(ATTR_START_DATE):
            data["startDate"] = datetime.combine(start_date, time())
        else:
            start_date = (
                current_task.startDate
                if current_task and current_task.startDate
                else dt_util.start_of_local_day()
            )
        if repeat := call.data.get(ATTR_REPEAT):
            if frequency is Frequency.WEEKLY:
                data["repeat"] = Repeat(**{d: d in repeat for d in WEEK_DAYS})
            else:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="frequency_not_weekly",
                )
        if repeat_monthly := call.data.get(ATTR_REPEAT_MONTHLY):
            if frequency is not Frequency.MONTHLY:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="frequency_not_monthly",
                )

            if repeat_monthly == "day_of_week":
                weekday = start_date.weekday()
                data["weeksOfMonth"] = [(start_date.day - 1) // 7]
                data["repeat"] = Repeat(
                    **{day: i == weekday for i, day in enumerate(WEEK_DAYS)}
                )
                data["daysOfMonth"] = []

            else:
                data["daysOfMonth"] = [start_date.day]
                data["weeksOfMonth"] = []

        if interval := call.data.get(ATTR_INTERVAL):
            data["everyX"] = interval

        if streak := call.data.get(ATTR_STREAK):
            data["streak"] = streak

        try:
            if is_update:
                if TYPE_CHECKING:
                    assert current_task
                    assert current_task.id
                response = await coordinator.habitica.update_task(current_task.id, data)
            else:
                response = await coordinator.habitica.create_task(data)
        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 HabiticaException as e:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="service_call_exception",
                translation_placeholders={"reason": str(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:
            return response.data.to_dict(omit_none=True)

    for service in (
        SERVICE_UPDATE_DAILY,
        SERVICE_UPDATE_HABIT,
        SERVICE_UPDATE_REWARD,
        SERVICE_UPDATE_TODO,
    ):
        hass.services.async_register(
            DOMAIN,
            service,
            create_or_update_task,
            schema=SERVICE_UPDATE_TASK_SCHEMA,
            supports_response=SupportsResponse.ONLY,
        )
    for service in (
        SERVICE_CREATE_DAILY,
        SERVICE_CREATE_HABIT,
        SERVICE_CREATE_REWARD,
        SERVICE_CREATE_TODO,
    ):
        hass.services.async_register(
            DOMAIN,
            service,
            create_or_update_task,
            schema=SERVICE_CREATE_TASK_SCHEMA,
            supports_response=SupportsResponse.ONLY,
        )
    hass.services.async_register(
        DOMAIN,
        SERVICE_API_CALL,
        handle_api_call,
        schema=SERVICE_API_CALL_SCHEMA,
    )

    hass.services.async_register(
        DOMAIN,
        SERVICE_CAST_SKILL,
        cast_skill,
        schema=SERVICE_CAST_SKILL_SCHEMA,
        supports_response=SupportsResponse.ONLY,
    )

    hass.services.async_register(
        DOMAIN,
        SERVICE_SCORE_HABIT,
        score_task,
        schema=SERVICE_SCORE_TASK_SCHEMA,
        supports_response=SupportsResponse.ONLY,
    )
    hass.services.async_register(
        DOMAIN,
        SERVICE_SCORE_REWARD,
        score_task,
        schema=SERVICE_SCORE_TASK_SCHEMA,
        supports_response=SupportsResponse.ONLY,
    )

    hass.services.async_register(
        DOMAIN,
        SERVICE_TRANSFORMATION,
        transformation,
        schema=SERVICE_TRANSFORMATION_SCHEMA,
        supports_response=SupportsResponse.ONLY,
    )
    hass.services.async_register(
        DOMAIN,
        SERVICE_GET_TASKS,
        get_tasks,
        schema=SERVICE_GET_TASKS_SCHEMA,
        supports_response=SupportsResponse.ONLY,
    )
