#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains objects that represent owned gifts."""

import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Final, Optional

from telegram import constants
from telegram._gifts import Gift
from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
from telegram._uniquegift import UniqueGift
from telegram._user import User
from telegram._utils import enum
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.entities import parse_message_entities, parse_message_entity
from telegram._utils.types import JSONDict

if TYPE_CHECKING:
    from telegram import Bot


class OwnedGift(TelegramObject):
    """This object describes a gift received and owned by a user or a chat. Currently, it
    can be one of:

    * :class:`telegram.OwnedGiftRegular`
    * :class:`telegram.OwnedGiftUnique`

    Objects of this class are comparable in terms of equality. Two objects of this class are
    considered equal, if their :attr:`type` is equal.

    .. versionadded:: 22.1

    Args:
        type (:obj:`str`): Type of the owned gift.

    Attributes:
        type (:obj:`str`): Type of the owned gift.
    """

    __slots__ = ("type",)

    REGULAR: Final[str] = constants.OwnedGiftType.REGULAR
    """:const:`telegram.constants.OwnedGiftType.REGULAR`"""
    UNIQUE: Final[str] = constants.OwnedGiftType.UNIQUE
    """:const:`telegram.constants.OwnedGiftType.UNIQUE`"""

    def __init__(
        self,
        type: str,  # pylint: disable=redefined-builtin
        *,
        api_kwargs: Optional[JSONDict] = None,
    ) -> None:
        super().__init__(api_kwargs=api_kwargs)
        self.type: str = enum.get_member(constants.OwnedGiftType, type, type)

        self._id_attrs = (self.type,)
        self._freeze()

    @classmethod
    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGift":
        """Converts JSON data to the appropriate :class:`OwnedGift` object, i.e. takes
        care of selecting the correct subclass.

        Args:
            data (dict[:obj:`str`, ...]): The JSON data.
            bot (:class:`telegram.Bot`, optional): The bot associated with this object.

        Returns:
            The Telegram object.

        """
        data = cls._parse_data(data)

        _class_mapping: dict[str, type[OwnedGift]] = {
            cls.REGULAR: OwnedGiftRegular,
            cls.UNIQUE: OwnedGiftUnique,
        }

        if cls is OwnedGift and data.get("type") in _class_mapping:
            return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)

        return super().de_json(data=data, bot=bot)


class OwnedGifts(TelegramObject):
    """Contains the list of gifts received and owned by a user or a chat.

    Objects of this class are comparable in terms of equality. Two objects of this class are
    considered equal, if their :attr:`total_count` and :attr:`gifts` are equal.

    .. versionadded:: 22.1

    Args:
        total_count (:obj:`int`): The total number of gifts owned by the user or the chat.
        gifts (Sequence[:class:`telegram.OwnedGift`]): The list of gifts.
        next_offset (:obj:`str`, optional): Offset for the next request. If empty,
            then there are no more results.

    Attributes:
        total_count (:obj:`int`): The total number of gifts owned by the user or the chat.
        gifts (Sequence[:class:`telegram.OwnedGift`]): The list of gifts.
        next_offset (:obj:`str`): Optional. Offset for the next request. If empty,
            then there are no more results.
    """

    __slots__ = (
        "gifts",
        "next_offset",
        "total_count",
    )

    def __init__(
        self,
        total_count: int,
        gifts: Sequence[OwnedGift],
        next_offset: Optional[str] = None,
        *,
        api_kwargs: Optional[JSONDict] = None,
    ):
        super().__init__(api_kwargs=api_kwargs)
        self.total_count: int = total_count
        self.gifts: tuple[OwnedGift, ...] = parse_sequence_arg(gifts)
        self.next_offset: Optional[str] = next_offset

        self._id_attrs = (self.total_count, self.gifts)

        self._freeze()

    @classmethod
    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGifts":
        """See :meth:`telegram.TelegramObject.de_json`."""
        data = cls._parse_data(data)

        data["gifts"] = de_list_optional(data.get("gifts"), OwnedGift, bot)
        return super().de_json(data=data, bot=bot)


class OwnedGiftRegular(OwnedGift):
    """Describes a regular gift owned by a user or a chat.

    Objects of this class are comparable in terms of equality. Two objects of this class are
    considered equal, if their :attr:`gift` and :attr:`send_date` are equal.

    .. versionadded:: 22.1

    Args:
        gift (:class:`telegram.Gift`): Information about the regular gift.
        owned_gift_id (:obj:`str`, optional): Unique identifier of the gift for the bot; for
            gifts received on behalf of business accounts only.
        sender_user (:class:`telegram.User`, optional): Sender of the gift if it is a known user.
        send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`.
            |datetime_localization|.
        text (:obj:`str`, optional): Text of the message that was added to the gift.
        entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that
            appear in the text.
        is_private (:obj:`bool`, optional): :obj:`True`, if the sender and gift text are shown
            only to the gift receiver; otherwise, everyone will be able to see them.
        is_saved (:obj:`bool`, optional): :obj:`True`, if the gift is displayed on the account's
            profile page; for gifts received on behalf of business accounts only.
        can_be_upgraded (:obj:`bool`, optional): :obj:`True`, if the gift can be upgraded to a
            unique gift; for gifts received on behalf of business accounts only.
        was_refunded (:obj:`bool`, optional): :obj:`True`, if the gift was refunded and isn't
            available anymore.
        convert_star_count (:obj:`int`, optional): Number of Telegram Stars that can be
            claimed by the receiver instead of the gift; omitted if the gift cannot be converted
            to Telegram Stars.
        prepaid_upgrade_star_count (:obj:`int`, optional): Number of Telegram Stars that were
            paid by the sender for the ability to upgrade the gift.

    Attributes:
        type (:obj:`str`): Type of the gift, always :attr:`~telegram.OwnedGift.REGULAR`.
        gift (:class:`telegram.Gift`): Information about the regular gift.
        owned_gift_id (:obj:`str`): Optional. Unique identifier of the gift for the bot; for
            gifts received on behalf of business accounts only.
        sender_user (:class:`telegram.User`): Optional. Sender of the gift if it is a known user.
        send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`.
            |datetime_localization|.
        text (:obj:`str`): Optional. Text of the message that was added to the gift.
        entities (Sequence[:class:`telegram.MessageEntity`]): Optional. Special entities that
            appear in the text.
        is_private (:obj:`bool`): Optional. :obj:`True`, if the sender and gift text are shown
            only to the gift receiver; otherwise, everyone will be able to see them.
        is_saved (:obj:`bool`): Optional. :obj:`True`, if the gift is displayed on the account's
            profile page; for gifts received on behalf of business accounts only.
        can_be_upgraded (:obj:`bool`): Optional. :obj:`True`, if the gift can be upgraded to a
            unique gift; for gifts received on behalf of business accounts only.
        was_refunded (:obj:`bool`): Optional. :obj:`True`, if the gift was refunded and isn't
            available anymore.
        convert_star_count (:obj:`int`): Optional. Number of Telegram Stars that can be
            claimed by the receiver instead of the gift; omitted if the gift cannot be converted
            to Telegram Stars.
        prepaid_upgrade_star_count (:obj:`int`): Optional. Number of Telegram Stars that were
            paid by the sender for the ability to upgrade the gift.

    """

    __slots__ = (
        "can_be_upgraded",
        "convert_star_count",
        "entities",
        "gift",
        "is_private",
        "is_saved",
        "owned_gift_id",
        "prepaid_upgrade_star_count",
        "send_date",
        "sender_user",
        "text",
        "was_refunded",
    )

    def __init__(
        self,
        gift: Gift,
        send_date: dtm.datetime,
        owned_gift_id: Optional[str] = None,
        sender_user: Optional[User] = None,
        text: Optional[str] = None,
        entities: Optional[Sequence[MessageEntity]] = None,
        is_private: Optional[bool] = None,
        is_saved: Optional[bool] = None,
        can_be_upgraded: Optional[bool] = None,
        was_refunded: Optional[bool] = None,
        convert_star_count: Optional[int] = None,
        prepaid_upgrade_star_count: Optional[int] = None,
        *,
        api_kwargs: Optional[JSONDict] = None,
    ) -> None:
        super().__init__(type=OwnedGift.REGULAR, api_kwargs=api_kwargs)

        with self._unfrozen():
            self.gift: Gift = gift
            self.send_date: dtm.datetime = send_date
            self.owned_gift_id: Optional[str] = owned_gift_id
            self.sender_user: Optional[User] = sender_user
            self.text: Optional[str] = text
            self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities)
            self.is_private: Optional[bool] = is_private
            self.is_saved: Optional[bool] = is_saved
            self.can_be_upgraded: Optional[bool] = can_be_upgraded
            self.was_refunded: Optional[bool] = was_refunded
            self.convert_star_count: Optional[int] = convert_star_count
            self.prepaid_upgrade_star_count: Optional[int] = prepaid_upgrade_star_count

            self._id_attrs = (self.type, self.gift, self.send_date)

    @classmethod
    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGiftRegular":
        """See :meth:`telegram.OwnedGift.de_json`."""
        data = cls._parse_data(data)

        loc_tzinfo = extract_tzinfo_from_defaults(bot)
        data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo)
        data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot)
        data["gift"] = de_json_optional(data.get("gift"), Gift, bot)
        data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot)

        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]

    def parse_entity(self, entity: MessageEntity) -> str:
        """Returns the text in :attr:`text`
        from a given :class:`telegram.MessageEntity` of :attr:`entities`.

        Note:
            This method is present because Telegram calculates the offset and length in
            UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
            (That is, you can't just slice ``OwnedGiftRegular.text`` with the offset and length.)

        Args:
            entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
                be an entity that belongs to :attr:`entities`.

        Returns:
            :obj:`str`: The text of the given entity.

        Raises:
            RuntimeError: If the owned gift has no text.

        """
        if not self.text:
            raise RuntimeError("This OwnedGiftRegular has no 'text'.")

        return parse_message_entity(self.text, entity)

    def parse_entities(self, types: Optional[list[str]] = None) -> dict[MessageEntity, str]:
        """
        Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
        It contains entities from this owned gift's text filtered by their ``type`` attribute as
        the key, and the text that each entity belongs to as the value of the :obj:`dict`.

        Note:
            This method should always be used instead of the :attr:`entities`
            attribute, since it calculates the correct substring from the message text based on
            UTF-16 codepoints. See :attr:`parse_entity` for more info.

        Args:
            types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
                    ``type`` attribute of an entity is contained in this list, it will be returned.
                    Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.

        Returns:
            dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
            the text that belongs to them, calculated based on UTF-16 codepoints.

        Raises:
            RuntimeError: If the owned gift has no text.

        """
        if not self.text:
            raise RuntimeError("This OwnedGiftRegular has no 'text'.")

        return parse_message_entities(self.text, self.entities, types)


class OwnedGiftUnique(OwnedGift):
    """
    Describes a unique gift received and owned by a user or a chat.

    Objects of this class are comparable in terms of equality. Two objects of this class are
    considered equal, if their :attr:`gift` and :attr:`send_date` are equal.

    .. versionadded:: 22.1

    Args:
        gift (:class:`telegram.UniqueGift`): Information about the unique gift.
        owned_gift_id (:obj:`str`, optional): Unique identifier of the received gift for the
            bot; for gifts received on behalf of business accounts only.
        sender_user (:class:`telegram.User`, optional): Sender of the gift if it is a known user.
        send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`.
            |datetime_localization|
        is_saved (:obj:`bool`, optional): :obj:`True`, if the gift is displayed on the account's
            profile page; for gifts received on behalf of business accounts only.
        can_be_transferred (:obj:`bool`, optional): :obj:`True`, if the gift can be transferred to
            another owner; for gifts received on behalf of business accounts only.
        transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid
            to transfer the gift; omitted if the bot cannot transfer the gift.
        next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be
            transferred. If it's in the past, then the gift can be transferred now.
            |datetime_localization|
            .. versionadded:: 22.3

    Attributes:
        type (:obj:`str`): Type of the owned gift, always :tg-const:`~telegram.OwnedGift.UNIQUE`.
        gift (:class:`telegram.UniqueGift`): Information about the unique gift.
        owned_gift_id (:obj:`str`): Optional. Unique identifier of the received gift for the
            bot; for gifts received on behalf of business accounts only.
        sender_user (:class:`telegram.User`): Optional. Sender of the gift if it is a known user.
        send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`.
            |datetime_localization|
        is_saved (:obj:`bool`): Optional. :obj:`True`, if the gift is displayed on the account's
            profile page; for gifts received on behalf of business accounts only.
        can_be_transferred (:obj:`bool`): Optional. :obj:`True`, if the gift can be transferred to
            another owner; for gifts received on behalf of business accounts only.
        transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid
            to transfer the gift; omitted if the bot cannot transfer the gift.
        next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be
            transferred. If it's in the past, then the gift can be transferred now.
            |datetime_localization|
            .. versionadded:: 22.3
    """

    __slots__ = (
        "can_be_transferred",
        "gift",
        "is_saved",
        "next_transfer_date",
        "owned_gift_id",
        "send_date",
        "sender_user",
        "transfer_star_count",
    )

    def __init__(
        self,
        gift: UniqueGift,
        send_date: dtm.datetime,
        owned_gift_id: Optional[str] = None,
        sender_user: Optional[User] = None,
        is_saved: Optional[bool] = None,
        can_be_transferred: Optional[bool] = None,
        transfer_star_count: Optional[int] = None,
        next_transfer_date: Optional[dtm.datetime] = None,
        *,
        api_kwargs: Optional[JSONDict] = None,
    ) -> None:
        super().__init__(type=OwnedGift.UNIQUE, api_kwargs=api_kwargs)

        with self._unfrozen():
            self.gift: UniqueGift = gift
            self.send_date: dtm.datetime = send_date
            self.owned_gift_id: Optional[str] = owned_gift_id
            self.sender_user: Optional[User] = sender_user
            self.is_saved: Optional[bool] = is_saved
            self.can_be_transferred: Optional[bool] = can_be_transferred
            self.transfer_star_count: Optional[int] = transfer_star_count
            self.next_transfer_date: Optional[dtm.datetime] = next_transfer_date

            self._id_attrs = (self.type, self.gift, self.send_date)

    @classmethod
    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGiftUnique":
        """See :meth:`telegram.OwnedGift.de_json`."""
        data = cls._parse_data(data)

        loc_tzinfo = extract_tzinfo_from_defaults(bot)
        data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo)
        data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot)
        data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot)
        data["next_transfer_date"] = from_timestamp(
            data.get("next_transfer_date"), tzinfo=loc_tzinfo
        )

        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
