import datetime

from peewee import *

import resources.Environment as Env
from src.model.BaseModel import BaseModel
from src.model.GroupChat import GroupChat
from src.model.User import User
from src.model.enums.daily_reward.DailyRewardBonus import DailyRewardBonus, DailyRewardBonusType
from src.model.enums.daily_reward.DailyRewardLimitation import DailyRewardLimitation
from src.model.enums.daily_reward.DailyRewardPrizeSource import DailyRewardPrizeSource
from src.model.enums.daily_reward.DailyRewardPrizeType import DailyRewardPrizeType
from src.service.date_service import get_day_in_past, is_same_day
from src.utils.math_utils import get_value_from_percentage


class DailyReward(BaseModel):
    """
    DailyReward class
    """

    user: User | ForeignKeyField = ForeignKeyField(User, backref="daily_bonuses")
    base_amount: int | BigIntegerField = BigIntegerField()
    bonus_list: str | TextField = TextField()
    total_amount: int | BigIntegerField = BigIntegerField()
    streak_count: int | IntegerField = IntegerField()
    limitation: str | DailyRewardLimitation = CharField(max_length=20)
    prize_type: DailyRewardPrizeType | CharField = CharField(max_length=20, null=True)
    prize_value: str | CharField = CharField(max_length=50, null=True)
    prize_source: DailyRewardPrizeSource | CharField = CharField(max_length=20, null=True)
    group_chat: GroupChat | ForeignKeyField = ForeignKeyField(
        GroupChat, backref="daily_rewards", null=True
    )
    message_id: int | IntegerField = IntegerField(null=True)
    message_id_prize: int | IntegerField = IntegerField(null=True)

    class Meta:
        db_table = "daily_reward"

    @staticmethod
    def reset():
        """
        Reset the daily rewards
        """

        User.reset_daily_reward()

    @staticmethod
    def get_daily_reward_reset_times():
        """
        Get how many times daily reward is reset during the day
        """
        cron_expression = Env.CRON_DAILY_REWARD.get()
        cron_parts = cron_expression.split()

        # Every hour, so 24 times
        if cron_parts[1] == "*":
            return 24

        # Every x hours
        if "*/" in cron_parts[1]:
            return 24 // int(cron_parts[1].split("/")[1])

        # Once
        return 1

    @staticmethod
    def get_next_reward_time() -> datetime.datetime:
        from src.service.date_service import get_next_run

        return get_next_run(Env.CRON_DAILY_REWARD.get())

    @staticmethod
    def get_remaining_time_to_next_reward() -> str:
        from src.service.date_service import get_remaining_duration

        return get_remaining_duration(DailyReward.get_next_reward_time())

    @staticmethod
    def get_last_streak_reward(user: User) -> "DailyReward":
        """
        Get the last streak reward for the user
        :param user: The user
        :return: The last streak reward
        """

        return (
            DailyReward.select()
            .where((DailyReward.user == user) & (DailyReward.date >= get_day_in_past(1)))
            .order_by(DailyReward.date.desc())
            .first()
        )

    @staticmethod
    def get_user_streak_count(user: User) -> int:
        """
        Get the user's streak count
        :param user: The user
        :return: The streak count
        """

        # Get the last reward that is from yesterday or later
        last_streak_reward = DailyReward.get_last_streak_reward(user)

        if last_streak_reward is None:
            return 1

        streak_count = last_streak_reward.streak_count

        # Is from yesterday, add one
        if is_same_day(last_streak_reward.date, get_day_in_past(1)):
            return streak_count + 1

        return last_streak_reward.streak_count

    @staticmethod
    def get_bonuses_for_user(user: User, base_amount: int) -> list[DailyRewardBonus]:
        """
        Get the bonus_list for the user
        :param user: The user
        :param base_amount: The base amount
        :return: The bonus_list
        """

        bonus_list: list[DailyRewardBonus] = []
        total_amount = base_amount

        for bonus_type in list(DailyRewardBonusType):
            multiplier = None
            match bonus_type:
                # Streak
                case DailyRewardBonusType.STREAK:
                    value = Env.DAILY_REWARD_BONUS_BASE_STREAK.get_int()
                    streak_count = min(
                        DailyReward.get_user_streak_count(user),
                        Env.DAILY_REWARD_BONUS_MAX_STREAK.get_int(),
                    )
                    multiplier = streak_count
                    value *= multiplier

                # Location
                case DailyRewardBonusType.LOCATION:
                    multiplier = user.location_level
                    value = Env.DAILY_REWARD_BONUS_BASE_LOCATION.get_int() * multiplier

                # Crew level
                case DailyRewardBonusType.CREW_LEVEL:
                    if not user.is_crew_member():
                        continue
                    multiplier = user.crew.level
                    value = Env.DAILY_REWARD_BONUS_BASE_CREW_LEVEL.get_int() * multiplier

                # Crew MVP
                case DailyRewardBonusType.CREW_MVP:
                    if not user.has_crew_mvp_bonus():
                        continue
                    value = Env.DAILY_REWARD_BONUS_CREW_MVP.get_int()

                case _:
                    raise ValueError(f"Invalid bonus type: {bonus_type}")

            amount = int(get_value_from_percentage(total_amount, value))
            total_amount += amount
            bonus_list.append(DailyRewardBonus(bonus_type, value, amount, multiplier))

        return bonus_list

    def get_days_to_next_prize(self) -> int:
        """
        Get the days to next prize
        :return: The days to next prize
        """

        prize_days = Env.DAILY_REWARD_STREAK_DAYS.get_int()
        last_streak_reward = self.get_last_streak_reward(self.user)

        # Not yet up to minimum streak days.
        # Ex. Day 3 with prize days 7, so 4 days remaining
        if last_streak_reward.streak_count < prize_days:
            return prize_days - last_streak_reward.streak_count

        # Ex. Day 9 with prize days 7, so 5 days remaining
        return prize_days - (last_streak_reward.streak_count % prize_days)

    def get_base_belly_prize_offer(self) -> int:
        """
        Get the base belly prize
        :return: The base belly prize offer
        """

        return self.total_amount

    @staticmethod
    def get_minimum_belly_prize() -> int:
        """
        Get the minimum belly prize
        :return: The minimum belly prize
        """

        return 1

    def get_maximum_belly_prize(self) -> int:
        """
        Get the maximum belly prize
        :return: The maximum belly prize
        """

        return self.total_amount * 2

    @staticmethod
    def get_reward_count_for_user(user: User) -> int:
        """
        Get the reward count for the user
        :param user: The user
        :return: The reward count for the user
        """

        return DailyReward.select().where(DailyReward.user == user).count()

    def should_award_prize(self) -> bool:
        """
        Should award prize
        :return: Should award prize
        """
        # First time ever claiming reward, award prize
        if DailyReward.get_reward_count_for_user(self.user) == 1:
            return True

        # User has reached streak
        if self.get_days_to_next_prize() != Env.DAILY_REWARD_STREAK_DAYS.get_int():
            return False

        # Award if it's the first claim of the day
        rewards_today = (
            DailyReward.select()
            .where((DailyReward.user == self.user) & (DailyReward.date >= get_day_in_past()))
            .count()
        )

        return rewards_today == 1


DailyReward.create_table()
