package tc.oc.pgm.cycle;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.Server;
import tc.oc.api.games.GameStore;
import tc.oc.api.games.TicketService;
import tc.oc.api.message.types.CycleRequest;
import tc.oc.api.servers.ServerStore;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.format.GameFormatter;
import tc.oc.commons.bukkit.teleport.PlayerServerChanger;
import tc.oc.commons.core.chat.Audience;
import tc.oc.commons.core.chat.BlankComponent;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.commands.CommandBinder;
import tc.oc.commons.core.util.Comparables;
import tc.oc.pgm.countdowns.MatchCountdown;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchEndEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.map.PGMMap;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchExecutor;
import tc.oc.pgm.match.MatchManager;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Repeatable;
import tc.oc.pgm.match.inject.MatchModuleFixtureManifest;
import tc.oc.pgm.module.ModuleDescription;
import tc.oc.pgm.restart.RestartListener;
import tc.oc.time.Time;

@ModuleDescription(name = "Cycle")
@ListenerScope(MatchScope.LOADED)
public class CycleMatchModule extends MatchModule implements Listener {

    public static class Manifest extends MatchModuleFixtureManifest<CycleMatchModule> {
        @Override protected void configure() {
            super.configure();
            new CommandBinder(binder())
                .register(CycleCommands.class);
        }
    }

    @Inject private MatchManager mm;
    @Inject private RestartListener restartListener;
    @Inject private TicketService ticketService;
    @Inject private Server localServer;
    @Inject private PlayerServerChanger serverChanger;
    @Inject private CycleConfig config;
    @Inject private OnlinePlayers onlinePlayers;
    @Inject private GameStore games;
    @Inject private ServerStore servers;
    @Inject private Audiences audiences;
    @Inject private GameFormatter gameFormatter;
    @Inject private MatchExecutor matchExecutor;

    private boolean autoCycle = true;

    public CycleConfig getConfig() {
        return config;
    }

    public boolean isCycling() {
        return match.countdowns().getCountdown() instanceof CycleCountdown;
    }

    public void cycleNow() {
        cycleNow(null);
    }

    public void cycleNow(PGMMap map) {
        startCountdown(Duration.ZERO, map);
    }

    public void startCountdown(@Nullable Duration duration) {
        if(duration == null) duration = config.countdown();
        match.ensureNotRunning();
        if(Duration.ZERO.equals(duration)) {
            requestCycle(false);
        } else {
            getMatch().countdowns().start(new CycleCountdown(mm, getMatch()), duration);
        }
    }

    public void startCountdown(@Nullable Duration duration, PGMMap nextMap) {
        mm.setNextMap(nextMap);
        startCountdown(duration);
    }

    private void requestCycle(boolean retryRotation) {
        if(localServer.game_id() != null) {
            final PGMMap nextMap = mm.getNextMap();
            matchExecutor.callback(
                ticketService.requestCycle(
                    new CycleRequest() {
                        @Override public String server_id() {
                            return localServer._id();
                        }

                        @Override public String map_id() {
                            return nextMap.getDocument()._id();
                        }

                        @Override public int min_players() {
                            return nextMap.getContext().playerLimits().lowerEndpoint();
                        }

                        @Override public int max_players() {
                            return nextMap.getContext().playerLimits().upperEndpoint();
                        }
                    }
                ),
                (match0, reply) -> {
                    final List<ListenableFuture<?>> quitFutures = new ArrayList<>();

                    if(localServer.game_id() != null) {
                        for(Map.Entry<UUID, String> entry : reply.destinations().entrySet()) {
                            final String serverId = entry.getValue();
                            if(localServer._id().equals(serverId)) continue;

                            final Player player = onlinePlayers.find(entry.getKey());
                            if(player == null) continue;;

                            final Audience audience = audiences.get(player);
                            final Game game = games.byId(localServer.game_id());

                            if(serverId == null) {
                                quitFutures.add(serverChanger.sendPlayerToLobby(player, true));
                            } else {
                                audience.sendMessage(gameFormatter.rejoining(game));
                                quitFutures.add(serverChanger.sendPlayerToServer(player, servers.byId(serverId), true));
                            }
                        }
                    }

                    if(quitFutures.isEmpty()) {
                        doCycle(retryRotation);
                    } else {
                        // Cycle after all requeued players are gone
                        matchExecutor.callback(
                            Futures.allAsList(quitFutures),
                            (match1, list) -> doCycle(retryRotation)
                        );
                    }
                }
            );
        } else {
            doCycle(retryRotation);
        }
    }

    private void doCycle(boolean retryRotation) {
        mm.cycleToNext(getMatch(), retryRotation, false);
    }

    @EventHandler
    public void onPartyChange(PlayerPartyChangeEvent event) {
        if(match.isRunning() && match.getParticipatingPlayers().isEmpty()) {
            checkEmptyServerCycle();
        }
    }

    @EventHandler
    public void onMatchEnd(MatchEndEvent event) {
        checkMatchEndCycle();
    }

    @Repeatable(scope = MatchScope.LOADED, interval = @Time(seconds = 1))
    public void periodicCheck() {
        // Check auto-cycle conditions periodically, so we don't get stuck when something fails
        checkMatchEndCycle();
        checkEmptyServerCycle();
    }

    private boolean canAutoCycle() {
        return autoCycle &&
               !isCycling() &&
               !restartListener.willRestart(match);
    }

    private void checkMatchEndCycle() {
        if(canAutoCycle() && match.isFinished()) {
            final CycleConfig.Auto autoConfig = config.matchEnd();
            if(autoConfig.enabled()) {
                startCountdown(autoConfig.countdown());
            }
        }
    }

    private void checkEmptyServerCycle() {
        if(canAutoCycle() && match.isRunning() && match.getParticipatingPlayers().isEmpty()) {
            final CycleConfig.Auto autoConfig = config.matchEmpty();
            if(autoConfig.enabled()) {
                logger.info("Cycling due to empty match");
                startCountdown(autoConfig.countdown());
            }
        }
    }

    public class CycleCountdown extends MatchCountdown {
        protected final MatchManager mm;

        public CycleCountdown(MatchManager mm, Match match) {
            super(match);
            this.mm = mm;
        }

        @Override
        public BaseComponent barText(Player viewer) {
            PGMMap nextMap = mm.getNextMap();
            if(nextMap == null || !nextMap.isLoaded()) return BlankComponent.INSTANCE;
            BaseComponent mapName = new Component(nextMap.getInfo().name, ChatColor.AQUA);

            if(Comparables.greaterThan(remaining, Duration.ZERO)) {
                return new Component(new TranslatableComponent("countdown.cycle.message", mapName, secondsRemaining(ChatColor.DARK_RED)), ChatColor.DARK_AQUA);
            } else {
                return new Component(new TranslatableComponent("countdown.cycle.complete", mapName), ChatColor.DARK_AQUA);
            }
        }

        @Override
        public void onCancel(Duration remaining, Duration total, boolean manual) {
            super.onCancel(remaining, total, manual);
            if(manual && match.isFinished()) {
                CycleMatchModule.this.autoCycle = false;
            }
        }

        @Override
        public void onEnd(Duration total) {
            super.onEnd(total);
            requestCycle(true);
        }
    }
}
