import datetime
from typing import Any
from zoneinfo import ZoneInfo

from peewee import *
from src.tg_compat import Update
from src.tg_compat.ext import ContextTypes

import constants as c
import resources.Environment as Env
from resources import phrases
from src.model.BaseModel import BaseModel
from src.model.Crew import Crew
from src.model.enums.ContextDataKey import ContextDataKey, ContextDataType
from src.model.enums.Location import (
    get_last_new_world,
    get_first_new_world,
    get_by_level,
    Location,
    is_paradise_by_level,
)
from src.model.enums.Screen import Screen
from src.model.enums.Language import Language, LANGUAGE_DEFAULT
from src.model.enums.crew.CrewRole import CrewRole
from src.model.enums.income_tax.IncomeTaxBracket import IncomeTaxBracket
from src.utils.context_utils import (
    get_user_context_data,
    set_user_context_data,
    remove_context_data,
)
from src.utils.math_utils import get_cumulative_percentage_sum


class User(BaseModel):
    """
    User class
    """

    tg_user_id: str | CharField = CharField(max_length=99, unique=True)
    tg_first_name: str | CharField = CharField(max_length=99)
    tg_last_name: str | CharField = CharField(max_length=99)
    tg_username: str | CharField = CharField(max_length=99)
    bounty: int | BigIntegerField = BigIntegerField(default=0)
    total_gained_bounty: int | BigIntegerField = BigIntegerField(
        default=0, constraints=[Check("total_gained_bounty >= 0")]
    )
    total_gained_bounty_unmodified: int | BigIntegerField = BigIntegerField(
        default=0, constraints=[Check("total_gained_bounty_unmodified >= 0")]
    )
    pending_bounty: int | BigIntegerField = BigIntegerField(default=0)
    doc_q_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    game_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    game_challenge_count: int | SmallIntegerField = SmallIntegerField(default=0)
    game_cooldown_start_time: datetime.datetime | DateTimeField = DateTimeField(null=True)
    game_accept_global_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(
        null=True
    )
    bounty_poster_limit: int | SmallIntegerField = SmallIntegerField(default=0)
    location_level: int | SmallIntegerField = SmallIntegerField(default=0)
    should_propose_new_world: bool | BooleanField = BooleanField(default=True)
    can_change_region: bool | BooleanField = BooleanField(default=True)
    fight_immunity_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    fight_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    plunder_immunity_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    plunder_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    impel_down_release_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    impel_down_is_permanent: bool | BooleanField = BooleanField(default=False)
    crew: Crew | ForeignKeyField = ForeignKeyField(Crew, backref="crew_members", null=True)
    crew_join_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    crew_role: int | SmallIntegerField = SmallIntegerField(null=True)
    crew_promotion_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    conscription_end_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    can_create_crew: bool | BooleanField = BooleanField(default=True)
    can_join_crew: bool | BooleanField = BooleanField(default=True)
    crew_davy_back_fight_priority: int | IntegerField = IntegerField(null=True)
    last_message_date: datetime.datetime | DateTimeField = DateTimeField(
        default=datetime.datetime.now
    )
    last_system_interaction_date: datetime.datetime | DateTimeField = DateTimeField(null=True)
    private_screen_list: str | CharField | None = CharField(max_length=99)
    private_screen_step: int | SmallIntegerField | None = SmallIntegerField(null=True)
    private_screen_in_edit_id: int | IntegerField | None = IntegerField(null=True)
    bounty_gift_tax: int | IntegerField = IntegerField(default=0)
    is_admin: bool | BooleanField = BooleanField(default=False)
    devil_fruit_collection_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(
        null=True
    )
    timezone: str | CharField = CharField(max_length=99, null=True)
    is_active: bool | BooleanField = BooleanField(default=True)
    prediction_creation_cooldown_end_date: datetime.datetime | DateTimeField = DateTimeField(
        null=True
    )
    bounty_loan_issue_cool_down_end_date: datetime.datetime | DateTimeField = DateTimeField(
        null=True
    )
    # For Legendary Pirate
    is_exempt_from_global_leaderboard_requirements: bool | BooleanField = BooleanField(
        default=False
    )
    can_collect_daily_reward: bool | BooleanField = BooleanField(default=True)
    fight_scout_count: int | IntegerField = IntegerField(default=0)
    plunder_scout_count: int | IntegerField = IntegerField(default=0)
    language: str | CharField = CharField(max_length=5, default=LANGUAGE_DEFAULT.value)

    # Transient fields
    # The private screen step with which the user arrived to the current screen
    private_screen_previous_step: int = None
    # If to stay on the same screen
    private_screen_stay: bool = False
    # Step to go back to
    private_screen_stay_step: int | None = None
    # If to force going back to previous screen
    private_screen_force_go_back: bool = False

    # If the model should be updated at script end. Sometimes the model is updated in functions
    # where it can not be passed as a parameter, so updating it at the end of the script would
    # overwrite the changes
    should_update_model: bool = True

    # Backref
    bounty_borrowers = None
    legendary_pirates = None
    warlords = None
    daily_bonuses = None

    class Meta:
        db_table = "user"

    def get_bounty_formatted(self) -> str:
        """
        Returns a formatted string of the bounty
        :return: The formatted string e.g. 1,000,000
        """

        from src.utils.string_utils import get_belly_formatted

        return get_belly_formatted(self.bounty)

    def is_arrested(self):
        """
        Returns True if the user is arrested
        :return: True if the user is arrested
        """

        return (
            self.impel_down_release_date is not None
            and self.impel_down_release_date > datetime.datetime.now()
        ) or self.impel_down_is_permanent

    def is_arrested_temporary(self):
        """
        Returns True if the user is temporarily arrested
        :return: True if the user is temporarily arrested
        """

        return self.is_arrested() and not self.impel_down_is_permanent

    def is_arrested_permanent(self):
        """
        Returns True if the user is permanently arrested
        :return: True if the user is permanently arrested
        """

        return self.is_arrested() and self.impel_down_is_permanent

    @staticmethod
    def get_is_not_arrested_statement_condition() -> Any:
        """
        Returns a case statement condition which returns true if the user is not arrested
        :return: The case statement
        """
        return (User.impel_down_is_permanent == False) & (
            (User.impel_down_release_date.is_null())
            | (User.impel_down_release_date < datetime.datetime.now())
        )

    def get_current_impel_down_log(self):
        """
        Returns the current impel down log
        :return: The current impel down log
        """

        from src.model.ImpelDownLog import ImpelDownLog

        return ImpelDownLog.get_current_for_user(self)

    def get_current_bail(self) -> int:
        """
        Get the bail for the user
        :return: The impel_down_log
        """
        current_impel_down_log = self.get_current_impel_down_log()
        if current_impel_down_log is None:
            return 0

        return current_impel_down_log.get_bail()

    def update_private_screen_list(
        self, screen: Screen, previous_screen_list: list[Screen] = None
    ):
        """
        Updates the private screen list
        :param screen: The screen
        :param previous_screen_list: The previous screen list
        """

        if previous_screen_list is not None and len(previous_screen_list) == 0:
            previous_screen_list = None

        if previous_screen_list is not None:
            self.private_screen_step = None  # Will have to update when Skip button is present
            self.private_screen_list = c.STANDARD_SPLIT_CHAR.join(previous_screen_list)

        if self.private_screen_list is None or screen is Screen.PVT_START:
            self.private_screen_list = screen
        else:
            private_screen_list = self.get_private_screen_list()

            if len(private_screen_list) == 0:
                self.private_screen_list = screen
                return

            # Back button, remove last screen
            if len(private_screen_list) > 2 and private_screen_list[-2] is screen:
                private_screen_list.pop()
            elif private_screen_list[-1] is not screen:  # Add screen
                private_screen_list.append(screen)

            self.private_screen_list = c.STANDARD_SPLIT_CHAR.join(private_screen_list)

        if len(self.private_screen_list) == 0:
            self.private_screen_list = None

    def in_edit_mode(self, current_screen=None):
        """
        Returns True if the user is in edit mode, i.e. is expected to send a text input
        as part of a multi-step flow (e.g. crew creation, prediction creation) or is editing
        an existing item.
        :param current_screen: The screen currently resolved for this request. Required to
        correctly detect edit mode based on private_screen_step, since that field is only
        meaningful while the user is actually on one of the screens that use it
        (see Screen.SCREENS_WITH_STEP). Without it, a stale/default private_screen_step value
        left on the user row would incorrectly be treated as "in edit mode" forever.
        :return: True if the user is in edit mode
        """

        from src.model.enums.Screen import SCREENS_WITH_STEP

        if self.private_screen_in_edit_id is not None:
            return True

        if self.private_screen_step is None:
            return False

        if current_screen is None:
            # Caller didn't provide the current screen, fall back to the saved one
            current_screen = self.get_current_private_screen()

        return current_screen in SCREENS_WITH_STEP

    def get_private_screen_list(self) -> list[Screen]:
        """
        Returns the private screen list
        :return: The private screen list
        """

        # OPTION 1 FIX: Safely check if string is completely empty or None
        if not self.private_screen_list:
            return []

        # OPTION 1 FIX: Filter out empty strings during the split loop
        return [
            Screen(str(screen)) 
            for screen in self.private_screen_list.split(c.STANDARD_SPLIT_CHAR) 
            if screen.strip()
        ]

    def get_current_private_screen(self) -> Screen:
        """
        Returns the current saved private screen
        :return: The private screen
        """

        return (self.get_private_screen_list() or [None])[-1]

    def reset_private_screen(self):
        """
        Resets the private screen
        """

        self.private_screen_step = None
        self.private_screen_in_edit_id = None
        self.private_screen_stay = False
        self.private_screen_stay_step = None

    def remove_last_private_screen(self):
        """
        Removes the last private screen
        """

        private_screen_list = self.get_private_screen_list()
        if len(private_screen_list) > 0:
            private_screen_list.pop()
            self.private_screen_list = c.STANDARD_SPLIT_CHAR.join(private_screen_list)

    def add_private_screen(self, screen: Screen):
        """
        Adds a private screen
        :param screen: The screen
        """

        private_screen_list = self.get_private_screen_list()
        private_screen_list.append(screen)
        self.private_screen_list = c.STANDARD_SPLIT_CHAR.join(private_screen_list)

    def replace_last_private_screen(self, screen: Screen):
        """
        Replaces the last private screen
        :param screen: The screen
        """

        self.remove_last_private_screen()
        self.add_private_screen(screen)

    def is_crew_captain(self):
        """
        Returns True if the user is the captain of a Crew
        :return: True if the user is the captain of a Crew
        """

        if self.is_crew_member() and self.crew_role is not None:
            return CrewRole(self.crew_role) is CrewRole.CAPTAIN

        return False

    def is_crew_first_mate(self):
        """
        Returns True if the user is the first mate of a Crew
        :return: True if the user is the first mate of a Crew
        """

        if self.is_crew_member() and self.crew_role is not None:
            return CrewRole(self.crew_role) is CrewRole.FIRST_MATE

        return False

    def is_crew_captain_or_first_mate(self):
        """
        Returns True if the user is the captain or first mate of a Crew
        :return: True if the user is the captain or first mate of a Crew
        """

        return self.is_crew_captain() or self.is_crew_first_mate()

    @staticmethod
    def get_by_tg_id(tg_user_id: str) -> "User":
        """
        Returns the User by the Telegram user id
        :param tg_user_id: The Telegram user id
        :return: The User
        """

        return User.get_or_none(User.tg_user_id == tg_user_id)

    def is_crew_member(self):
        """
        Returns True if the user is in a crew
        :return: True if the user is in a crew
        """

        return self.crew is not None

    def in_same_crew(self, user: "User") -> bool:
        """
        Returns True if the user is in the same crew as the given user
        :param user: The user
        :return: True if the user is in the same crew as the given user
        """

        return self.crew is not None and user is not None and self.crew == user.crew

    def get_markdown_mention(self) -> str:
        """
        Returns the markdown mention of the user
        :return: The markdown mention of the user
        """

        from src.service.message_service import mention_markdown_v2

        return mention_markdown_v2(self.tg_user_id, self.tg_first_name)

    def get_you_markdown_mention(self):
        """
        Returns the markdown mention of the user
        :return: The markdown mention of the user
        """

        from src.service.message_service import mention_markdown_v2

        return mention_markdown_v2(self.tg_user_id, phrases.TEXT_YOU)

    def get_markdown_name(self):
        """
        Returns the markdown name of the user
        :return: The markdown name of the user
        """

        from src.service.message_service import escape_valid_markdown_chars

        return escape_valid_markdown_chars(self.tg_first_name)

    def get_max_bounty(self) -> int:
        """
        Returns the max bounty between the user and the crew average
        :return: The max bounty between the user and the crew average
        """

        if self.is_crew_member():
            return max(self.bounty, self.crew.get_average_bounty())

        return self.bounty

    def in_new_world(self):
        """
        Returns True if the user is in the New World
        :return: True if the user is in the New World
        """

        from src.model.enums.Location import is_new_world_by_level

        return is_new_world_by_level(self.location_level)

    def has_higher_bounty_than_crew_average(self) -> bool:
        """
        Returns True if the user has a higher bounty than the crew average
        :return: True if the user has a higher bounty than the crew average
        """

        if self.is_crew_member():
            return self.bounty > self.crew.get_average_bounty()

        return False

    def has_new_world_bonus(self) -> bool:
        """
        Returns True if the user has the New World bonus
        :return: True if the user has the New World bonus
        """

        return self.in_new_world() and not self.has_bounty_gain_limitations()

    def has_crew_bonus(self) -> bool:
        """
        Returns True if the user has the Crew bonus
        :return: True if the user has the Crew bonus
        """

        return self.is_crew_member() and not self.has_bounty_gain_limitations()

    def has_crew_mvp_bonus(self) -> bool:
        """
        Returns True if the user has the Crew MVP bonus
        :return: True if the user has the Crew MVP bonus
        """

        return (
            self.has_higher_bounty_than_crew_average() and not self.has_bounty_gain_limitations()
        )

    def is_in_paradise(self) -> bool:
        """
        Returns True if the user is in Paradise
        :return: True if the user is in Paradise
        """

        return is_paradise_by_level(self.location_level)

    def get_location(self) -> Location:
        """
        Returns the location of the user
        :return: The location of the user
        """

        return get_by_level(self.location_level)

    def get_location_name(self) -> str:
        """
        Returns the location name of the user
        :return: The location name of the user
        """

        return self.get_location().name

    def has_bounty_gain_limitations(self) -> bool:
        """
        Returns True if the user has bounty gain limitations
        This occurs when the user is in Paradise but has enough bounty to be in the New World, or
        their bounty is higher than that required to reach the final location
        :return: True if the user has bounty gain limitations
        """

        return (
            self.is_in_paradise()
            and (self.bounty + self.pending_bounty) >= get_first_new_world().required_bounty
        ) or ((self.bounty + self.pending_bounty) >= get_last_new_world().required_bounty)

    @staticmethod
    def get_has_bounty_gain_limitations_statement_condition() -> Any:
        """
        Returns the has bounty gain limitations statement condition
        :return: The has bounty gain limitations statement condition
        """

        return (
            (User.location_level < get_first_new_world().level)
            & ((User.bounty + User.pending_bounty) >= get_first_new_world().required_bounty)
        ) | ((User.bounty + User.pending_bounty) >= get_last_new_world().required_bounty)

    def get_in_same_crew_statement_condition(self, other_user: "User") -> Any:
        """
        Returns the in same crew statement condition
        :param other_user: The other user
        :return: The in same crew statement condition
        """

        return (
            User.select(fn.COUNT(User.id)).where(
                (other_user.crew.is_null(False))
                & (other_user.crew == self.crew)
                & (other_user == User.id)
            )
            > 0
        )

    async def is_chat_admin(
        self, update: Update = None, context: ContextTypes.DEFAULT_TYPE = None, chat_id: str = None
    ) -> bool:
        """
        Returns True if the user is an admin of the chat
        :param update: The update
        :param context: The context
        :param chat_id: The chat id
        :return: True if the user is an admin of the chat
        """
        from src.service.user_service import user_is_chat_admin

        return await user_is_chat_admin(self, update=update, context=context, tg_group_id=chat_id)

    async def is_chat_member(self, context: ContextTypes.DEFAULT_TYPE, chat_id: str):
        """
        Returns True if the user is in the chat
        :param context: The context
        :param chat_id: The chat id
        :return: True if the user is in the chat
        """

        from src.service.user_service import user_is_chat_member

        return await user_is_chat_member(self, context=context, tg_group_id=chat_id)

    async def in_authorized_groups(self, context: ContextTypes.DEFAULT_TYPE):
        """
        Returns True if the user is in authorized groups
        :param context: The context
        :return: True if the user is in authorized groups
        """

        group_ids = Env.AUTHORIZED_GROUPS.get_list()
        for group_id in group_ids:
            if await self.is_chat_member(context, group_id):
                return True

        return False

    @staticmethod
    def get_context_data(
        context: ContextTypes.DEFAULT_TYPE,
        key: ContextDataKey,
        tolerate_key_exception: bool = True,
        inner_key: str = None,
    ) -> any:
        """
        Get user data from context
        :param context: The context
        :param key: The key
        :param tolerate_key_exception: If the key is not found, raise CommonChatException instead
        of KeyError
        :param inner_key: The inner key
        :return: The value
        """

        return get_user_context_data(
            context, key, tolerate_key_exception=tolerate_key_exception, inner_key=inner_key
        )

    @staticmethod
    def get_context_data_or_none(
        context: ContextTypes.DEFAULT_TYPE, key: ContextDataKey, inner_key: str = None
    ) -> any:
        """
        Get user data from context or None
        :param context: The context
        :param key: The key
        :param inner_key: The inner key
        :return: The value
        """
        try:
            return get_user_context_data(
                context, key, tolerate_key_exception=False, inner_key=inner_key
            )
        except KeyError:
            return None

    @staticmethod
    def set_context_data(
        context: ContextTypes.DEFAULT_TYPE, key: ContextDataKey, value: Any, inner_key: str = None
    ) -> Any:
        """
        Set user data to context
        :param context: The context
        :param key: The key
        :param value: The value
        :param inner_key: The inner key
        """

        return set_user_context_data(context, key, value, inner_key=inner_key)

    @staticmethod
    def remove_context_data(
        context: ContextTypes.DEFAULT_TYPE, key: ContextDataKey, inner_key: str = None
    ):
        """
        Remove user data from context
        :param context: The context
        :param key: The key
        :param inner_key: The inner key
        """

        try:
            remove_context_data(context, ContextDataType.USER, key, inner_key=inner_key)
        except KeyError:
            pass

    @staticmethod
    def clear_context_filters(context: ContextTypes.DEFAULT_TYPE):
        """
        Clear the context filters
        :param context: The context
        """

        # Clear context filters
        remove_context_data(context, ContextDataType.USER, ContextDataKey.FILTER)
        remove_context_data(context, ContextDataType.USER, ContextDataKey.INBOUND_KEYBOARD)

    def get_expired_bounty_loans(self) -> list:
        """
        Get the expired bounty loans
        :return: The expired bounty loans
        """

        from src.model.enums.BountyLoanStatus import BountyLoanStatus
        from src.model.BountyLoan import BountyLoan

        return self.bounty_borrowers.where(BountyLoan.status == BountyLoanStatus.EXPIRED).order_by(
            BountyLoan.date.asc()
        )

    def has_expired_bounty_loans(self) -> bool:
        """
        Returns True if the user has expired bounty loans
        :return: True if the user has expired bounty loans
        """

        return len(self.get_expired_bounty_loans()) > 0

    def get_expired_bounty_loans_cumulative_percentage(self) -> int:
        """
        Returns the cumulative percentage of the expired bounty loans
        :return: The cumulative percentage of the expired bounty loans
        """

        from src.utils.math_utils import format_percentage_value

        return format_percentage_value(
            get_cumulative_percentage_sum(
                [Env.BOUNTY_LOAN_GARNISH_PERCENTAGE.get_float()]
                * len(self.get_expired_bounty_loans())
            )
        )

    def get_bounty_plus_pending_bounty(self) -> int:
        """
        Returns the bounty plus the pending bounty
        :return: The bounty plus the pending bounty
        """

        return self.bounty + self.pending_bounty

    def has_income_tax(self):
        """
        Returns True if the user has income tax
        :return: True if the user has income tax
        """

        return self.get_income_tax_percentage() > 0

    def reduce_tax_bracket(self, levels_down: int = 1) -> None:
        """
        Reduces the tax bracket by the given amount
        :param levels_down: The number of levels down
        :return: None
        """

        # Get previous tax bracket
        previous_tax_bracket = IncomeTaxBracket.get_previous_bracket(
            self.total_gained_bounty, levels_down
        )

        # Update user
        self.total_gained_bounty = previous_tax_bracket.min_amount
        self.save()

    def get_income_tax_percentage(self) -> float:
        """
        Returns the income tax percentage
        :return: The income tax percentage
        """

        from src.utils.math_utils import format_percentage_value

        return format_percentage_value(
            IncomeTaxBracket.get_bracket(self.total_gained_bounty).percentage
        )

    @staticmethod
    def get_active_interactive_users() -> list:
        """
        Get the active users who interacted with the system
        :return: The active interactive users
        """

        return User.select().where(
            User.last_system_interaction_date
            > datetime.datetime.now()
            - datetime.timedelta(days=Env.INACTIVE_GROUP_USER_DAYS.get_int())
        )

    def get_timezone(self) -> ZoneInfo:
        """
        Returns the timezone of the user
        :return: The timezone of the user
        """

        return ZoneInfo(self.timezone or Env.TZ.get())

    def get_language(self) -> Language:
        """
        Returns the language of the user, used for texts sent in private chat
        :return: The language of the user
        """

        try:
            return Language(self.language)
        except ValueError:
            return LANGUAGE_DEFAULT

    def get_current_time(self) -> datetime.datetime:
        """
        Returns the current time of the user
        :return: The current time of the user
        """

        return datetime.datetime.now(self.get_timezone())

    def get_datetime_formatted(self, date: datetime.datetime) -> str:
        """
        Returns the formatted datetime of the user in their timezone

        :param date: The datetime
        :return: The formatted datetime of the user
        """

        from src.service.date_service import default_datetime_format

        return default_datetime_format(date, self)

    def get_date_formatted(self, date: datetime.datetime) -> str:
        """
        Returns the formatted date of the user in their timezone

        :param date: The date
        :return: The formatted date of the user
        """

        from src.service.date_service import default_date_format

        return default_date_format(date, self)

    def get_crew_role(self):
        """
        Returns the crew role
        :return: The crew role
        """
        return CrewRole(self.crew_role) if self.crew_role is not None else None

    def get_crew_role_description(self):
        """
        Returns the crew role description
        :return: The crew role description
        """

        return self.get_crew_role().get_description()

    def is_legendary_pirate(self) -> bool:
        """
        Returns True if the user is a Legendary Pirate
        :return: True if the user is a Legendary Pirate
        """
        from src.model.LegendaryPirate import LegendaryPirate, ensure_legendary_pirate_schema

        ensure_legendary_pirate_schema()
        now = datetime.datetime.now()
        return self.legendary_pirates.where(
            (LegendaryPirate.end_date.is_null()) | (LegendaryPirate.end_date > now)
        ).count() > 0

    def is_permanent_legendary_pirate(self) -> bool:
        """
        Returns True if the user is a permanent Legendary Pirate
        :return: True if the user is a permanent Legendary Pirate
        """
        from src.model.LegendaryPirate import LegendaryPirate, ensure_legendary_pirate_schema

        ensure_legendary_pirate_schema()
        now = datetime.datetime.now()
        return (
            self.legendary_pirates.where(
                (LegendaryPirate.is_permanent == True)
                & ((LegendaryPirate.end_date.is_null()) | (LegendaryPirate.end_date > now))
            ).count()
            > 0
        )

    def is_warlord(self) -> bool:
        """
        Returns True if the user is a Warlord
        :return: True if the user is a Warlord
        """
        from src.model.Warlord import Warlord

        return self.warlords.where(Warlord.end_date > datetime.datetime.now()).count() > 0

    def get_mention_url(self) -> str:
        """
        Returns the mention url of the user
        :return: The mention url of the user
        """

        return f"tg://user?id={self.tg_user_id}"

    def get_conscription_remaining_time(self) -> str:
        """
        Get the conscription remaining time
        :return: The conscription remaining time
        """
        from src.service.date_service import get_remaining_duration

        return get_remaining_duration(self.conscription_end_date)

    @staticmethod
    def reset_daily_reward():
        """
        Reset the daily reward
        :return: None
        """

        User.update(can_collect_daily_reward=True).execute()


User.create_table()

# Auto-migrate: add new challenge cooldown bucket columns if they don't exist yet
try:
    from playhouse.migrate import MySQLMigrator, migrate as pw_migrate
    from src.model.BaseModel import db_obj

    _migrator = MySQLMigrator(db_obj.get_db())
    pw_migrate(
        _migrator.add_column('user', 'game_challenge_count', SmallIntegerField(default=0)),
    )
except Exception:
    pass  # Column already exists

try:
    from playhouse.migrate import MySQLMigrator, migrate as pw_migrate
    from src.model.BaseModel import db_obj

    _migrator = MySQLMigrator(db_obj.get_db())
    pw_migrate(
        _migrator.add_column('user', 'game_cooldown_start_time', DateTimeField(null=True)),
    )
except Exception:
    pass  # Column already exists

# Auto-migrate: private_screen_step was originally created as NOT NULL with no default,
# causing new rows to default to 0 instead of NULL. Since 0 is a valid step value for some
# screens, this made User.in_edit_mode() always evaluate as True, which silently disabled
# the safeguard against replaying the last completed action on unrelated text messages.
# Safe to run on every startup: MODIFYing an already-nullable column to NULL is a no-op.
try:
    from playhouse.migrate import MySQLMigrator, migrate as pw_migrate
    from src.model.BaseModel import db_obj

    _migrator = MySQLMigrator(db_obj.get_db())
    pw_migrate(
        _migrator.drop_not_null('user', 'private_screen_step'),
    )
except Exception:
    pass  # Column already nullable or migration not needed

# Auto-migrate: composite index on (crew_role, conscription_end_date). Queried every minute by
# end_all_conscription (WHERE crew_role == CONSCRIPT AND conscription_end_date < NOW()) with no
# index, forcing a full table scan on the user table on every single run, 24/7.
try:
    from playhouse.migrate import MySQLMigrator, migrate as pw_migrate
    from src.model.BaseModel import db_obj

    _migrator = MySQLMigrator(db_obj.get_db())
    pw_migrate(
        _migrator.add_index("user", ("crew_role", "conscription_end_date"), unique=False),
    )
except Exception:
    pass  # Index already exists

# Auto-migrate: index on bounty. Leaderboard generation filters and ORDER BY's on this column
# with no index, forcing a full table scan + sort on every leaderboard run.
try:
    from playhouse.migrate import MySQLMigrator, migrate as pw_migrate
    from src.model.BaseModel import db_obj

    _migrator = MySQLMigrator(db_obj.get_db())
    pw_migrate(
        _migrator.add_index("user", ("bounty",), unique=False),
    )
except Exception:
    pass  # Index already exists

# Auto-migrate: add language column for the DM language setting
try:
    from playhouse.migrate import MySQLMigrator, migrate as pw_migrate
    from src.model.BaseModel import db_obj

    _migrator = MySQLMigrator(db_obj.get_db())
    pw_migrate(
        _migrator.add_column(
            "user", "language", CharField(max_length=5, default=LANGUAGE_DEFAULT.value)
        ),
    )
except Exception:
    pass  # Column already exists
