package jenkinsci.plugins.telegrambot.telegram;

import com.fasterxml.jackson.databind.ObjectMapper;
import hudson.FilePath;
import hudson.ProxyConfiguration;
import hudson.model.Run;
import hudson.model.TaskListener;
import jenkins.model.GlobalConfiguration;
import jenkinsci.plugins.telegrambot.TelegramBotGlobalConfiguration;
import jenkinsci.plugins.telegrambot.telegram.commands.*;
import jenkinsci.plugins.telegrambot.users.Subscribers;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.apache.http.util.EntityUtils;
import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException;
import org.jenkinsci.plugins.tokenmacro.TokenMacro;
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.Chat;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException;

import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.telegram.telegrambots.Constants.SOCKET_TIMEOUT;

public class TelegramBot extends TelegramLongPollingCommandBot {
    private static final Logger LOG = Logger.getLogger(TelegramBot.class.getName());

    private static final TelegramBotGlobalConfiguration CONFIG = GlobalConfiguration.all().get(TelegramBotGlobalConfiguration.class);
    private static final Subscribers SUBSCRIBERS = Subscribers.getInstance();

    private final ObjectMapper objectMapper = new ObjectMapper();
    private final String token;
    private volatile CloseableHttpClient httpclient;
    private volatile RequestConfig requestConfig;


    public TelegramBot(String token, String name) {
        super(name);
        this.token = token;

        initializeProxy();

        Arrays.asList(
                new StartCommand(),
                new HelpCommand(),
                new SubCommand(),
                new UnsubCommand(),
                new StatusCommand()
        ).forEach(this::register);
    }

    public void sendMessage(Long chatId, String message) {
        final SendMessage sendMessageRequest = new SendMessage();

        sendMessageRequest.setChatId(chatId.toString());
        sendMessageRequest.setText(message);
        sendMessageRequest.enableMarkdown(true);

        try {
            execute(sendMessageRequest);
        } catch (TelegramApiException e) {
            LOG.log(Level.SEVERE, String.format(
                    "TelegramBot: Error while sending message: %s%n%s", chatId, message), e);
        }
    }

    private static String expandMessage(String message, Run<?, ?> run, FilePath filePath, TaskListener taskListener)
            throws IOException, InterruptedException {

        try {
            return TokenMacro.expandAll(run, filePath, taskListener, message);
        } catch (MacroEvaluationException e) {
            LOG.log(Level.SEVERE, "Error while expanding the message", e);
        }

        return message;
    }

    public void sendMessage(
            Long chatId, String message, Run<?, ?> run, FilePath filePath, TaskListener taskListener)
            throws IOException, InterruptedException {

        final String expandedMessage = expandMessage(message, run, filePath, taskListener);

        try {
            if (chatId == null) {
                SUBSCRIBERS.getApprovedUsers()
                        .forEach(user -> this.sendMessage(user.getId(), expandedMessage));
            } else {
                sendMessage(chatId, expandedMessage);
            }

        } catch (Exception e) {
            LOG.log(Level.SEVERE, "Error while sending the message", e);
        }

        if (CONFIG.isShouldLogToConsole()) taskListener.getLogger().println(expandedMessage);
    }

    public void sendMessage(
            String message, Run<?, ?> run, FilePath filePath, TaskListener taskListener)
            throws IOException, InterruptedException {

        sendMessage(null, message, run, filePath, taskListener);
    }

    @Override
    public void processNonCommandUpdate(Update update) {
        if (update == null) {
            LOG.log(Level.WARNING, "Update is null");
            return;
        }

        final String nonCommandMessage = CONFIG.getBotStrings()
                .get("message.noncommand");

        final Message message = update.getMessage();
        final Chat chat = message.getChat();

        if (chat.isUserChat()) {
            sendMessage(chat.getId(), nonCommandMessage);
            return;
        }

        final String text = message.getText();

        try {
            // Skip not direct messages in chats
            if (text.length() < 1 || text.charAt(0) != '@') return;
            final String[] tmp = text.split(" ");
            if (tmp.length < 2 || !CONFIG.getBotName().equals(tmp[0].substring(1, tmp[0].length()))) return;
        } catch (Exception e) {
            LOG.log(Level.SEVERE, "Something bad happened while message processing", e);
            return;
        }

        sendMessage(chat.getId(), nonCommandMessage);
    }

    @Override
    public String getBotToken() {
        return token;
    }

    @Override
    public <T extends Serializable, Method extends BotApiMethod<T>> T execute(Method method)
            throws TelegramApiException {
        if (method == null) throw new TelegramApiException("Parameter method can not be null");
        return sendApiMethodWithProxy(method);
    }

    private HttpPost configuredHttpPost(String url) {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(requestConfig);
        return httpPost;
    }

    @Override
    public String toString() {
        return "TelegramBot{" + token + "}";
    }

    private void initializeProxy() {
        try {
            HttpHost proxy = getProxy();
            httpclient = getHttpClient(proxy);
            requestConfig = getRequestConfig(proxy);
            getOptions().setRequestConfig(requestConfig);
            LOG.log(Level.INFO, "Proxy successfully initialized");
        } catch (IOException e) {
            LOG.log(Level.SEVERE, "TelegramBot: Failed to set proxy", e);
        }
    }

    private HttpHost getProxy() throws IOException {
        ProxyConfiguration proxyConfig = ProxyConfiguration.load();
        if (proxyConfig != null) {
            LOG.log(Level.FINE, String.format("Proxy settings: %s:%d", proxyConfig.name, proxyConfig.port));
            return new HttpHost(proxyConfig.name, proxyConfig.port);
        } else {
            LOG.log(Level.FINE, "No proxy settings in Jenkins");
            return null;
        }
    }

    private CloseableHttpClient getHttpClient(HttpHost proxy) {
        HttpClientBuilder builder = HttpClientBuilder.create()
                .setSSLHostnameVerifier(new NoopHostnameVerifier())
                .setConnectionTimeToLive(70, TimeUnit.SECONDS)
                .setMaxConnTotal(100);
        if (proxy != null) {
            builder.setProxy(proxy)
                    .setRoutePlanner(new DefaultProxyRoutePlanner(proxy));
        }
        return builder.build();
    }

    private RequestConfig getRequestConfig(HttpHost proxy) {
        RequestConfig botRequestConfig = getOptions().getRequestConfig();
        if (botRequestConfig == null) {
            botRequestConfig = RequestConfig.custom()
                    .setSocketTimeout(SOCKET_TIMEOUT)
                    .setConnectTimeout(SOCKET_TIMEOUT)
                    .setConnectionRequestTimeout(SOCKET_TIMEOUT)
                    .build();
        }
        RequestConfig.Builder builder = RequestConfig.copy(botRequestConfig);
        if (proxy != null) {
            builder.setProxy(proxy);
        }
        return builder.build();
    }

    private <T extends Serializable, Method extends BotApiMethod<T>> T sendApiMethodWithProxy(
            Method method) throws TelegramApiException {
        try {
            String responseContent = sendMethodRequest(method);
            return method.deserializeResponse(responseContent);
        } catch (IOException e) {
            throw new TelegramApiException(
                    String.format("Unable to execute %s method", method.getMethod()), e);
        }
    }

    private <T extends Serializable, Method extends BotApiMethod<T>> String sendMethodRequest(
            Method method) throws TelegramApiValidationException, IOException {
        method.validate();
        String url = getBaseUrl() + method.getMethod();
        HttpPost httpPost = configuredHttpPost(url);
        httpPost.addHeader("charset", StandardCharsets.UTF_8.name());
        httpPost.setEntity(new StringEntity(
                objectMapper.writeValueAsString(method), ContentType.APPLICATION_JSON));
        return sendHttpPostRequest(httpPost);
    }

    private String sendHttpPostRequest(HttpPost httpPost) throws IOException {
        try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
            HttpEntity httpEntity = response.getEntity();
            BufferedHttpEntity bufferedHttpEntity = new BufferedHttpEntity(httpEntity);
            return EntityUtils.toString(bufferedHttpEntity, StandardCharsets.UTF_8);
        }
    }
}