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.GameStatus import GameStatus
from src.service.date_service import (
    get_elapsed_hours,
    get_remaining_duration,
    get_datetime_in_future_hours,
)


class Fight(BaseModel):
    """
    Fight class
    """

    challenger: User | ForeignKeyField = ForeignKeyField(
        User, backref="fight_challengers", on_delete="CASCADE", on_update="CASCADE"
    )
    opponent: User | ForeignKeyField = ForeignKeyField(
        User, backref="fight_opponents", on_delete="CASCADE", on_update="CASCADE"
    )
    win_probability: float | FloatField = FloatField()
    date: datetime.datetime | DateTimeField = DateTimeField(default=datetime.datetime.now)
    status: bool | SmallIntegerField = SmallIntegerField(default=GameStatus.IN_PROGRESS)
    group_chat: GroupChat | ForeignKeyField = ForeignKeyField(
        GroupChat,
        null=True,
        backref="fight_groups_chats",
        on_delete="RESTRICT",
        on_update="CASCADE",
    )
    message_id: int | IntegerField = IntegerField(null=True)
    belly: int | BigIntegerField = BigIntegerField(null=True)

    in_revenge_to_fight = ForeignKeyField(
        "self",
        null=True,
        backref="revenge_attack_fights",
        on_delete="RESTRICT",
        on_update="RESTRICT",
    )

    # Backref
    revenge_attack_fights = None

    class Meta:
        db_table = "fight"

    def get_win_probability(self, user: User) -> float:
        """
        Get the win probability given a user

        :param user: The user
        :return: The win probability
        """

        if user.id == self.challenger.id:
            return float(str(self.win_probability))
        else:
            return 100 - self.win_probability

    @staticmethod
    def get_total_win_or_loss(user: User, status: GameStatus) -> int:
        """
        Get the total amount of wins or losses a user has
        param user: The user
        param status: The status (won or lost)
        return: The total amount of wins or losses
        """

        return (
            Fight().select().where((Fight.challenger == user) & (Fight.status == status)).count()
            + Fight()
            .select()
            .where((Fight.opponent == user) & (Fight.status == status.get_opposite_status()))
            .count()
        )

    @staticmethod
    def get_total_belly_won_or_lost(user: User, status: GameStatus) -> int:
        """
        Get the total amount of belly a user has won or lost
        param user: The user
        param status: The status (won or lost)
        return: The total amount of belly
        """

        return (
            Fight.select(fn.SUM(Fight.belly)).where(
                (Fight.challenger == user) & (Fight.status == status)
            )
        ).scalar() + (
            Fight.select(fn.SUM(Fight.belly)).where(
                (Fight.opponent == user) & (Fight.status == status.get_opposite_status())
            )
        ).scalar()

    @staticmethod
    def get_max_won_or_lost(user: User, status: GameStatus) -> "Fight":
        """
        Get the fight with the max belly won or lost
        param user: The user
        param status: The status (won or lost)
        return: The fight
        """

        # Max fight as challenger
        max_fight_as_challenger: Fight = (
            Fight()
            .select()
            .where((Fight.challenger == user) & (Fight.status == status))
            .order_by(Fight.belly.desc())
            .first()
        )

        # Max fight as opponent
        max_fight_as_opponent: Fight = (
            Fight()
            .select()
            .where((Fight.opponent == user) & (Fight.status == status.get_opposite_status()))
            .order_by(Fight.belly.desc())
            .first()
        )

        # The max fight
        return max(max_fight_as_challenger, max_fight_as_opponent, key=lambda f: f.belly)

    @staticmethod
    def get_most_fought_user(user: User) -> (User, int):
        """
        Get the most fought user and the amount of fights
        param user: The user
        return: The most fought user and the amount of fights
        """

        most_fought_user_as_challenger = (
            Fight()
            .select(Fight.opponent, fn.COUNT(Fight.opponent).alias("count"))
            .where(Fight.challenger == user)
            .group_by(Fight.opponent)
            .order_by(SQL("count").desc())
            .first()
        )
        amount_as_challenger = most_fought_user_as_challenger.count

        most_fought_user_as_opponent = (
            Fight()
            .select(Fight.challenger, fn.COUNT(Fight.challenger).alias("count"))
            .where(Fight.opponent == user)
            .group_by(Fight.challenger)
            .order_by(SQL("count").desc())
            .first()
        )
        amount_as_opponent = most_fought_user_as_opponent.count

        if amount_as_opponent > amount_as_challenger:
            return most_fought_user_as_opponent.challenger, amount_as_opponent

        return most_fought_user_as_challenger.opponent, amount_as_challenger

    def get_opponent(self, user: User) -> User:
        """
        Get the other opponent
        param user: The user
        return: The opponent
        """

        # noinspection PyTypeChecker
        # Warns that the return type is ForeignKeyField
        return self.challenger if self.challenger != user else self.opponent

    def get_status(self) -> GameStatus:
        """
        Get the status of the game
        :return: The status of the game
        """

        return GameStatus(self.status)

    def get_revenge_fight(self) -> "Fight":
        """
        Get the revenge fight
        :return: The revenge fight
        """

        return Fight.get_or_none(Fight.in_revenge_to_fight == self)

    def can_revenge(self, user: User = None) -> bool:
        """
        Check if a fight can be revenged.
        User can revenge a fight if they were attacked less than x time ago and have not
        already revenged that fight
        :param user: The user
        :return: True if the user can revenge the fight, False otherwise
        """

        # User is challenger
        if self.challenger == user:
            return False

        # Attack was more than x hours ago
        if get_elapsed_hours(self.date) > Env.FIGHT_PLUNDER_REVENGE_DURATION_HOURS.get_int():
            return False

        # Fight was in response to attack
        if self.in_revenge_to_fight is not None:
            return False

        # This fight has already been revenged
        if self.get_revenge_fight() is not None:
            return False

        return True

    def get_revenge_remaining_duration(self) -> str:
        """
        Get the remaining duration for the revenge
        :return: The remaining duration
        """

        return get_remaining_duration(
            get_datetime_in_future_hours(
                Env.FIGHT_PLUNDER_REVENGE_DURATION_HOURS.get_int(), self.date
            )
        )


Fight.create_table()
