/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General 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 Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.velocitycore.network.handlers;

import com.velocitypowered.api.proxy.Player;
import de.steamwar.linkage.EventMode;
import de.steamwar.linkage.Linked;
import de.steamwar.messages.Chatter;
import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.common.FightEndsPacket;
import de.steamwar.sql.SchematicType;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserElo;
import de.steamwar.velocitycore.ArenaMode;
import de.steamwar.velocitycore.VelocityCore;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
import java.util.stream.Collectors;

@Linked
public class EloPlayerHandler extends PacketHandler {

    private static final int MEDIAN_ELO_GAIN = 40;
    private static final int MEDIAN_ELO_LOSE = 20;
    private static final long REMATCH_LIFETIME = (long) 60 * 60 * 1000;

    private final Map<String, LinkedList<Game>> gameModeGames = new HashMap<>();

    /**
     * {@link FightEndsPacket#getWin()} == 1 -> Blue won
     * {@link FightEndsPacket#getWin()} == 2 -> Red won
     */
    @Handler
    public void handle(FightEndsPacket fightEndsPacket) {
        SchematicType schematicType = SchematicType.fromDB(fightEndsPacket.getGameMode());
        ArenaMode arenaMode;
        if (schematicType == null) {
            arenaMode = ArenaMode.getByInternal(fightEndsPacket.getGameMode());
        } else {
            arenaMode = ArenaMode.getBySchemType(schematicType);
        }
        if (arenaMode == null) return;
        if (!arenaMode.isRanked()) return;

        if (EloSchemHandler.publicVsPrivate(fightEndsPacket))
            return;

        // Die nächsten Zeilen filtern ein Fight innerhalb eines Teams nicht gewertet wird, bzw auch wenn nur Teile beider Teams im
        // gleichen Team sind dieser ungewertet ist.
        Set<Integer> teamsIds = fightEndsPacket.getBluePlayers().stream().map(SteamwarUser::get).map(SteamwarUser::getTeam).collect(Collectors.toSet());
        for (int redPlayer : fightEndsPacket.getRedPlayers()) {
            int team = SteamwarUser.get(redPlayer).getTeam();
            if (team != 0 && teamsIds.contains(team)) {
                return;
            }
        }

        // Get sizes of both teams
        int blueTeamSize = fightEndsPacket.getBluePlayers().size();
        int redTeamSize = fightEndsPacket.getRedPlayers().size();

        // Calculate the favored team
        double bluePlayerFactor = 1 / (1 + Math.exp(0 - (fightEndsPacket.getWin() == 2 ? (double) redTeamSize / blueTeamSize : (double) blueTeamSize / redTeamSize) * 3 + 3)) * 2;
        double redPlayerFactor = 1 / (1 + Math.exp(0 - (fightEndsPacket.getWin() == 1 ? (double) blueTeamSize / redTeamSize : (double) redTeamSize / blueTeamSize) * 3 + 3)) * 2;

        // Calculate the time factor
        double timeFactor = getTimeFactor(fightEndsPacket.getDuration());

        // Get total elo of both teams
        int blueTeamElo = fightEndsPacket.getBluePlayers().stream().mapToInt(player -> UserElo.getEloOrDefault(player, fightEndsPacket.getGameMode())).sum();
        int redTeamElo = fightEndsPacket.getRedPlayers().stream().mapToInt(player -> UserElo.getEloOrDefault(player, fightEndsPacket.getGameMode())).sum();

        // Adaptive elo bonus
        int blueTeamEloBonus = 0;
        int redTeamEloBonus = 0;
        if (Math.abs(blueTeamElo / (double) fightEndsPacket.getBluePlayers().size() - redTeamElo / (double) fightEndsPacket.getRedPlayers().size()) > 400) {
            int outlivedDuration = Math.max(fightEndsPacket.getDuration() - 60, 0);
            if (fightEndsPacket.getWin() == 1 && blueTeamElo < redTeamElo) {
                blueTeamEloBonus = outlivedDuration / 20;
                redTeamEloBonus = -(blueTeamEloBonus / 2);
            } else if (fightEndsPacket.getWin() == 2 && redTeamElo < blueTeamElo) {
                redTeamEloBonus = outlivedDuration / 20;
                blueTeamEloBonus = -(redTeamEloBonus / 2);
            } else if (fightEndsPacket.getWin() == 0) {
                if (redTeamElo < blueTeamElo) {
                    blueTeamEloBonus = outlivedDuration / 20;
                    redTeamEloBonus = -(blueTeamEloBonus / 2);
                } else if (blueTeamElo < redTeamElo) {
                    redTeamEloBonus = outlivedDuration / 20;
                    blueTeamEloBonus = -(redTeamEloBonus / 2);
                }
            }
        }

        // Calculate the elo factor
        double blueEloFactor = ((fightEndsPacket.getWin() == 1 ? 1 : 0) - 1 / (1 + Math.pow(10, (redTeamElo - blueTeamElo) / 600.0))) * (1 / (1 + Math.exp(-Math.abs(blueTeamElo - redTeamElo) / 1200.0))) * 4;
        double redEloFactor = blueEloFactor * -1;

        // Calculate favoured team on draw
        if (fightEndsPacket.getWin() == 0) {
            if (bluePlayerFactor > 1) {
                blueEloFactor = Math.abs(blueEloFactor) * -1;
                redEloFactor = Math.abs(redEloFactor);
            } else if (bluePlayerFactor < 1) {
                blueEloFactor = Math.abs(blueEloFactor);
                redEloFactor = Math.abs(redEloFactor) * -1;
            } else {
                if (Math.abs(blueEloFactor) > 1) {
                    // Do nothing
                } else if (Math.abs(blueEloFactor) < 1) {
                    blueEloFactor = -blueEloFactor;
                    redEloFactor = -blueEloFactor;
                } else {
                    blueEloFactor = 0;
                    redEloFactor = 0;
                }
            }
        }

        // Calculate the rematch factor
        double rematchFactor = getRematchFactor(fightEndsPacket);

        // Calculate the win factor
        double blueWinFactor = (fightEndsPacket.getWin() == 1 ? 1 : 0.7);
        double redWinFactor = (fightEndsPacket.getWin() == 2 ? 1 : 0.7);

        // Calculate division factor
        double divisionFactor = 1D / Math.max(blueTeamSize, redTeamSize);

        double blueFactor = bluePlayerFactor * timeFactor * blueEloFactor * rematchFactor * blueWinFactor * divisionFactor;
        double redFactor = redPlayerFactor * timeFactor * redEloFactor * rematchFactor * redWinFactor * divisionFactor;

        // Calculate the elo gain for each player
        int blueEloGain = (int) Math.round((blueFactor < 0 ? MEDIAN_ELO_LOSE : MEDIAN_ELO_GAIN) * blueFactor) + blueTeamEloBonus;
        int redEloGain = (int) Math.round((redFactor < 0 ? MEDIAN_ELO_LOSE : MEDIAN_ELO_GAIN) * redFactor) + redTeamEloBonus;

        // BungeeCore.get().getLogger().info("Blue: " + fightEndsPacket.getBluePlayers() + " " + blueTeamSize + " " + bluePlayerFactor + " " + timeFactor + " " + blueEloFactor + " " + rematchFactor + " " + blueWinFactor + " " + divisionFactor + " " + blueEloGain);
        // BungeeCore.get().getLogger().info("Red: " + fightEndsPacket.getRedPlayers() + " " + redTeamSize + " " + redPlayerFactor + " " + timeFactor + " " + redEloFactor + " " + rematchFactor + " " + redWinFactor + " " + divisionFactor + " " + redEloGain);

        update(fightEndsPacket.getBluePlayers(), fightEndsPacket.getGameMode(), blueEloGain);
        update(fightEndsPacket.getRedPlayers(), fightEndsPacket.getGameMode(), redEloGain);
    }

    private void update(List<Integer> players, String gameMode, int eloGain) {
        for (int player : players) {
            // BungeeCore.get().getLogger().log(Level.INFO, "Player: " + player + " Elo: " + UserElo.getEloOrDefault(player, gameMode) + " Factor: " + factor);
            int playerElo = UserElo.getEloOrDefault(player, gameMode);
            playerElo += eloGain;
            if (playerElo < 0) playerElo = 0;

            int oldProgression = UserElo.getProgression(player, gameMode);
            UserElo.setElo(player, gameMode, playerElo);
            int newProgression = UserElo.getProgression(player, gameMode);

            animate(player(player), UserElo.toEmblem(oldProgression).trim(), UserElo.toEmblem(newProgression).trim(), (oldProgression < newProgression) ? "§a" : "§c", eloGain);
        }
    }

    private void animate(Player player, String oldEmblem, String newEmblem, String arrowColor, int eloGain) {
        if (player == null)
            return;

        IntFunction<String> getRankup = getEmblemTransition(oldEmblem, newEmblem, arrowColor);

        String color = ((eloGain > 0) ? "§a+" : (eloGain == 0 ? "§7" : "§c"));

        double eloStep = eloGain / 40.0;
        for (int i = 0; i < 40; i++) {
            Component eloGainComponent = Chatter.SERIALIZER.deserialize(color + (int) (eloStep * (i + 1)));
            int finalI = i;
            VelocityCore.schedule(() -> player.showTitle(Title.title(
                    Chatter.SERIALIZER.deserialize(getRankup.apply(finalI)),
                    eloGainComponent,
                    Title.Times.times(Duration.ofMillis(finalI == 0 ? 250 : 0), Duration.ofSeconds(2), Duration.ofMillis(finalI == 39 ? 250 : 0))
            ))).delay(i * 50L, TimeUnit.MILLISECONDS).schedule();
        }
    }

    private static IntFunction<String> getEmblemTransition(String oldEmblem, String newEmblem, String arrowColor) {
        String finalOldEmblem = (oldEmblem.isEmpty() ? "/" : oldEmblem).trim();
        String finalNewEmblem = (newEmblem.isEmpty() ? "/" : newEmblem).trim();

        return i -> {
            if (oldEmblem.equals(newEmblem)) return "§8" + finalOldEmblem;
            if (i < 8) return "§8" + finalOldEmblem;
            if (i < 16) return "§8" + finalOldEmblem + arrowColor + " >";
            if (i < 24) return "§8" + finalOldEmblem + arrowColor + " >>";
            if (i < 32) return "§8" + finalOldEmblem + arrowColor + " >>>";
            return "§8" + finalOldEmblem + arrowColor + " >>> §8" + finalNewEmblem;
        };
    }

    private Player player(int userId) {
        return VelocityCore.getProxy().getPlayer(SteamwarUser.get(userId).getUUID()).orElse(null);
    }

    private double getTimeFactor(int duration) {
        if (duration <= 10) {
            return 0.5;
        }
        if (duration <= 60) {
            return 0.8;
        }
        return 1.0;
    }

    private double getRematchFactor(FightEndsPacket fightEndsPacket) {
        gameModeGames.computeIfAbsent(fightEndsPacket.getGameMode(), s -> new LinkedList<>()).add(new Game(fightEndsPacket.getBluePlayers(), fightEndsPacket.getRedPlayers()));

        LinkedList<Game> games = gameModeGames.get(fightEndsPacket.getGameMode());
        while (!games.isEmpty()) {
            Game game = games.getFirst();
            if (game.livedMillis() > REMATCH_LIFETIME) {
                games.removeFirst();
            } else {
                break;
            }
        }

        long rematchCount = games.stream().filter(game -> game.isSame(fightEndsPacket.getBluePlayers(), fightEndsPacket.getRedPlayers())).count();
        return 1.0 / rematchCount;
    }

    @RequiredArgsConstructor
    private static class Game {
        private long time = System.currentTimeMillis();
        private final List<Integer> bluePlayers;
        private final List<Integer> redPlayers;

        public long livedMillis() {
            return System.currentTimeMillis() - time;
        }

        public boolean isSame(List<Integer> bluePlayers, List<Integer> redPlayers) {
            return bluePlayers.containsAll(this.bluePlayers) && redPlayers.containsAll(this.redPlayers);
        }
    }
}
