package com.github.ilpersi.BHBot;

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;

import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.function.Supplier;

public class DiscordManager {
    private final BHBot bot;

    DiscordManager(BHBot bot) {
        this.bot = bot;
    }

    synchronized void sendDiscordMessage(String content, File attachment) {
        if (bot.settings.enableDiscord) {

            if ("https://discordapp.com/api/webhooks/your_hook/your_token".equals(bot.settings.discordWebHookUrl)) {
                BHBot.logger.warn("Discord webhook not correctly configured! Disabling Discord notifications");
                bot.settings.enableDiscord = false;
                return;
            }

            if (!"".equals(bot.settings.username) && !"yourusername".equals(bot.settings.username)) {
                content = "[" + bot.settings.username + "]\n" + content;
            }

            DiscordWebHookMessage discordWebHookMessage = new DiscordWebHookMessage();
            discordWebHookMessage.userName = bot.settings.discordUserName;
            discordWebHookMessage.messageText = content;
            discordWebHookMessage.avatarURL = "https://i.imgur.com/FEJh2M7.png";

            Gson gson = new Gson();

            MultiPartBodyPublisher publisher = new MultiPartBodyPublisher()
                    .addPart("payload_json", gson.toJson(discordWebHookMessage));

            if (attachment != null) {
                publisher.addPart("file", () -> {
                    try {
                        return new FileInputStream(attachment);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                    return new ByteArrayInputStream("".getBytes());
                }, attachment.getName(), "image/png");
            }

            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(bot.settings.discordWebHookUrl))
                    .header("Content-Type", "multipart/form-data; boundary=" + publisher.getBoundary())
                    .timeout(Duration.ofMinutes(1))
                    .POST(publisher.build())
                    .build();

            HttpClient client = HttpClient.newHttpClient();
            HttpResponse<String> response;
            try {
                response = client.send(request, HttpResponse.BodyHandlers.ofString());

                if (response.statusCode() != 200) {
                    BHBot.logger.warn("Invalid HTTP Status when sending Discord notification!");
                }

            } catch (IOException | InterruptedException e) {
                BHBot.logger.error("Exception while getting latest version info from Git Hub", e);
            }
        }
    }
}

class DiscordWebHookMessage {

    @SerializedName(value = "username")
    String userName;

    @SerializedName(value = "content")
    String messageText;

    @SerializedName(value = "avatar_url")
    String avatarURL;

}

// https://stackoverflow.com/questions/46392160/java-9-httpclient-send-a-multipart-form-data-request
class MultiPartBodyPublisher {
    private final List<PartsSpecification> partsSpecificationList = new ArrayList<>();
    private final String boundary = UUID.randomUUID().toString();

    public HttpRequest.BodyPublisher build() {
        if (partsSpecificationList.size() == 0) {
            throw new IllegalStateException("Must have at least one part to build multipart message.");
        }
        addFinalBoundaryPart();
        return HttpRequest.BodyPublishers.ofByteArrays(PartsIterator::new);
    }

    String getBoundary() {
        return boundary;
    }

    MultiPartBodyPublisher addPart(@SuppressWarnings("SameParameterValue") String name, String value) {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.STRING;
        newPart.name = name;
        newPart.value = value;
        partsSpecificationList.add(newPart);
        return this;
    }

    /*MultiPartBodyPublisher addPart(String name, Path value) {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.FILE;
        newPart.name = name;
        newPart.path = value;
        partsSpecificationList.add(newPart);
        return this;
    }*/

    @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"})
    MultiPartBodyPublisher addPart(String name, Supplier<InputStream> value, String filename, String contentType) {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.STREAM;
        newPart.name = name;
        newPart.stream = value;
        newPart.filename = filename;
        newPart.contentType = contentType;
        partsSpecificationList.add(newPart);
        return this;
    }

    private void addFinalBoundaryPart() {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.FINAL_BOUNDARY;
        newPart.value = "--" + boundary + "--";
        partsSpecificationList.add(newPart);
    }

    static class PartsSpecification {

        public enum TYPE {
            STRING, FILE, STREAM, FINAL_BOUNDARY
        }

        PartsSpecification.TYPE type;
        String name;
        String value;
        Path path;
        Supplier<InputStream> stream;
        String filename;
        String contentType;

    }

    class PartsIterator implements Iterator<byte[]> {

        private final Iterator<PartsSpecification> iter;
        private InputStream currentFileInput;

        private boolean done;
        private byte[] next;

        PartsIterator() {
            iter = partsSpecificationList.iterator();
        }

        @Override
        public boolean hasNext() {
            if (done) return false;
            if (next != null) return true;
            try {
                next = computeNext();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
            if (next == null) {
                done = true;
                return false;
            }
            return true;
        }

        @Override
        public byte[] next() {
            if (!hasNext()) throw new NoSuchElementException();
            byte[] res = next;
            next = null;
            return res;
        }

        private byte[] computeNext() throws IOException {
            if (currentFileInput == null) {
                if (!iter.hasNext()) return null;
                PartsSpecification nextPart = iter.next();
                if (PartsSpecification.TYPE.STRING.equals(nextPart.type)) {
                    String part =
                            "--" + boundary + "\r\n" +
                                    "Content-Disposition: form-data; name=" + nextPart.name + "\r\n" +
                                    "Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
                                    nextPart.value + "\r\n";
                    return part.getBytes(StandardCharsets.UTF_8);
                }
                if (PartsSpecification.TYPE.FINAL_BOUNDARY.equals(nextPart.type)) {
                    return nextPart.value.getBytes(StandardCharsets.UTF_8);
                }
                String filename;
                String contentType;
                if (PartsSpecification.TYPE.FILE.equals(nextPart.type)) {
                    Path path = nextPart.path;
                    filename = path.getFileName().toString();
                    contentType = Files.probeContentType(path);
                    if (contentType == null) contentType = "application/octet-stream";
                    currentFileInput = Files.newInputStream(path);
                } else {
                    filename = nextPart.filename;
                    contentType = nextPart.contentType;
                    if (contentType == null) contentType = "application/octet-stream";
                    currentFileInput = nextPart.stream.get();
                }
                String partHeader =
                        "--" + boundary + "\r\n" +
                                "Content-Disposition: form-data; name=" + nextPart.name + "; filename=" + filename + "\r\n" +
                                "Content-Type: " + contentType + "\r\n\r\n";
                return partHeader.getBytes(StandardCharsets.UTF_8);
            } else {
                byte[] buf = new byte[8192];
                int r = currentFileInput.read(buf);
                if (r > 0) {
                    byte[] actualBytes = new byte[r];
                    System.arraycopy(buf, 0, actualBytes, 0, r);
                    return actualBytes;
                } else {
                    currentFileInput.close();
                    currentFileInput = null;
                    return "\r\n".getBytes(StandardCharsets.UTF_8);
                }
            }
        }
    }
}