package main;

import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import elements.Chat;
import elements.Persistence;
import elements.exceptions.*;
import elements.messages.ChannelMessage;
import elements.messages.GroupMessage;
import elements.messages.Message;
import elements.messages.PrivateMessage;
import org.json.JSONArray;
import org.json.JSONObject;

import java.time.LocalDate;
import java.util.*;

/**
 * Created by Guillermo Serrahima on 4/1/16.
 */

public class Geiserbot {
    private static final String TOKEN = "267229954:AAHx49MXLmT1nT0QkccrSIzgmRVCQbjbJaQ";
    private static final String USERNAME = "gserbot";
    private static final String BASE_URL = "https://api.telegram.org/bot" + TOKEN;
    private static final String PARSE_MARKDOWN = "Markdown";
    private static final String PARSE_HTML = "HTML";

    private static final int WARNING_COUNT_START = 0;
    private static final int START_MESSAGE = WARNING_COUNT_START;
    private static final int HELP_MESSAGE = START_MESSAGE + 1;
    private static final int WARNING_UNKNOWN_ERROR = HELP_MESSAGE + 1;
    private static final int WARNING_NO_USERNAME = WARNING_UNKNOWN_ERROR + 1;
    private static final int WARNING_NOT_IMPLEMENTED = WARNING_NO_USERNAME + 1;
    private static final int WARNING_NO_GROUP = WARNING_NOT_IMPLEMENTED + 1;
    private static final int WARNING_COUNT_END = WARNING_NO_GROUP + 1;

    //private List<String> commands;
    private Map<Long, Chat> chats;

    public Geiserbot() throws ReadErrorListException, ReadErrorChatException {
        chats = Persistence.getInstance().readChats();
    }

    public String getUsername() {
        return USERNAME;
    }

    public String getToken() {
        return TOKEN;
    }

    public String myName() {
        return "R. Geiser Pont";
    }

    //--- Http handling

    public HttpResponse<JsonNode> sendMessage(Long chatId, String text) throws UnirestException {
        System.out.println("Sending message: \n" + text + "\n");

        return Unirest.post(BASE_URL + "/sendMessage")
                .field("chat_id", chatId)
                .field("text", text)
                .asJson();
    }

    public HttpResponse<JsonNode> sendMessage(Long chatId, String text, String parseMode) throws UnirestException {
        System.out.println("Sending message: \n" + text + "\n");

        return Unirest.post(BASE_URL + "/sendMessage")
                .field("chat_id", chatId)
                .field("text", text)
                .field("parse_mode", parseMode)
                .asJson();
    }

    public HttpResponse<JsonNode> sendMessage(String channel, String text) throws UnirestException {
        System.out.println("Sending message to " + channel + ": \n" + text + "\n");

        return Unirest.post(BASE_URL + "/sendMessage")
                .field("chat_id", channel)
                .field("text", text)
                .asJson();
    }

    public HttpResponse<JsonNode> sendMessage(String channel, String text, String parseMode) throws UnirestException {
        System.out.println("Sending message to " + channel + ": \n" + text + "\n");

        return Unirest.post(BASE_URL + "/sendMessage")
                .field("chat_id", channel)
                .field("text", text)
                .field("parse_mode", parseMode)
                .asJson();
    }

    public HttpResponse<JsonNode> replyMessage(Long chatId, Integer repliedMessage, String text) throws UnirestException {
        System.out.println("Sending message: \n" + text + "\n");

        return Unirest.post(BASE_URL + "/sendMessage")
                .field("chat_id", chatId)
                .field("text", text)
                .field("reply_to_message_id", repliedMessage)
                .asJson();
    }

    public HttpResponse<JsonNode> getUpdates(Integer offset) throws UnirestException {
        return Unirest.post(BASE_URL + "/getUpdates")
                .field("offset", offset)
                .asJson();
    }

    public HttpResponse<JsonNode> getUpdates(Integer offset, Integer timeout) throws UnirestException {
        return Unirest.post(BASE_URL + "/getUpdates")
                .field("offset", offset)
                .field("timeout", timeout)
                .asJson();
    }

    //--- bot thinking

    public void run() throws UnirestException {
        int last_upd_id = 0;
        HttpResponse<JsonNode> response;
        System.out.println("Listening.\n");

        while (true) {
            response = getUpdates(last_upd_id++);
            if (response.getStatus() == 200) {
                JSONArray responses = response.getBody().getObject().getJSONArray("result");
                if (responses.isNull(0)) continue;
                else last_upd_id = responses.getJSONObject(responses.length() - 1).getInt("update_id") + 1;

                for (int i = 0; i < responses.length(); i++) {
                    if(responses.getJSONObject(i).has("message")) {
                        JSONObject message = responses.getJSONObject(i).getJSONObject("message");

                        processMessage(message);
                    }
                }
            }
        }
    }

    private void processMessage(JSONObject message) throws UnirestException {
        //Process message into a nice and cozy java object
        Message m = null;
        try {
            m = Message.getMessage(message);
        } catch (MessageException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }

        //Console log
        printLog(true, m);

        //Actually read the message
        handleInput(m);
    }

    private void printLog(boolean full_log, Message m) {
        if (full_log) System.out.println("\n" + m);
        else {
            String log = "\nMessage";

            if(m.isPrivate()) log += " received from a private chat";
            else log += " received from chat " + m.getChatName();

            log += " (chat id: " + m.getCid() + ")";
            try {
                log += " by user @" + m.getUsername();
            } catch (NoUsernameMessageException e) {
                log += " by user";
            } catch (NoAuthorMessageException ignored) {}

            try {
                log += " named " + m.getAuthor();
            } catch (NoAuthorMessageException ignored) {}

            System.out.println(log + ".");
        }
    }

    //--- Command handling

    private void handleInput(Message m) throws UnirestException {
        if (m.isChannel())
            handleInputChannel((ChannelMessage) m);

        else if (m.isGroup())
            handleInputGroup((GroupMessage) m);

        else if (m.isPrivate())
            handleInputPrivate((PrivateMessage) m);
    }

    private boolean isCommand(String m, String command) {
        return (m.startsWith(command) &&
                (!m.substring(command.length()).startsWith("@") ||
                m.substring(command.length()).startsWith("@" + USERNAME)));
    }

    private String removeCommandHead(String m, String command) {
        String m2 = m.substring(command.length()).trim();
        String uname = "@" + USERNAME;
        if(m2.startsWith(uname)) m2 = m2.substring(uname.length());

        return m2;
    }

    private void handleInputGroup(GroupMessage m) throws UnirestException {
        try {
            if (m.isEvent()) {
                System.out.println("\nDetected group event.");
                handleGroupEvent(m);
            } else if (isCommand(m.getText(), "/start"))
                handleStart(m.getCid());

            else if (isCommand(m.getText(), "/help"))
                handleHelp(m.getCid());

            else if (isCommand(m.getText(), "/whoami"))
                handleWhoami(m);

            else if (isCommand(m.getText(), "/list"))
                handleList(m);

            else if (isCommand(m.getText(), "/foldl")) {
                if(removeCommandHead(m.getText(), "/foldl").startsWith("-v"))
                    explainFoldHaskellVerbose(m.getCid());
                else explainFoldHaskell(m.getCid());
            }

            else if (isCommand(m.getText(), "/foldr")) {
                if(removeCommandHead(m.getText(), "/foldr").startsWith("-v"))
                    explainFoldHaskellVerbose(m.getCid());
                else explainFoldHaskell(m.getCid());
            }
        } catch (NoTextMessageException ignored) {}
    }

    private void handleInputPrivate(PrivateMessage m) throws UnirestException {
        try {
            if (isCommand(m.getText(), "/start"))
                handleStart(m.getCid());

            else if (isCommand(m.getText(), "/help"))
                handleHelp(m.getCid());

            else if (isCommand(m.getText(), "/whoami"))
                handleWhoami(m);

            else if (isCommand(m.getText(), "/list"))
                handleList(m);

            else if (isCommand(m.getText(), "/foldl")) {
                if(removeCommandHead(m.getText(), "/foldl").startsWith("-v"))
                    explainFoldHaskellVerbose(m.getCid());
                else explainFoldHaskell(m.getCid());
            }

            else if (isCommand(m.getText(), "/foldr")) {
                if(removeCommandHead(m.getText(), "/foldr").startsWith("-v"))
                    explainFoldHaskellVerbose(m.getCid());
                else explainFoldHaskell(m.getCid());
            }
        } catch (NoTextMessageException ignored) {}
    }

    private void handleInputChannel(ChannelMessage m) throws UnirestException {
    }

    private void handleStart(long chat_id) throws UnirestException {
        sendMessage(chat_id, "Hola. Soy el bot personal de Guillermo Serrahima.");
        return;
    }

    private void handleHelp(long chat_id) throws UnirestException {
        String help = "Aquí tienes una lista de comandos que acepto:\n\n" +
                "/start - Repite el saludo inicial\n" +
                "/help - Saca esta lista\n" +
                "/whoami - Muestra información visible sobre ti mismo\n" +
                "/list [nombre de la lista] - Permite crear, mostrar o " +
                "modificar una lista\n";

        sendMessage(chat_id, help.trim());
        sendMessage(chat_id, "_" + myName() + " v1.0.0_", PARSE_MARKDOWN);
    }

    private void handleWhoami(Message m) throws UnirestException {
        String s = "Estamos en";

        if(m.isPrivate()) s += " un chat privado";
        else s += " el chat " + m.getChatName();

        s += " de ID " + m.getCid() + ".\n";

        try {
            s += "Eres " + m.getAuthor() + ", con";
            try {
                s += " nombre de usuario @" + m.getUsername() +" e";
            } catch (NoUsernameMessageException ignored) {}

            s += " ID de usuario " + m.getUid() + ".\n";
        } catch (NoAuthorMessageException ignored) {}

        s += "En el PC que me mantiene, la fecha actual es " + LocalDate.now() +
                " en el formato ISO-8601.\n";

        s+= "Tu mensaje tiene hora de " + m.getTimestamp() + "\n";

        replyMessage(m.getCid(), m.getMid(), s.trim());
    }

    private void handleGroupEvent(GroupMessage m) throws NoTextMessageException, UnirestException {
        System.out.print("Bot @" + USERNAME + " detects event: ");
        if(m.userJoined()) {
            System.out.println("JOINING");
            System.out.println("New chat participant: " + m.getText());
            if(m.getText().equals("@" + USERNAME)) handleStart(m.getCid());
        }
        else if(m.userLeft()) {
            System.out.println("LEAVING");
            System.out.println("Leaving chat participant: " + m.getText());
        }
        else if(m.chatNameChanged()) {
            System.out.println("CHANGING CHAT NAME");
            System.out.println("New chat name: " + m.getText());

            try {
                chats.get(m.getCid()).setName(m.getText());
                saveChatIndex();
            } catch (SaveErrorChatException e) {
                sendMessage(m.getCid(), "Ha habido un error guardando el nuevo nombre de este chat.");
            }
        }
        else {
            System.out.println("UNKNOWN");
        }
    }

    private void handleList(GroupMessage m) throws UnirestException, NoTextMessageException {
        String[] reading = m.getText().split(" ", 2);
        if (reading.length < 2) {
            sendMessage(m.getCid(), "Formato incorrecto. " +
                    "Debes poner un espacio entre el comando /list y el nombre de la lista.");
            return;
        }
        String list = reading[1];

        if(!exists(m.getCid())) {
            try {
                chats.put(m.getCid(), new Chat(m.getChatName(), m.getCid()));
                saveChatIndex();
            } catch (SaveErrorChatException e) {
                sendMessage(m.getCid(), "Ha habido un error guardando los datos de este chat.");
                return;
            }
        }
        if(!exists(m.getCid(), list)) {
            try {
                chats.get(m.getCid()).add(list);
                saveChat(m.getCid());
            } catch (SaveErrorChatException e) {
                sendMessage(m.getCid(), "Ha habido un error creando la lista.");
                return;
            }
        }

        sendMessage(m.getCid(), getWarning(WARNING_NOT_IMPLEMENTED));
    }

    private void handleList(PrivateMessage m) throws UnirestException, NoTextMessageException {
        String[] reading = m.getText().split(" ", 2);
        if(reading.length < 2) {
            sendMessage(m.getCid(), "Formato incorrecto. " +
                    "Debes poner un espacio entre el comando /list y el nombre de la lista.");
            return;
        }
        String list = reading[1];

        if(!exists(m.getCid())) {
            try {
                chats.put(m.getCid(), new Chat(m.getChatName(), m.getCid()));
                saveChatIndex();
            } catch (SaveErrorChatException e) {
                sendMessage(m.getCid(), "Ha habido un error guardando los datos de este chat.");
                return;
            }
        }
        if(!exists(m.getCid(), list)) {
            try {
                chats.get(m.getCid()).add(list);
                saveChat(m.getCid());
            } catch (SaveErrorChatException e) {
                sendMessage(m.getCid(), "Ha habido un error creando la lista.");
                return;
            }
        }

        sendMessage(m.getCid(), getWarning(WARNING_NOT_IMPLEMENTED));
    }

    private void explainFoldHaskell(long chat_id) throws UnirestException {


        String fold = "The recursion for `foldr f x ys` where `ys = [y1,y2,...,yk]` looks like\n" +
                "\n" +
                "`f y1 (f y2 (... (f yk x) ...))`\n" +
                "\n" +
                "whereas the recursion for `foldl f x ys` looks like\n" +
                "\n" +
                "`f (... (f (f x y1) y2) ...) yk`";
        sendMessage(chat_id, fold, PARSE_MARKDOWN);
    }

    private void explainFoldHaskellVerbose(long chat_id) throws UnirestException {
        String fold = "The recursion for `foldr f x ys` where `ys = [y1,y2,...,yk]` looks like\n" +
                "\n" +
                "`f y1 (f y2 (... (f yk x) ...))`\n" +
                "\n" +
                "whereas the recursion for `foldl f x ys` looks like\n" +
                "\n" +
                "`f (... (f (f x y1) y2) ...) yk`\n" +
                "\n" +
                "An important difference here is that if the result of `f x y` can be computed using only the value of `x`, then `foldr` doesn't need to examine the entire list. For example:\n" +
                "\n" +
                "`foldr (&&) False (repeat False)`\n" +
                "returns `False` whereas\n" +
                "`foldl (&&) False (repeat False)`\n" +
                "never terminates. (Note: `repeat False` creates an infinite list where every element is `False`.)\n" +
                "\n" +
                "On the other hand, `foldl'` is tail recursive and strict. If you know that you'll have to traverse the whole list no matter what (e.g., summing the numbers in a list), then `foldl'` is more space- (and probably time-) efficient than `foldr`.";
        sendMessage(chat_id, fold, PARSE_MARKDOWN);
    }

    private boolean exists(long chat_id) {
        return  chats.containsKey(chat_id);
    }

    private boolean exists(long chat_id, String list) {
        return  chats.containsKey(chat_id) && chats.get(chat_id).exists(list);
    }

    private void saveChat(long chat_id) throws SaveErrorChatException {
        Persistence.getInstance().saveChat(chats.get(chat_id));
    }

    private void saveChatIndex() throws SaveErrorChatException {
        Persistence.getInstance().saveChatIndex(chats);
    }

    private String getWarning(int warning_id) {
        switch (warning_id) {
            case WARNING_UNKNOWN_ERROR:
                return "Algo ha ido MUY mal.";

            case WARNING_NOT_IMPLEMENTED:
                return "Aún no está implementado.";

            case WARNING_NO_GROUP:
                return "Debes estar en un grupo para poder hacer eso.";

            case WARNING_NO_USERNAME:
                return "No tienes nombre de usuario. Regístrate con uno en la " +
                        "configuración de Telegram para poder utilizarme.";

            default: return "Entrada incorrecta.";
        }
    }

    private String getFormatExample(int command) {
        return "Entrada incorrecta.";
    }
}