/*
 * This file is a part of the SteamWar software.
 *
 * Copyright (C) 2020  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.commands;

import com.google.gson.JsonParser;
import com.velocitypowered.api.proxy.Player;
import de.steamwar.velocitycore.VelocityCore;
import de.steamwar.velocitycore.listeners.IPSanitizer;
import de.steamwar.command.PreviousArguments;
import de.steamwar.command.SWCommand;
import de.steamwar.command.TypeMapper;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.sql.BannedUserIPs;
import de.steamwar.sql.Punishment;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm;

import java.io.IOException;
import java.net.URL;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class PunishmentCommand {

    private static final String API_URL = "https://api.mojang.com/users/profiles/minecraft/";

    public static SteamwarUser getOrCreateOfflinePlayer(String name) {
        SteamwarUser user = SteamwarUser.get(name);
        if (user != null) {
            return user;
        }

        UUID uuid = getUUIDofOfflinePlayer(name);
        if (uuid == null) {
            return null;
        }

        return SteamwarUser.getOrCreate(uuid, name, u -> {});
    }

    private static UUID getUUIDofOfflinePlayer(String playerName) {
        try {
            final URL url = new URL(API_URL + playerName);
            String uuid = JsonParser.parseString(new Scanner(url.openConnection().getInputStream()).nextLine()).getAsJsonObject().get("id").getAsString();
            return UUID.fromString(uuid.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"));
        } catch (NoSuchElementException e) {
            // ignore, player does not exist
        } catch (IOException e) {
            VelocityCore.getLogger().log(Level.SEVERE, "Could not get offline player UUID %s".formatted(playerName), e);
        }
        return null;
    }

    public static boolean isPunishedWithMessage(Chatter player, Punishment.PunishmentType punishment) {
        SteamwarUser user = player.user();
        if (!user.isPunished(punishment)) {
            return false;
        }
        player.system(punishmentMessage(user, punishment));
        return true;
    }

    public static void ban(SteamwarUser user, Timestamp time, String banReason, SteamwarUser punisher, boolean perma) {
        Player player = VelocityCore.getProxy().getPlayer(user.getUUID()).orElse(null);
        if (player != null) {
            String ip = IPSanitizer.getTrueAddress(player).getHostAddress();
            Chatter.disconnect(player).system(punishmentMessage(user, Punishment.PunishmentType.Ban));
            for (BannedUserIPs banned : BannedUserIPs.get(ip)) {
                SteamwarUser bannedUser = SteamwarUser.get(banned.getUserID());
                if (bannedUser.isPunished(Punishment.PunishmentType.Ban) && bannedUser.getPunishment(Punishment.PunishmentType.Ban).getEndTime().before(time)) {
                    bannedUser.punish(Punishment.PunishmentType.Ban, time, banReason, punisher.getId(), perma);
                }
            }
            BannedUserIPs.banIP(user.getId(), ip);
        }
    }

    public static Message punishmentMessage(SteamwarUser user, Punishment.PunishmentType punishment) {
        Punishment currentPunishment = user.getPunishment(punishment);
        if (currentPunishment.isPerma()) {
            return new Message(punishment.getPlayerMessagePerma(), currentPunishment.getReason());
        } else {
            return new Message(punishment.getPlayerMessageUntil(), currentPunishment.getEndTime(), currentPunishment.getReason());
        }
    }

    public PunishmentCommand(String command, Punishment.PunishmentType punishmentType) {
        new PunishCommand(command, punishmentType);

        if (punishmentType.getUnpunishmentMessage() == null)
            return;

        new UnpunishCommand("un" + command, punishmentType);
    }

    private static final Pattern RELATIVE_PATTERN = Pattern.compile("([1-9]\\d*[hdwmy])+");

    public static Timestamp parseTime(Chatter player, String arg) {
        if (arg.equalsIgnoreCase("perma")) {
            return Punishment.PERMA_TIME;
        } else {
            if (RELATIVE_PATTERN.matcher(arg).matches()) {
                Instant instant = Instant.now();
                StringBuilder st = new StringBuilder();
                for (int i = 0; i < arg.length(); i++) {
                    char c = arg.charAt(i);
                    if (c >= '0' && c <= '9') {
                        st.append(c);
                        continue;
                    }
                    int amount = Integer.parseInt(st.toString());
                    st = new StringBuilder();
                    switch (c) {
                        case 'h':
                            instant = instant.plus(amount, ChronoUnit.HOURS);
                            break;
                        case 'd':
                            instant = instant.plus(amount, ChronoUnit.DAYS);
                            break;
                        case 'w':
                            instant = instant.plus(Duration.ofSeconds(amount * ChronoUnit.WEEKS.getDuration().getSeconds()));
                            break;
                        case 'm':
                            instant = instant.plus(Duration.ofSeconds(amount * ChronoUnit.MONTHS.getDuration().getSeconds()));
                            break;
                        case 'y':
                            instant = instant.plus(Duration.ofSeconds(amount * ChronoUnit.YEARS.getDuration().getSeconds()));
                            break;
                    }
                }
                return Timestamp.from(instant);
            }
            SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy_HH:mm");
            try {
                Date parsedDate = dateFormat.parse(arg);
                return new java.sql.Timestamp(parsedDate.getTime());
            } catch (ParseException e) {
                dateFormat = new SimpleDateFormat("dd.MM.yyyy");
                try {
                    Date parsedDate = dateFormat.parse(arg.split("_")[0]);
                    return new java.sql.Timestamp(parsedDate.getTime());
                } catch (ParseException exception) {
                    player.system("INVALID_TIME");
                    return null;
                }
            }
        }
    }

    private static class PunishCommand extends SWCommand {

        private final String command;
        private final Punishment.PunishmentType punishmentType;

        private PunishCommand(String command, Punishment.PunishmentType punishmentType) {
            super(command, UserPerm.TEAM);
            this.command = command;
            this.punishmentType = punishmentType;
        }

        private SteamwarUser unsafeUser(Chatter sender, String arg) {
            SteamwarUser target = getOrCreateOfflinePlayer(arg);
            if(target == null)
                sender.system("UNKNOWN_PLAYER");
            return target;
        }

        @Register
        public void genericCommand(Chatter sender, @Mapper("toPunish") String toPunish, String date, @ErrorMessage(allowEAs = false, value = "PUNISHMENT_USAGE_REASON") String... message) {
            SteamwarUser punisher = sender.user();
            if (punishmentType.isNeedsAdmin() && !punisher.hasPerm(UserPerm.MODERATION))
                return;

            SteamwarUser target = unsafeUser(sender, toPunish);
            if (target == null)
                return;

            Timestamp banTime = parseTime(sender, date);
            if (banTime == null)
                return;

            boolean isPerma = date.equalsIgnoreCase("perma");
            String msg = String.join(" ", message);
            target.punish(punishmentType, banTime, msg, punisher.getId(), isPerma);
            if(punishmentType == Punishment.PunishmentType.Ban)
                ban(target, banTime, msg, punisher, isPerma);
            Chatter.serverteam().system(punishmentType.getTeamMessage(), new Message("PREFIX"), target, sender, new Message((isPerma ? "PUNISHMENT_PERMA" : "PUNISHMENT_UNTIL"), banTime), msg);
        }

        @Register
        public void genericError(Chatter sender, String... args) {
            sender.system("PUNISHMENT_USAGE", command);
        }

        @Mapper(value = "toPunish", local = true)
        public TypeMapper<String> allUsers() {
            return new TypeMapper<String>() {
                @Override
                public String map(Chatter sender, PreviousArguments previousArguments, String s) {
                    return s;
                }

                @Override
                public Collection<String> tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) {
                    List<String> players = VelocityCore.getProxy().getAllPlayers().stream()
                            .map(Player::getUsername)
                            .collect(Collectors.toList());
                    players.add(s);
                    return players;
                }
            };
        }
    }

    private static class UnpunishCommand extends SWCommand {

        private final String command;
        private final Punishment.PunishmentType punishmentType;

        private UnpunishCommand(String command, Punishment.PunishmentType punishmentType) {
            super(command, UserPerm.TEAM);
            this.command = command;
            this.punishmentType = punishmentType;
        }

        @Register
        public void genericCommand(Chatter sender, @ErrorMessage("UNKNOWN_PLAYER") SteamwarUser target) {
            if (punishmentType.isNeedsAdmin() && !sender.user().hasPerm(UserPerm.MODERATION))
                return;

            if (!target.isPunished(punishmentType)) {
                sender.system(punishmentType.getUsageNotPunished());
                return;
            }

            target.punish(punishmentType, Timestamp.from(new Date().toInstant()), command, sender.user().getId(), false);
            if(punishmentType == Punishment.PunishmentType.Ban)
                BannedUserIPs.unbanIPs(target.getId());

            sender.system(punishmentType.getUnpunishmentMessage(), target.getUserName());
        }

        @Register
        public void genericError(Chatter sender, String... args) {
            sender.system("UNPUNISHMENT_USAGE", command);
        }
    }
}
