/*
 *  asbots
 *  Copyright (C) 2021  niansa
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "instance.hpp"
#include "config.hpp"
#include "uid.hpp"
#include "utility.hpp"

#include <fmt/format.h>
#include <string>
#include <tuple>
#include <string_view>
#include <algorithm>
#include <memory>
#include <ctime>

using fmt::operator""_format;
using std::literals::operator""sv;
using namespace Utility;



void Event::parse(std::string_view str) {
    auto split = strSplit(str, ' ', 2);
    // Check split size
    argsSizeCheck("basic", split, 2);
    // Move values
    split[0] = {split[0].data()+1, split[0].size()-1}; // Erase leading ':'
    auto id_len = split[0].size();
    if (id_len == SUID_len) {
        sender = SUID(std::move(split[0]));
    } else if (id_len == UUID_len) {
        sender = UUID(std::move(split[0]));
    } else {
        sender = split[0];
    }
    name = std::move(split[1]);
    if (split.size() == 3) {
        // Get args and text
        std::tie(raw_args, text) = splitOnce(split[2], " :");
    }
}


void Command::parse(std::string_view str) {
    auto split = strSplit(str, ' ', 1);
    name = std::move(split[0]);
    // Get text from args
    std::tie(args, text) = splitOnce(split[1], " :");
}


template<bool channelModes>
void ModeSet::parse(std::string_view in, NetworkInfo &netInfo) {
    enum {
        ADDING,
        REMOVING
    } op;
    // Split up
    auto split = strSplit(in, ' ');
    auto paramIt = split.begin() + 1;
    // Iterate through mode characters
    for (const char character : split[0]) {
        if (character == '+') {
            op = ADDING;
        } else if (character == '-') {
            op = REMOVING;
        } else {
            if (op == ADDING) {
                if (str.find(character) == str.npos || netInfo.channelModes.isListMode(character)) {
                    str.push_back(character);
                    if constexpr(channelModes) {
                        if (netInfo.channelModes.takesParamOnSet(character)) {
                            if (paramIt == split.end()) {
                                throw DesyncError();
                            }
                            params.push_back({character, std::string(*(paramIt++))});
                        }
                    }
                } else {
                    throw DesyncError();
                }
            } else if (op == REMOVING) {
                auto res = str.find(character);
                if (res != str.npos) {
                    str.erase(res, 1);
                    if constexpr(channelModes) {
                        if (netInfo.channelModes.takesParamOnUnset(character)) {
                            if (paramIt == split.end()) {
                                throw DesyncError();
                            }
                            for (auto it = params.begin(); ; it++) {
                                if (it == params.end()) {
                                    throw DesyncError();
                                }
                                auto &[c, d] = *it;
                                if (c == character && (d == *paramIt || *paramIt == "*")) {
                                    params.erase(it);
                                    break;
                                }
                            }
                            paramIt++;
                        }
                    }
                } else {
                    throw DesyncError();
                }
            }
        }
    }
}


void User::parse_euid(const Event& event, NetworkInfo& netInfo) {
    this->server = std::get<SUID>(event.sender.id);
    // Check size
    argsSizeCheck("EUID", event.args, 10);
    // Move values
    nick = event.args[0];
    hops = std::stoull(std::string(event.args[1]));
    umode.parse<false>(event.args[3], netInfo);
    ident = event.args[4];
    vhost = event.args[5];
    ip = event.args[6];
    uid = UUID(event.args[7]);
    realhost = event.args[8];
    if (event.args[9] != "*" && event.args[9] != "0") {
        loginName = event.args[9];
    }
    // Get realname
    realname = std::move(event.text);
}

void User::removeChannel(const u_Channel& channel) {
    std::remove_if(channels.begin(), channels.end(), [&] (auto obj) {return obj == &channel;});
}


void Channel::parse_sjoin(const Event& event, Cache& cache, NetworkInfo& netInfo) {
    this->server = std::get<SUID>(event.sender.id);
    // Check size
    argsSizeCheck("SJOIN", event.args, 3);
    // Move values
    name = std::move(event.args[1]);
    mode.parse<true>(event.args[2], netInfo);
    // Get members
    for (auto& raw_uuid : strSplit(event.text, ' ')) {
        // Erase prefix
        if (raw_uuid.size() > UUID_len) {
            char prefixChar = raw_uuid[0];
            raw_uuid = {raw_uuid.data()+1, raw_uuid.size()-1};
            char modeChar = netInfo.channelModes.prefixMap[prefixChar];
            mode.str.push_back(modeChar);
            mode.params.push_back({modeChar, std::string(raw_uuid)});
        }
        // Find user in cache
        auto res = cache.find_user_by_uid(UUID(raw_uuid));
        if (res == cache.users.end()) {
            throw DesyncError();
        }
        u_User& member = *res;
        // Append member to list
        members.push_back(&member);
    }
}

void Channel::removeMember(const u_User& member) {
    std::remove_if(members.begin(), members.end(), [&] (auto obj) {return *obj == member;});
}


std::vector<u_User>::iterator Cache::find_user_by_nick(std::string_view nick) {
    for (auto it = users.begin(); ; it++) {
        if (it == users.end() || it->get()->nick == nick) {
            return it;
        }
    }
}

std::vector<u_User>::iterator Cache::find_user_by_uid(const UUID& uid) {
    for (auto it = users.begin(); ; it++) {
        if (it == users.end() || it->get()->uid == uid) {
            return it;
        }
    }
}

std::vector<u_Channel>::iterator Cache::find_channel_by_name(std::string_view name) {
    for (auto it = channels.begin(); ; it++) {
        if (it == channels.end() || it->get()->name == name) {
            return it;
        }
    }
}


async::result<void> Instance::run() {
    // Prepare services
    for (auto& service : services) {
        service->i = this;
        service->uuid = UUIDGen();
        async::detach(service->intitialize());
    }

    // Create connection
    socket.reset(new uvpp::tcp{s});
    co_await socket->connect(addr);
    socket->recv_start();

    // Login
    co_await login();

    // Mainloop
    while (true) {
        // Read
        auto data = co_await socket->recv();
        // Check for general error
        if (data->error()) {
            continue;
        }
        // Check for broken connection
        if (data->broken()) {
            break;
        }
        // Make string
        auto dataStr = std::string_view{
                data->data.get(),
                static_cast<std::string_view::size_type>(data->nread)
        };
        // Split by newlines
        for (auto& line : strSplit(dataStr, '\n')) {
            if (line.size() < 2) {
                continue; // Empty line
            }
            // Remove \r
            if (line.back() == '\r') {
                line = {line.data(), line.size()-1};
            }
            // Check if server sent an event or command, then parse and process it
            if (line[0] == ':') {
                Event event;
                event.parse(std::move(line));
                async::detach(process(std::move(event)));
            } else {
                Command command;
                command.parse(std::move(line));
                async::detach(process(std::move(command)));
            }
        }
    }
}

async::result<void> Instance::login() {
    co_await socket->send("PASS {} TS 6 :{}\n"_format(config.auth.send_password, config.server.uid.str()));
    co_await socket->send("CAPAB :QS EX IE KLN UNKLN ENCAP TB SERVICES EUID EOPMOD MLOCK\n"sv);
    co_await socket->send("SERVER {} 1 :{}{}\n"_format(config.server.name, config.server.hidden ? "(H) " : "", config.server.description));
    co_await socket->send("SVINFO 6 3 0 :{}\n"_format(time(nullptr)));
}

async::result<void> Instance::burst() {
    std::cout << "I'm done bursting too. Waiting for network informations..." << std::endl;
    co_await socket->send("VERSION\n"sv);
    co_await netInfo.wait_ready();
    std::cout << "Ready." << std::endl;
    co_await send_ping();
    client_bursting = false;
}

async::result<void> Instance::process(const Command command) {
    std::clog << command.dump() << std::flush;
    if (command.name == "PASS") {
        // Check password
        auto given_password = strSplit(command.args, ' ', 1)[0];
        if (given_password != config.auth.accept_password) {
            throw ConnectionError("Server supplied wrong password during authentication");
        }
        authed = true;
        // Get server ID
        connected_server.uid = SUID(command.text);
    } else if (command.name == "ERROR") {
        throw ConnectionError(command.dump());
    } else if (command.name == "SQUIT") {
        if (command.args == config.server.uid.str()) {
            throw ConnectionError(command.dump());
        }
    } else if (!authed) {
        throw ConnectionError("Server tried to execute a command before authenticating");
    } else if (command.name == "SERVER") {
        // Get name and description of connected server
        connected_server.name = strSplit(command.args, ' ', 1)[0];
        connected_server.description = command.text;
    } else if (command.name == "PING") {
        // End of burst
        if (server_bursting) {
            server_bursting = false;
            std::cout << "Server burst is over. It's my turn." << std::endl;
            co_await burst();
        }
        // Reply
        co_await socket->send(":{} PONG {} {} :{}\n"_format(config.server.uid.str(), config.server.name, command.args, command.text));
    }
}

async::result<void> Instance::process(Event event) {
    event.splitArgs();
    std::clog << event.dump() << std::flush;
    if (!authed && event.name != "NOTICE") {
        throw ConnectionError("Server tried to send an event before authenticating");
    }
    // Fetched info
    else if (event.name == "005") {
        // Check if that 005 was for me
        if (event.args[0] != config.server.uid.str()) {
            co_return;
        }
        // Split the list
        // Iterate and find the information we need
        for (const auto& field : event.args) {
            // Split into key and value
            auto split = strSplit(field, '=', 1);
            // Check size
            if (split.size() != 2) {
                continue;
            }
            // Check if we've got the right key
            if (split[0] == "NETWORK") {
                // Network name
                netInfo.name = std::move(split[1]);
                netInfo.fields_received++;
            } else if (split[0] == "CHANMODES") {
                // Channel modes
                auto modeLists = strSplit(split[1], ',', 3);
                netInfo.channelModes = {
                    .listModes = std::string(modeLists[0]),
                    .paramOnSetAndUnsetModes = std::string(modeLists[1]),
                    .paramOnSetOnlyModes = std::string(modeLists[2]),
                    .paramLessModes = std::string(modeLists[3])
                };
                netInfo.fields_received++;
            } else if (split[0] == "PREFIX") {
                auto val = split[1];
                // User prefixes
                auto modesIt = val.find("(") + 1;
                auto prefixesIt = val.find(")") + 1;
                while (val[modesIt] != ')') {
                    netInfo.channelModes.prefixMap[val[prefixesIt]] = val[modesIt];
                    netInfo.channelModes.listModes.push_back(val[modesIt]);
                    modesIt++;
                    prefixesIt++;
                }
                netInfo.fields_received++;
            }
        }
        // Check if everything needed has been fetched
        if (netInfo.fields_received == 3 && !netInfo.ready) {
            netInfo.mark_ready();
        }
    } else {
        co_await netInfo.wait_ready();
        // User updates
        if (event.name == "EUID") {
            // User connected to the network
            // Create user and parse event
            auto user = std::make_unique<User>();
            user->parse_euid(event, netInfo);
            // Append user to cache
            cache.users.push_back(std::move(user));
        } else if (event.name == "QUIT") {
            // User disconnected from the network
            // Find user in cache
            auto res = cache.find_user_by_uid(std::get<UUID>(event.sender.id));
            if (res == cache.users.end()) {
                throw DesyncError();
            }
            // Delete user from all channels
            for (auto& channel : res->get()->channels) {
                channel->get()->removeMember({*res});
            }
            // Delete user from cache
            cache.users.erase(res);
        } else if (event.name == "NICK") {
            // User changed their nick
            // Find user in cache
            auto res = cache.find_user_by_uid(std::get<UUID>(event.sender.id));
            if (res == cache.users.end()) {
                throw DesyncError();
            }
            // Set nick
            res->get()->nick = event.args[0];
        } else if (event.name == "MODE") {
            // User changed their mode
            // Find user in cache
            auto res = cache.find_user_by_uid(std::get<UUID>(event.sender.id));
            if (res == cache.users.end()) {
                throw DesyncError();
            }
            // Update mode
            res->get()->umode.parse<false>(event.text, netInfo);
        } else if (event.name == "ENCAP") {
            // Get args
            argsSizeCheck("ENCAP", event.args, 3);
            if (event.args[1] == "SU") {
                // User logged in
                // Check args
                argsSizeCheck("ENCAP (SU)", event.args, 4);
                // Find user in cache
                auto res = cache.find_user_by_uid(event.args[2]);
                if (res == cache.users.end()) {
                    throw DesyncError();
                }
                // Update login name
                if (event.args.size() > 3) {
                    auto loginName = event.args[3];
                    if (loginName.empty() || loginName == "*" || loginName == "0") {
                        res->get()->loginName.reset();
                    } else {
                        res->get()->loginName = event.args[3];
                    }
                } else {
                    res->get()->loginName.reset();
                }
            }
        }
        // Channel updates
        else if (event.name == "SJOIN") {
            // Channel was created
            // Create channel and parse event
            auto channel = std::make_unique<Channel>();
            channel->parse_sjoin(event, cache, netInfo);
            // Append channel to cache
            cache.channels.push_back(std::move(channel));
        } else if (event.name == "TOPIC" || event.name == "TB") {
            // Channels topic changed
            // Find channel in cache
            auto res = cache.find_channel_by_name(event.args[0]);
            if (res == cache.channels.end()) {
                throw DesyncError();
            }
            // Set topic
            res->get()->topic = event.text;
        } else if (event.name == "JOIN") {
            // User joined existing channel
            argsSizeCheck("JOIN", event.args, 3);
            // Get channel from cache
            auto c_res = cache.find_channel_by_name(event.args[1]);
            if (c_res == cache.channels.end()) {
                throw DesyncError();
            }
            // Get user from cache
            auto u_res = cache.find_user_by_uid(std::get<UUID>(event.sender.id));
            if (u_res == cache.users.end()) {
                throw DesyncError();
            }
            // Assign user to channel and vice versa
            c_res->get()->members.push_back(&*u_res);
            u_res->get()->channels.push_back(&*c_res);
            // Update channel modes
            c_res->get()->mode.parse<true>(event.args[2], netInfo);
        } else if (event.name == "PART" || event.name == "KICK") {
            // User left channel
            // Get channel from cache
            auto c_res = cache.find_channel_by_name(event.args[0]);
            if (c_res == cache.channels.end()) {
                throw DesyncError();
            }
            // Get user from cache
            auto u_res = cache.find_user_by_uid((event.name=="PART")?std::get<UUID>(event.sender.id):event.args[1]);
            if (u_res == cache.users.end()) {
                throw DesyncError();
            }
            // Remove channel from both user and channel
            c_res->get()->removeMember(*u_res);
            u_res->get()->removeChannel(*c_res);
        } else if (event.name == "TMODE" || event.name == "BMASK") {
            // Channel modes changed
            argsSizeCheck(event.name, event.args, 3);
            // Split args
            std::string_view modes, channelName;
            {
                channelName = event.args[1];
                modes = event.args[2];
            }
            // Get channel from cache
            auto c_res = cache.find_channel_by_name(channelName);
            if (c_res == cache.channels.end()) {
                throw DesyncError();
            }
            // Apply changes
            c_res->get()->mode.parse<true>("+{} {}"_format(modes, event.text), netInfo);
        }
        // Messages
        else if (event.name == "PRIVMSG") {
            // On message
            // Check that message is not directed to channel
            if (isalnum(event.raw_args[0])) {
                // Get author from cache
                auto res = cache.find_user_by_uid(std::get<UUID>(event.sender.id));
                if (res == cache.users.end()) {
                    throw DesyncError();
                }
                // Get target UUID
                UUID target(event.args[0]);
                // Pass to services with ownership over target
                for (auto& service : services) {
                    if (service->ready && service->uuid.str() == target.str()) {
                        co_await service->on_direct_privmsg(event.text, *res);
                    }
                }
            }
        }
        // Pass to services
        for (auto& service : services) {
            if (service->ready) {
                co_await service->on_event(event);
            }
        }
    }
}

async::result<void> Instance::send_event(const Event& event) {
    co_await socket->send(event.dump());
    co_await process(event);
}

async::result<void> Instance::send_ping() {
    co_await socket->send("PING :{}\n"_format(connected_server.uid.str()));
}