package com.alttd.altitudeweb.services.discord;

import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.discord.AppealList;
import com.alttd.altitudeweb.database.discord.AppealListMapper;
import com.alttd.altitudeweb.database.discord.OutputChannel;
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
import com.alttd.altitudeweb.database.litebans.HistoryCountMapper;
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
import com.alttd.altitudeweb.database.litebans.HistoryType;
import com.alttd.altitudeweb.database.litebans.UserType;
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal;
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper;
import com.alttd.altitudeweb.setup.Connection;
import com.alttd.webinterface.appeals.AppealSender;
import com.alttd.webinterface.objects.MessageForEmbed;
import com.alttd.webinterface.send_message.DiscordSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class AppealDiscord {

    private static final String OUTPUT_TYPE = "APPEAL";

    public void sendAppealToDiscord(DiscordAppeal discordAppeal) {
        CompletableFuture<List<OutputChannel>> channelsFuture = getChannelListFuture();
        List<OutputChannel> channels = channelsFuture.join();
        if (channels.isEmpty()) {
            log.warn("Discord appeal: No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
            return;
        }

        String createdAt = formatInstant(discordAppeal.createdAt());
        List<DiscordSender.EmbedField> fields = new ArrayList<>();

        // Group: User
        fields.add(new DiscordSender.EmbedField(
                "User",
                """
                Discord Username: `%s`
                Discord id: %s
                MC UUID: %s
                Submitted: %s
                """.formatted(
                        safe(discordAppeal.discordUsername()),
                        discordAppeal.discordId(),
                        safe(String.valueOf(discordAppeal.uuid())),
                        createdAt
                             ),
                false
        ));

        Optional<Long> optionalAssignedTo = assignAppeal();
        if (optionalAssignedTo.isPresent()) {
            Long assignedTo = optionalAssignedTo.get();
            fields.add(new DiscordSender.EmbedField(
                    "Assigned to",
                    "Assigned to: <@" + assignedTo + ">",
                    true
            ));
            assignDiscordAppealTo(discordAppeal.id(), assignedTo);
        } else {
            fields.add(new DiscordSender.EmbedField(
                    "Assigned to",
                    "Assigned to: None (failed to assign)",
                    true
            ));
        }

        String description = safe(discordAppeal.reason());

        List<Long> channelIds = channels.stream()
                .map(OutputChannel::channel)
                .toList();

        // colorRgb = null (use default), timestamp = appeal.createdAt if available
        Instant timestamp = discordAppeal.createdAt() != null ? discordAppeal.createdAt() : Instant.now();
        MessageForEmbed newAppealSubmitted = new MessageForEmbed(
                "New Discord Appeal Submitted", description, fields, null, timestamp, null);
        AppealSender.getInstance().sendAppeal(channelIds, newAppealSubmitted, optionalAssignedTo.orElse(0L));
    }
    
    public void sendAppealToDiscord(Appeal appeal, HistoryRecord history) {
        // Fetch channels
        CompletableFuture<List<OutputChannel>> channelsFuture = getChannelListFuture();

        CompletableFuture<Integer> bansF = getCountAsync(HistoryType.BAN, appeal.uuid());
        CompletableFuture<Integer> mutesF = getCountAsync(HistoryType.MUTE, appeal.uuid());
        CompletableFuture<Integer> warnsF = getCountAsync(HistoryType.WARN, appeal.uuid());
        CompletableFuture<Integer> kicksF = getCountAsync(HistoryType.KICK, appeal.uuid());

        List<OutputChannel> channels = channelsFuture.join();
        int bans = bansF.join();
        int mutes = mutesF.join();
        int warns = warnsF.join();
        int kicks = kicksF.join();

        if (channels.isEmpty()) {
            log.warn("No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
            return;
        }

        // Build embed
        boolean active = history.getUntil() == null || history.getUntil() <= 0 || history.getUntil() > System.currentTimeMillis();
        String createdAt = formatInstant(appeal.createdAt());

        List<DiscordSender.EmbedField> fields = new ArrayList<>();
        // Group: User
        fields.add(new DiscordSender.EmbedField(
                "User",
                "Username: `" + safe(appeal.username()) + "`\n" +
                        "UUID: " + safe(String.valueOf(appeal.uuid())) + "\n" +
                        "Submitted: " + createdAt,
                false
        ));
        // Group: Punishment
        fields.add(new DiscordSender.EmbedField(
                "Punishment",
                "Type: " + safe(String.valueOf(appeal.historyType())) + "\n" +
                        "ID: " + safe(String.valueOf(appeal.historyId())) + "\n" +
                        "Reason: " + safe(history.getReason()) + "\n" +
                        "Active: " + active,
                false
        ));
        // Group: Previous punishments
        fields.add(new DiscordSender.EmbedField(
                "Previous punishments",
                "Bans: " + bans + "\n" +
                        "Mutes: " + mutes + "\n" +
                        "Warnings: " + warns + "\n" +
                        "Kicks: " + kicks,
                true
        ));
        Optional<Long> optionalAssignedTo = assignAppeal();
        if (optionalAssignedTo.isPresent()) {
            Long assignedTo = optionalAssignedTo.get();
            fields.add(new DiscordSender.EmbedField(
                    "Assigned to",
                    "Assigned to: <@" + assignedTo + ">",
                    true
            ));
            assignMinecraftAppealTo(appeal.id(), assignedTo);
        } else {
            fields.add(new DiscordSender.EmbedField(
                    "Assigned to",
                    "Assigned to: None (failed to assign)",
                    true
            ));
        }

        String description = safe(appeal.reason());

        List<Long> channelIds = channels.stream()
                .map(OutputChannel::channel)
                .toList();

        // colorRgb = null (use default), timestamp = appeal.createdAt if available
        Instant timestamp = appeal.createdAt() != null ? appeal.createdAt() : Instant.now();
        MessageForEmbed newAppealSubmitted = new MessageForEmbed(
                "New Appeal Submitted", description, fields, null, timestamp, null);
        AppealSender.getInstance().sendAppeal(channelIds, newAppealSubmitted, optionalAssignedTo.orElse(0L));
    }

    private static CompletableFuture<List<OutputChannel>> getChannelListFuture() {
        CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
        Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
            try {
                List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
                        .getChannelsWithOutputType(OUTPUT_TYPE);
                channelsFuture.complete(channels);
            } catch (Exception e) {
                log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
                channelsFuture.complete(new ArrayList<>());
            }
        });
        return channelsFuture;
    }

    private void assignMinecraftAppealTo(UUID appealId, Long assignedTo) {
        Connection.getConnection(Databases.DEFAULT).runQuery(sql -> {
            try {
                sql.getMapper(AppealMapper.class).assignAppeal(appealId, assignedTo);
            } catch (Exception e) {
                log.error("Failed to assign appeal to {}", assignedTo, e);
            }
        });
    }

    private void assignDiscordAppealTo(UUID appealId, Long assignedTo) {
        Connection.getConnection(Databases.DEFAULT).runQuery(sql -> {
            try {
                sql.getMapper(DiscordAppealMapper.class).assignDiscordAppeal(appealId, assignedTo);
            } catch (Exception e) {
                log.error("Failed to assign appeal to {}", assignedTo, e);
            }
        });
    }

    private CompletableFuture<Integer> getCountAsync(HistoryType type, java.util.UUID uuid) {
        CompletableFuture<Integer> future = new CompletableFuture<>();
        Connection.getConnection(Databases.LITE_BANS).runQuery(sql -> {
            try {
                Integer count = sql.getMapper(HistoryCountMapper.class)
                        .getUuidPunishmentCount(type, UserType.PLAYER, uuid);
                future.complete(count == null ? 0 : count);
            } catch (Exception e) {
                log.error("Failed to load punishment count for {} ({})", type, uuid, e);
                future.complete(0);
            }
        });
        return future;
    }

    private String safe(String s) {
        return s == null ? "unknown" : s;
    }

    private String formatInstant(Instant instant) {
        if (instant == null) return "unknown";
        return instant.atZone(ZoneId.of("UTC"))
                .format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"));
    }

    private Optional<Long> assignAppeal() {
        CompletableFuture<Long> assignToCompletableFuture = new CompletableFuture<>();
        Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
            try {
                AppealListMapper mapper = sql.getMapper(AppealListMapper.class);
                List<AppealList> appealList = mapper
                        .getAppealList();


                if (appealList.isEmpty()) {
                    log.warn("No appeal lists found. Skipping assignment.");
                    assignToCompletableFuture.complete(0L);
                    return;
                }
                Optional<AppealList> optionalAssignTo = appealList
                        .stream()
                        .filter(AppealList::next).findFirst();
                AppealList assignTo = optionalAssignTo.orElseGet(appealList::getFirst);
                assignToCompletableFuture.complete(assignTo.userId());

                try {
                    Optional<AppealList> optionalNextAppealList = appealList
                            .stream()
                            .filter(entry -> entry.userId() > assignTo.userId())
                            .min(Comparator.comparing(AppealList::userId));
                    AppealList nextAppealList = optionalNextAppealList.orElse(appealList.stream()
                                                                                      .min(Comparator.comparing(AppealList::userId))
                                                                                      .orElse(assignTo));
                    mapper.updateNext(assignTo.userId(), false);
                    mapper.updateNext(nextAppealList.userId(), true);
                } catch (Exception e) {
                    log.error("Failed to assign next appeal", e);
                }

            } catch (Exception e) {
                log.error("Failed to load appeal list", e);
                assignToCompletableFuture.complete(0L);
            }
        });
        Long assignTo = assignToCompletableFuture.join();
        if (assignTo.equals(0L)) {
            return Optional.empty();
        }
        return Optional.of(assignTo);
    }
}
