package eu.jonahbauer.wizard.client.cli.state;

import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.commands.GameCommand;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.messages.player.ContinueMessage;
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.common.util.Pair;
import lombok.Getter;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

@Getter
public final class Game extends BaseState {
    private final UUID self;
    private final UUID session;
    private final String secret;

    private final Map<UUID, String> players;
    private final Map<UUID, Integer> scores = new HashMap<>();

    private int round = -1;
    private final Map<UUID, Integer> predictions = new HashMap<>();
    private final Map<UUID, List<Card>> hands = new HashMap<>();
    private final Map<UUID, List<List<Card>>> tricks = new HashMap<>();

    private int trick = -1;
    private final List<Pair<UUID, Card>> stack = new ArrayList<>();

    private Card trumpCard;
    private Card.Suit trumpSuit;

    public Game(UUID self, UUID session, String secret, Map<UUID, String> players) {
        this.self = self;
        this.session = session;
        this.secret = secret;
        this.players = players;
    }

    @Override
    public Optional<ClientState> onMessage(Client client, ServerMessage message) {
        if (message instanceof GameMessage game) {
            var observerMessage = game.getObserverMessage();
            if (observerMessage instanceof StateMessage state) {
                switch (state.getState()) {
                    case "starting_round" -> {
                        client.printfln("Round %d is starting...", ++round);
                        predictions.clear();
                        tricks.clear();
                        trumpSuit = null;
                        trumpCard = null;
                        stack.clear();
                        trick = -1;
                    }
                    case "starting_trick" -> {
                        client.printfln("Trick %d is starting...", ++trick);
                        stack.clear();
                    }
                    case "finished" -> {
                        client.println("The game has finished.");
                        return returnToSession();
                    }
                    case "error" -> {
                        client.println("The game has finished with an error.");
                        return returnToSession();
                    }
                }
            } else if (observerMessage instanceof HandMessage hand) {
                hands.put(hand.getPlayer(), new ArrayList<>(hand.getHand()));
                if (hand.getPlayer().equals(self)) {
                    client.printfln("Your hand cards are: %s", hand.getHand());
                } else {
                    client.printfln("%s's hand cards are: %s", nameOf(hand.getPlayer()), hand.getHand());
                }
            } else if (observerMessage instanceof PredictionMessage prediction) {
                predictions.put(prediction.getPlayer(), prediction.getPrediction());
                if (prediction.getPlayer().equals(self)) {
                    client.printfln("You predicted: %d", prediction.getPrediction());
                } else {
                    client.printfln("%s predicted: %d", nameOf(prediction.getPlayer()), prediction.getPrediction());
                }
            } else if (observerMessage instanceof TrumpMessage trump) {
                trumpCard = trump.getCard();
                trumpSuit = trump.getSuit();
                if (trumpCard == null) {
                    client.println("There is no trump in this round.");
                } else {
                    client.printfln("The trump suit is %s (%s).", trumpSuit, trumpCard);
                }
            } else if (observerMessage instanceof TrickMessage trick) {
                this.stack.clear();
                this.tricks.computeIfAbsent(trick.getPlayer(), player -> new ArrayList<>())
                           .add(trick.getCards());
                client.printfln("This trick %s goes to %s.", trick.getCards(), nameOf(trick.getPlayer()));
            } else if (observerMessage instanceof CardMessage card) {
                this.stack.add(Pair.of(card.getPlayer(), card.getCard()));

                var hand = this.hands.get(card.getPlayer());
                if (hand != null) {
                    switch (card.getCard()) {
                        case CHANGELING_JESTER, CHANGELING_WIZARD -> hand.remove(Card.CHANGELING);
                        case JUGGLER_BLUE, JUGGLER_GREEN, JUGGLER_RED, JUGGLER_YELLOW -> hand.remove(Card.JUGGLER);
                        case CLOUD_BLUE, CLOUD_GREEN, CLOUD_RED, CLOUD_YELLOW -> hand.remove(Card.CLOUD);
                        default -> hand.remove(card.getCard());
                    }
                }

                if (card.getPlayer().equals(self)) {
                    client.printfln("You played %s.", card.getCard());
                } else {
                    client.printfln("%s played %s.", nameOf(card.getPlayer()), card.getCard());
                }
            } else if (observerMessage instanceof ScoreMessage score) {
                score.getPoints().forEach((player, points) -> scores.merge(player, points, Integer::sum));

                String[] col0 = new String[players.size()];
                String[] col1 = new String[players.size()];

                int i = 0;
                for (UUID player : players.keySet()) {
                    col0[i] = nameOf(player);
                    col1[i] = Objects.toString(scores.getOrDefault(player, 0));
                    i++;
                }

                client.print("The scores are as follows:", "", col0, col1);
            } else if (observerMessage instanceof UserInputMessage input) {
                if (input.getAction() == UserInputMessage.Action.SYNC) {
                    client.send(new InteractionMessage(new ContinueMessage()));
                } else if (self.equals(input.getPlayer())) {
                    client.printfln("It is your turn to %s. You have time until %s.", switch (input.getAction()) {
                        case CHANGE_PREDICTION -> "change your prediction";
                        case JUGGLE_CARD -> "juggle a card";
                        case PLAY_CARD -> "play a card";
                        case PICK_TRUMP -> "pick the trump suit";
                        case MAKE_PREDICTION -> "make a prediction";
                        default -> throw new AssertionError();
                    }, LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault()));
                } else {
                    client.printfln(
                            "Waiting for input %s from %s. (times out at %s)",
                            input.getAction(),
                            nameOf(input.getPlayer()),
                            LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()),
                                                    ZoneId.systemDefault()
                            )
                    );
                }
            } else if (observerMessage instanceof TimeoutMessage) {
                client.println("Timed out.");
            } else {
                throw new AssertionError("Unknown observer message " + observerMessage.getClass().getSimpleName() + "");
            }
            return Optional.empty();
        } else if (message instanceof NackMessage nack) {
            int code = nack.getCode();
            if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
                client.println("Error: " + nack.getMessage());
                client.ready();
                return Optional.empty();
            } else {
                return unexpectedMessage(client, message);
            }
        } else if (message instanceof AckMessage) {
            client.ready();
            return Optional.empty();
        } else {
            return unexpectedMessage(client, message);
        }
    }

    public String nameOf(UUID player) {
        if (player == null) {
            return "all players";
        } else {
            return players.get(player);
        }
    }

    @Override
    public Object getCommand() {
        return GameCommand.class;
    }

    private Optional<ClientState> returnToSession() {
        return Optional.of(new Session(
                self,
                session,
                secret,
                false,
                players.entrySet().stream()
                       .collect(Collectors.toMap(
                               Map.Entry::getKey,
                               entry -> new PlayerData(entry.getKey(), entry.getValue(), false)
                       ))
        ));
    }
}
