/**
 * Copyright (c) 2010-2020 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.action.telegram.internal;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HeaderElement;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.scriptengine.action.ActionDoc;
import org.openhab.core.scriptengine.action.ParamDoc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class provides static methods that can be used in automation rules for
 * sending Telegrams.
 *
 * @author Paolo Denti
 * @since 1.8.0
 *
 */
public class Telegram {

    private static final Logger logger = LoggerFactory.getLogger(Telegram.class);

    private static final String TELEGRAM_URL = "https://api.telegram.org/bot%s/sendMessage";
    private static final String TELEGRAM_PHOTO_URL = "https://api.telegram.org/bot%s/sendPhoto";
    private static final int HTTP_TIMEOUT = 2000;
    private static final int HTTP_PHOTO_TIMEOUT = 10000;
    private static final int HTTP_RETRIES = 3;

    private static Map<String, TelegramBot> groupTokens = new HashMap<String, TelegramBot>();

    public static void addToken(String group, String chatId, String token) {
        groupTokens.put(group, new TelegramBot(chatId, token));
    }

    public static void addToken(String group, String chatId, String token, String parseMode) {
        groupTokens.put(group, new TelegramBot(chatId, token, parseMode));
    }

    @ActionDoc(text = "Sends a Telegram via Telegram REST API - direct message")
    public static boolean sendTelegram(@ParamDoc(name = "group") String group,
            @ParamDoc(name = "message") String message) {

        TelegramBot bot = groupTokens.get(group);
        if (bot == null) {
            logger.warn("Bot '{}' not defined; action skipped.", group);
            return false;
        }

        String url = String.format(TELEGRAM_URL, bot.getToken());

        HttpClient client = new HttpClient();

        PostMethod postMethod = createPostMethod(url, HTTP_TIMEOUT, HTTP_RETRIES);
        NameValuePair[] data = {
                new NameValuePair("chat_id", bot.getChatId()),
                new NameValuePair("text", message),
                new NameValuePair("parse_mode", bot.getParseMode())
        };
        postMethod.setRequestBody(data);

        try {
            int statusCode = client.executeMethod(postMethod);

            if (statusCode == HttpStatus.SC_NO_CONTENT || statusCode == HttpStatus.SC_ACCEPTED) {
                return true;
            }

            if (statusCode != HttpStatus.SC_OK) {
                logger.warn("Method failed: {}", postMethod.getStatusLine());
                return false;
            }

            InputStream tmpResponseStream = postMethod.getResponseBodyAsStream();
            Header encodingHeader = postMethod.getResponseHeader("Content-Encoding");
            if (encodingHeader != null) {
                for (HeaderElement ehElem : encodingHeader.getElements()) {
                    if (ehElem.toString().matches(".*gzip.*")) {
                        tmpResponseStream = new GZIPInputStream(tmpResponseStream);
                        logger.debug("GZipped InputStream from {}", url);
                    } else if (ehElem.toString().matches(".*deflate.*")) {
                        tmpResponseStream = new InflaterInputStream(tmpResponseStream);
                        logger.debug("Deflated InputStream from {}", url);
                    }
                }
            }

            String responseBody = IOUtils.toString(tmpResponseStream);
            if (!responseBody.isEmpty()) {
                logger.debug("Response body: {}", responseBody);
            }

            return true;
        } catch (HttpException e) {
            logger.warn("HTTP protocol violation: {}", e);
            return false;
        } catch (IOException e) {
            logger.warn("Transport error: {}", e);
            return false;
        } finally {
            postMethod.releaseConnection();
        }
    }

    private static PostMethod createPostMethod(String url, int timeOut, int retries) {
        PostMethod postMethod = new PostMethod(url);
        postMethod.getParams().setContentCharset("UTF-8");
        postMethod.getParams().setSoTimeout(timeOut);
        postMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(retries, false));
        return postMethod;
    }

    @ActionDoc(text = "Sends a Telegram via Telegram REST API - build message with format and args")
    public static boolean sendTelegram(@ParamDoc(name = "group") String group, @ParamDoc(name = "format") String format,
            @ParamDoc(name = "args") Object... args) {

        return sendTelegram(group, String.format(format, args));
    }

    @ActionDoc(text = "Sends a Picture via Telegram REST API")
    public static boolean sendTelegramPhoto(@ParamDoc(name = "group") String group,
            @ParamDoc(name = "photoURL") String photoURL, @ParamDoc(name = "caption") String caption) {

        return sendTelegramPhoto(group, photoURL, caption, null, null, HTTP_PHOTO_TIMEOUT, HTTP_RETRIES);
    }

    @ActionDoc(text = "Sends a Picture via Telegram REST API, using custom HTTP timeout")
    public static boolean sendTelegramPhoto(@ParamDoc(name = "group") String group,
            @ParamDoc(name = "photoURL") String photoURL, @ParamDoc(name = "caption") String caption,
            @ParamDoc(name = "timeoutMillis") Integer timeoutMillis) {

        return sendTelegramPhoto(group, photoURL, caption, null, null, timeoutMillis, HTTP_RETRIES);
    }

    @ActionDoc(text = "Sends a Picture, protected by username/password authentication, via Telegram REST API")
    public static boolean sendTelegramPhoto(@ParamDoc(name = "group") String group,
            @ParamDoc(name = "photoURL") String photoURL, @ParamDoc(name = "caption") String caption,
            @ParamDoc(name = "username") String username, @ParamDoc(name = "password") String password) {
        return sendTelegramPhoto(group, photoURL, caption, username, password, HTTP_PHOTO_TIMEOUT, HTTP_RETRIES);

    }

    @ActionDoc(text = "Sends a Picture, protected by username/password authentication, using custom HTTP timeout and retries, via Telegram REST API")
    public static boolean sendTelegramPhoto(@ParamDoc(name = "group") String group,
            @ParamDoc(name = "photoURL") String photoURL, @ParamDoc(name = "caption") String caption,
            @ParamDoc(name = "username") String username, @ParamDoc(name = "password") String password,
            @ParamDoc(name = "timeoutMillis") int timeoutMillis, @ParamDoc(name = "retries") int retries) {

        TelegramBot bot = groupTokens.get(group);
        if (bot == null) {
            logger.warn("Bot '{}' not defined; action skipped.", group);
            return false;
        }

        if (photoURL == null) {
            logger.warn("Photo URL not defined; unable to retrieve photo for sending.");
            return false;
        }

        byte[] image;

        if (photoURL.toLowerCase().startsWith("http")) {
            // load image from url
            logger.debug("Photo URL provided.");

            HttpClient getClient = new HttpClient();

            if (username != null && password != null) {
                getClient.getParams().setAuthenticationPreemptive(true);
                Credentials defaultcreds = new UsernamePasswordCredentials(username, password);
                getClient.getState().setCredentials(AuthScope.ANY, defaultcreds);
            }

            GetMethod getMethod = new GetMethod(photoURL);
            getMethod.getParams().setSoTimeout(timeoutMillis);
            getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                    new DefaultHttpMethodRetryHandler(retries, false));
            try {
                int statusCode = getClient.executeMethod(getMethod);
                if (statusCode != HttpStatus.SC_OK) {
                    logger.warn("Failed to retrieve an image. Received status: {}", getMethod.getStatusLine());
                    return false;
                }

                // if the content-length is 0 (which shouldn't happen),
                // flag an appropriate error
                if (getMethod.getResponseContentLength() == 0) {
                    logger.warn("Failed to retrieve an image. Fetched URL returned no data.");
                    return false;
                }

                image = getMethod.getResponseBody();
            } catch (HttpException e) {
                logger.warn("HTTP protocol violation: {}", e);
                return false;
            } catch (IOException e) {
                logger.warn("Transport error: {}", e);
                return false;
            } finally {
                getMethod.releaseConnection();
            }
        } else if (photoURL.toLowerCase().startsWith("file")) {
            // Load image from local file system
            logger.debug("Read file from local file system: {}", photoURL);
            URL url;
            try {
                url = new URL(photoURL);
                image = Files.readAllBytes(Paths.get(url.getPath()));
            } catch (MalformedURLException e) {
                logger.warn("File specification {} is not properly formed: {}", photoURL, e.getMessage());
                return false;
            } catch (IOException e) {
                logger.warn("Unable to read file {} from local file system: {}", photoURL, e.getMessage());
                return false;
            }
        } else {
            // Load image from provided base64 image
            logger.debug("Photo base64 provided; converting to binary.");

            if (photoURL.split(",").length > 1) {
                String base64Image = photoURL.split(",")[1];

                try {
                    image = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image);
                } catch (Exception e) {
                    logger.warn("Failed to convert base64 image to binary: {}", e);
                    return false;
                }
            } else {
                logger.warn("Invalid base64 image provided.");
                return false;
            }
        }

        // parse image type
        String imageType;
        try {
            ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(image));
            logger.debug("imageInputStream length: {}", iis.length());
            Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(iis);
            if (!imageReaders.hasNext()) {
                logger.warn("Fetched photo URL did not contain a known image type.");
                byte[] bytes = new byte[24];
                iis.read(bytes);
                logger.debug("first 24 bytes of data: {}", Arrays.toString(bytes));
                return false;
            }
            ImageReader reader = imageReaders.next();
            imageType = reader.getFormatName();
        } catch (IOException e) {
            logger.warn("Cannot parse data fetched from photo URL as an image. Error: {}", e.getMessage());
            return false;
        }

        // post photo to telegram
        String url = String.format(TELEGRAM_PHOTO_URL, bot.getToken());

        PostMethod postMethod = createPostMethod(url, timeoutMillis, retries);
        try {
            Part[] parts = createSendPhotoRequestParts(bot, image, imageType, caption);
            postMethod.setRequestEntity(new MultipartRequestEntity(parts, postMethod.getParams()));

            HttpClient client = new HttpClient();
            int statusCode = client.executeMethod(postMethod);

            if (statusCode == HttpStatus.SC_NO_CONTENT || statusCode == HttpStatus.SC_ACCEPTED) {
                return true;
            }

            if (statusCode != HttpStatus.SC_OK) {
                logger.warn("Failed to send photo. Received status: {}", postMethod.getStatusLine());
                return false;
            }

            return true;
        } catch (HttpException e) {
            logger.warn("HTTP protocol violation: {}", e);
            return false;
        } catch (IOException e) {
            logger.warn("Transport error: {}", e);
            return false;
        } finally {
            postMethod.releaseConnection();
        }
    }

    private static Part[] createSendPhotoRequestParts(TelegramBot bot, byte[] image, String imageType, String caption) {
        List<Part> partList = new ArrayList<>();
        partList.add(new StringPart("chat_id", bot.getChatId()));
        partList.add(new FilePart("photo", new ByteArrayPartSource(String.format("image.%s", imageType), image)));

        if (StringUtils.isNotBlank(caption)) {
            partList.add(new StringPart("caption", caption, "UTF-8"));
        }

        if (StringUtils.isNotBlank(bot.getParseMode())) {
            partList.add(new StringPart("parse_mode", bot.getParseMode()));
        }
        return partList.toArray(new Part[0]);
    }
}