package discord4j.core.support;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import discord4j.common.util.Snowflake;
import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.*;
import discord4j.core.object.entity.channel.TextChannel;
import discord4j.core.object.reaction.ReactionEmoji;
import discord4j.discordjson.json.ApplicationInfoData;
import discord4j.rest.util.Image;
import org.slf4j.LoggerFactory;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.function.TupleUtils;
import reactor.netty.http.client.HttpClient;
import reactor.util.Logger;
import reactor.util.Loggers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

public class ExtraBotSupport {

    private static final Logger log = Loggers.getLogger(ExtraBotSupport.class);

    private final GatewayDiscordClient client;

    public static ExtraBotSupport create(GatewayDiscordClient client) {
        return new ExtraBotSupport(client);
    }

    ExtraBotSupport(GatewayDiscordClient client) {
        this.client = client;
    }

    public Mono<Void> eventHandlers() {
        return commandHandler(client);
    }

    public static Mono<Void> commandHandler(GatewayDiscordClient client) {
        Mono<Long> ownerId = client.rest().getApplicationInfo()
                .map(ApplicationInfoData::owner)
                .map(user -> Snowflake.asLong(user.id()))
                .cache();

        List<EventHandler> eventHandlers = new ArrayList<>();
        eventHandlers.add(new AddRole());
        eventHandlers.add(new BurstMessages());
        eventHandlers.add(new ChangeAvatar());
        eventHandlers.add(new LogLevelChange());
        eventHandlers.add(new Reactor());
        eventHandlers.add(new UserInfo());
        eventHandlers.add(new ReactionRemove());

        return client.on(MessageCreateEvent.class,
                event -> ownerId.filter(
                        owner -> {
                            Long author = event.getMessage().getAuthor()
                                    .map(User::getId)
                                    .map(Snowflake::asLong)
                                    .orElse(null);
                            return owner.equals(author);
                        })
                        .flatMap(id -> Mono.when(eventHandlers.stream()
                                .map(handler -> handler.onMessageCreate(event))
                                .collect(Collectors.toList()))
                        ))
                .then();
    }

    public static class AddRole extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            return Mono.justOrEmpty(message.getContent())
                    .filter(content -> content.startsWith("!addrole "))
                    .filterWhen(content -> message.getGuild().hasElement())
                    .flatMap(content -> {
                        // !addrole userId roleName
                        String[] tokens = content.split(" ", 3);
                        if (tokens.length > 2) {
                            String user = tokens[1];
                            String roleName = tokens[2];
                            Mono<Member> member = message.getGuild()
                                    .flatMap(g -> g.getMemberById(Snowflake.of(user)));
                            Mono<Role> role = message.getGuild()
                                    .flatMapMany(Guild::getRoles)
                                    .filter(r -> r.getName().equalsIgnoreCase(roleName))
                                    .next();
                            return member.zipWith(role)
                                    .flatMap(t2 -> t2.getT1().addRole(t2.getT2().getId()));
                        }
                        return Mono.empty();
                    })
                    .then();
        }
    }

    public static class UserInfo extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().startsWith("!user ")) {
                return Mono.justOrEmpty(message.getContent())
                        .map(content -> content.split(" ", 2))
                        .flatMap(tokens -> message.getClient().getUserById(Snowflake.of(tokens[1])))
                        .flatMap(user -> message.getChannel()
                                .flatMap(channel -> channel.createMessage(msg ->
                                        msg.setEmbed(embed -> embed
                                                .addField("Name", user.getUsername(), false)
                                                .addField("Avatar URL", user.getAvatarUrl(), false)
                                                .setImage(user.getAvatarUrl()))
                                )))
                        .switchIfEmpty(Mono.just("Not found")
                                .flatMap(reason -> message.getChannel()
                                        .flatMap(channel -> channel.createMessage(reason)))
                        )
                        .then();
            }
            return Mono.empty();
        }
    }

    public static class LogLevelChange extends EventHandler {

        private final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().startsWith("!log ")) {
                return Mono.justOrEmpty(message.getContent())
                        .map(content -> content.split(" ", 3))
                        .doOnNext(tokens -> {
                            String level = tokens[1];
                            String name = tokens[2];
                            Level logLevel = Level.valueOf(level);
                            context.getLoggerList().stream()
                                    .filter(logger -> logger.getName().startsWith(name))
                                    .forEach(logger -> {
                                        log.info("Changing {} to {}", logger, logLevel);
                                        logger.setLevel(logLevel);
                                    });
                        })
                        .then();
            }
            return Mono.empty();
        }
    }

    public static class Reactor extends EventHandler {

        private final Random random = new Random();
        private final List<String> emoji = new ArrayList<>(Arrays.asList(
                "😀", "😬", "😂", "😄", "😅", "😇", "☺", "😋", "😘", "😚", "😜", "🤑", "😎", "🤗", "😳", "🙄", "😤",
                "😱", "😨", "😰", "😥", "🤒", "😭", "💩", "👹", "💀", "👻", "👽", "🤖", "😺", "😹", "😻", "😼", "😽",
                "🙀", "😿", "😾", "🙌", "👏", "👋", "👍", "👊", "✊", "✌", "👌", "✋", "👐", "💪", "☝", "🙏", "👆",
                "🖐", "🤘", "🖖", "✍", "💅", "👄", "👅", "👂", "👁", "👀", "👶", "👦", "👧", "👨", "👩", "👱", "👴",
                "👵", "👲", "👳", "👷", "💂", "🕵", "🎅", "👼", "👸", "👰", "🚶", "🏃", "💃", "👯", "👫", "👬", "👭",
                "🙇", "💁", "🙅", "🙆", "🙋", "🙎", "🙍", "💇", "💆", "💑", "👨‍❤️‍👨", "💏", "👩‍❤️‍💋‍👩",
                "👨‍❤️‍💋‍👨", "👩‍👩‍👦", "👨‍👨‍👦", "👮", "👚", "👕", "👖", "👔", "👗", "👙", "👘", "💄", "💋",
                "🎩", "👟", "👞", "👢", "👡", "👠", "👣", "⛑", "🎓", "👑", "🎒", "👝", "👛", "👜", "💼", "🌂", "💍",
                "🕶", "👓", "🐯", "🦁", "🐮", "🐷", "🐽", "🐸", "🐙", "🐵", "🐦", "🐧", "🐔", "🐒", "🙉", "🙈", "🐣",
                "🐥", "🐺", "🐗", "🐴", "🦄", "🐝", "🐛", "🐢", "🦀", "🦂", "🕷", "🐜", "🐞", "🐌", "🐠", "🐟", "🐡",
                "🐬", "🐋", "🐊", "🐆", "🐘", "🐫", "🐪", "🐄", "🐂", "🐃", "🐏", "🐑", "🐀", "🐁", "🐓", "🦃", "🐉",
                "🐾", "🐿", "🐇", "🐈", "🐩", "🐕", "🐲", "🌵", "🎄", "🌲", "🌴", "🌱", "🌿", "🌾", "🍁", "🍂", "🍃",
                "🎋", "🎍", "🍀", "🌺", "🌻", "🌹", "🌷", "🌼", "🌸", "💐", "🍄", "🎃", "🐚", "🌎", "🌍", "🌏", "🌕",
                "🌖", "🌗", "🌘", "🌑", "🌒", "🌓", "🌔", "🌚", "🌝", "🌛", "🌜", "🌞", "⭐", "🌟", "💫", "✨", "🌥",
                "🌦", "🌧", "⛈", "⚡", "🔥", "❄", "🌨", "☔", "☂", "🌪", "💨", "☃", "⛄", "💧", "💦", "🌊", "🍏", "🍎",
                "🍐", "🍋", "🍌", "🍉", "🍇", "🌶", "🍅", "🍍", "🍑", "🍈", "🍓", "🌽", "🍠", "🍯", "🍞", "🍗", "🧀",
                "🍖", "🍤", "🌯", "🌮", "🍝", "🍕", "🌭", "🍟", "🍔", "🍳", "🍜", "🍲", "🍥", "🍣", "🍱", "🍛", "🍙",
                "🍚", "🎂", "🍰", "🍦", "🍨", "🍧", "🍡", "🍢", "🍘", "🍮", "🍬", "🍭", "🍫", "🍿", "🍩", "🍪", "🍺",
                "☕", "🍵", "🍶", "🍹", "🍻", "🍼", "🍴", "🍷", "🍽", "⚽", "🏀", "🏈", "⚾", "🎾", "🏐", "🏉", "🎱",
                "🎿", "🏏", "🏑", "🏓", "🏌", "⛳", "⛷", "🏂", "⛸", "🏹", "🎣", "🚣", "🏊🏼", "🏄", "🏆", "🕴", "🏇",
                "🚵", "🚴", "🏋", "⛹", "🛀", "🎽", "🏅", "🎖", "🎗", "🏵", "🎫", "🎟", "🎭", "🎺", "🎷", "🎹", "🎤",
                "🎪", "🎨", "🎸", "🎻", "🎬", "🎮", "👾", "🎯", "🎲", "🎰", "🎳", "🚗", "🚕", "🚙", "🚌", "🚎", "🏎",
                "🚓", "🚒", "🚐", "🚛", "🚜", "🏍", "🚲", "🚨", "🚃", "🚟", "🚠", "🚡", "🚖", "🚘", "🚍", "🚔", "🚋",
                "🚝", "🚄", "🚅", "🚈", "🚞", "🚂", "🚆", "🛬", "🛫", "✈", "🛩", "🚁", "🚉", "🚊", "🚇", "⛵", "🛥",
                "🚤", "⛴", "🚀", "🛳", "🛰", "💺", "🏁", "🚥", "🚦", "🚏", "⛽", "🚧", "⚓", "🎡", "🎢", "🎠", "🏗",
                "🌁", "🗼", "🏭", "⛲", "⛺", "🏕", "🗾", "🌋", "🗻", "🏔", "⛰", "🎑", "🏞", "🛣", "🛤", "🌅", "🌄",
                "🏜", "🏖", "🏝", "🎇", "🌠", "🌌", "🌉", "🌃", "🏙", "🌆", "🌇", "🎆", "🌈", "🏘", "🏰", "🏯", "🏠",
                "🗽", "🏟", "🏡", "🏚", "🏢", "🏬", "🏣", "🏤", "🏥", "🏦", "🕌", "🏛", "💒", "🏩", "🏫", "🏪", "🏨",
                "🕍", "🕋", "⛩", "🕹", "💽", "💾", "💿", "📼", "📷", "📹", "🎥", "☎", "⏱", "🎙", "📻", "📺", "📠",
                "📟", "⏲", "⏰", "🕰", "⏳", "📡", "🔋", "💴", "💵", "💸", "🛢", "🔦", "💡", "💶", "💷", "💰", "💳",
                "💎", "🔨", "💣", "🔫", "🔪", "☠", "🔮", "💈", "💊", "💉", "🔖", "🚿", "🔑", "🛋", "🚪", "🛎", "🖼",
                "🎁", "🎀", "🎏", "🎈", "🛍", "⛱", "🗺", "🎊", "🎉", "🎎", "🎐", "🎌", "🏮", "📮", "📫", "📯", "📊",
                "🗃", "📇", "📅", "📉", "📈", "📰", "📕", "📙", "📒", "✂", "🖇", "📖", "📚", "📌", "📍", "🚩", "❤",
                "💔", "❣", "💕", "💓", "💗", "💖", "💘", "💝", "💠", "🔔"));

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().startsWith("!react")) {
                String rawCount = message.getContent().substring("!react".length());
                int count = 1;
                if (!rawCount.isEmpty()) {
                    try {
                        count = Math.max(1, Math.min(20, Integer.parseInt(rawCount.trim())));
                    } catch (NumberFormatException e) {
                        throw Exceptions.propagate(e);
                    }
                }
                return Flux.fromIterable(fetch(count))
                        .flatMap(emoji -> message.addReaction(ReactionEmoji.unicode(emoji)))
                        .then();
            }
            return Mono.empty();
        }

        private List<String> fetch(int count) {
            List<String> reactions = new ArrayList<>();
            for (int i = 0; i < count; i++) {
                int index = random.nextInt(emoji.size());
                reactions.add(emoji.get(index));
                emoji.remove(index);
            }
            return reactions;
        }
    }

    public static class ChangeAvatar extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().equals("!avatar")) {
                for (Attachment attachment : message.getAttachments()) {
                    // This code is very optimistic as it does not check for status codes or file types
                    return HttpClient.create()
                            .get()
                            .uri(attachment.getUrl())
                            .responseSingle((res, mono) -> mono.asByteArray())
                            .flatMap(input -> message.getClient()
                                    .edit(spec -> spec.setAvatar(Image.ofRaw(input, Image.Format.PNG))))
                            .then();
                }
            }
            return Mono.empty();
        }
    }

    public static class BurstMessages extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            if (!event.getMessage().getContent().startsWith("!burstmessages")) {
                return Mono.empty();
            }
            return event.getGuild()
                    .flatMapMany(Guild::getChannels)
                    .ofType(TextChannel.class)
                    .filter(channel -> channel.getName().startsWith("test"))
                    .collectList()
                    .doOnNext(channelList -> Flux.fromIterable(channelList)
                            .flatMap(channel -> Flux.range(1, 5)
                                    .map(String::valueOf)
                                    .flatMap(channel::createMessage))
                            .collectList()
                            .elapsed()
                            .doOnNext(TupleUtils.consumer(
                                    (time, list) -> log.info("Sent {} messages in {} milliseconds ({} messages/s)",
                                            list.size(), time, (list.size() / (double) time) * 1000)))
                            .subscribe()) // Separate subscribe in order to improve accuracy of elapsed time
                    .then();
        }
    }

    public static class ReactionRemove extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().startsWith("!rr ")) {
                String[] tokens = message.getContent().split(" ");
                return event.getClient().getMessageById(Snowflake.of(tokens[1]), Snowflake.of(tokens[2]))
                        .flatMap(m -> m.removeReactions(ReactionEmoji.unicode("✅")));
            }
            return Mono.empty();
        }
    }
}