/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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, 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, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/

#include "config.h"

#include "mu-msg-fields.h"
#include "mu-msg.hh"
#include "mu-server.hh"

#include <iostream>
#include <string>
#include <algorithm>
#include <atomic>
#include <thread>
#include <mutex>
#include <functional>

#include <cstring>
#include <glib.h>
#include <glib/gprintf.h>

#include "mu-runtime.hh"
#include "mu-maildir.hh"
#include "mu-query.hh"
#include "index/mu-indexer.hh"
#include "mu-store.hh"
#include "mu-msg-part.hh"
#include "mu-contacts.hh"

#include "utils/mu-str.h"
#include "utils/mu-utils.hh"
#include "utils/mu-command-parser.hh"
#include "utils/mu-readline.hh"

using namespace Mu;
using namespace Command;

/// @brief object to manage the server-context for all commands.
struct Server::Private {
        Private(Store& store, Output output):
                store_{store},
                output_{output},
                command_map_{make_command_map()},
                query_{store_},
                keep_going_{true} {}
        //
        // construction helpers
        //
        CommandMap make_command_map();

        //
        // acccessors
        Store& store()                        { return store_; }
        const Store& store() const            { return store_; }
        Indexer& indexer()                    { return store().indexer(); }
        const CommandMap& command_map() const { return command_map_; }
        const Query& query() const            { return query_; }

        //
        // invoke
        //
        bool invoke (const std::string& expr) noexcept;

        //
        // output
        //
        void output_sexp(Sexp&& sexp) const {
                if (output_)
                        output_(std::move(sexp));
        }
        void output_sexp(Sexp::List&& lst) const {
                output_sexp(Sexp::make_list(std::move(lst)));
        }
        size_t output_sexp (const QueryResults& qres);

        //
        // handlers for various commands.
        //
        void add_handler (const Parameters& params);
        void compose_handler (const Parameters& params);
        void contacts_handler (const Parameters& params);
        void extract_handler (const Parameters& params);
        void find_handler (const Parameters& params);
        void help_handler (const Parameters& params);
        void index_handler (const Parameters& params);
        void move_handler (const Parameters& params);
        void mkdir_handler (const Parameters& params);
        void ping_handler (const Parameters& params);
        void quit_handler (const Parameters& params);
        void remove_handler (const Parameters& params);
        void sent_handler (const Parameters& params);
        void view_handler (const Parameters& params);

private:
        // helpers
        Sexp build_message_sexp(MuMsg *msg, unsigned docid,
                                const Option<QueryMatch&> qm,
                                MuMsgOptions opts);

        Sexp::List move_docid (Store::Id docid, const std::string& flagstr,
                               bool new_name, bool no_view);
        Sexp::List perform_move (Store::Id docid, MuMsg *msg,
                                 const std::string& maildirarg,
                                 MuFlags flags, bool new_name, bool no_view);

        bool maybe_mark_as_read (MuMsg *msg, Store::Id docid);
        bool maybe_mark_msgid_as_read (const  Mu::Query& query, const char* msgid);

        Store&           store_;
        Server::Output   output_;
        const CommandMap command_map_;
        const Query      query_;

        std::atomic<bool> keep_going_{};
};

static void
add_thread_info (Sexp::List& items, const QueryMatch& qmatch)
{
        Sexp::List info;

        auto symbol_t = []{return Sexp::make_symbol("t");};

        info.add_prop(":path",  Sexp::make_string(qmatch.thread_path));
        info.add_prop(":level", Sexp::make_number(qmatch.thread_level));
        info.add_prop(":date",  Sexp::make_string(qmatch.thread_date));

        Sexp::List dlist;
        const auto td {::atoi(qmatch.thread_date.c_str())};
        dlist.add(Sexp::make_number((unsigned)(td >> 16)));
        dlist.add(Sexp::make_number((unsigned)(td & 0xffff)));
        dlist.add(Sexp::make_number(0));
        info.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist)));

        if (qmatch.has_flag(QueryMatch::Flags::Root))
                info.add_prop(":root", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::Related))
                info.add_prop(":related", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::First))
                info.add_prop(":first-child", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::Last))
                info.add_prop(":last-child", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::Orphan))
                info.add_prop(":orphan", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::Duplicate))
                info.add_prop(":duplicate", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::HasChild))
                info.add_prop(":has-child", symbol_t());
        if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject))
                info.add_prop(":thread-subject", symbol_t());

        items.add_prop(":thread", Sexp::make_list(std::move(info)));
}

Sexp
Server::Private::build_message_sexp (MuMsg *msg, unsigned docid,
                                     const Option<QueryMatch&> qm,
                                     MuMsgOptions opts)
{
        auto lst{Mu::msg_to_sexp_list(msg, docid, opts)};

        if (qm)
                add_thread_info(lst, *qm);

        return Sexp::make_list(std::move(lst));
}
CommandMap
Server::Private::make_command_map ()
{
      CommandMap cmap;

      using Type = Sexp::Type;

      cmap.emplace("add",
                   CommandInfo{
                           ArgMap{ {":path", ArgInfo{Type::String, true, "file system path to the message" }}},
                           "add a message to the store",
                           [&](const auto& params){add_handler(params);}});

      cmap.emplace("compose",
                   CommandInfo{
                           ArgMap{{":type",    ArgInfo{Type::Symbol, true,
                                           "type of composition: reply/forward/edit/resend/new"}},
                                  {":docid",   ArgInfo{Type::Number, false,
                                           "document id of parent-message, if any"}},
                                  {":decrypt", ArgInfo{Type::Symbol, false,
                                           "whether to decrypt encrypted parts (if any)" }}},
                           "compose a new message",
                           [&](const auto& params){compose_handler(params);}});

      cmap.emplace("contacts",
                   CommandInfo{
                           ArgMap{ {":personal", ArgInfo{Type::Symbol, false,
                                                   "only personal contacts" }},
                                   {":after",    ArgInfo{Type::String, false,
                                            "only contacts seen after time_t string" }},
                                   {":tstamp",   ArgInfo{Type::String, false,
                                            "return changes since tstamp" }}},
                           "get contact information",
                           [&](const auto& params){contacts_handler(params);}});

      cmap.emplace("extract",
                   CommandInfo{
                           ArgMap{{":docid",    ArgInfo{Type::Number, true,  "document for the message" }},
                                   {":index",   ArgInfo{Type::Number, true,
                                            "index for the part to operate on" }},
                                   {":action",  ArgInfo{Type::Symbol, true, "what to do with the part" }},
                                   {":decrypt", ArgInfo{Type::Symbol, false,
                                            "whether to decrypt encrypted parts (if any)" }},
                                   {":path",    ArgInfo{Type::String, false,
                                            "part for saving (for action: save)" }},
                                   {":what",    ArgInfo{Type::Symbol, false,
                                            "what to do with the part (feedback)" }},
                                   {":param",   ArgInfo{Type::String, false, "parameter for 'what'" }}},
                           "extract mime-parts from a message",
                           [&](const auto& params){extract_handler(params);}});

      cmap.emplace("find",
                   CommandInfo{
                           ArgMap{ {":query",   ArgInfo{Type::String, true, "search expression" }},
                                   {":threads", ArgInfo{Type::Symbol, false,
                                            "whether to include threading information" }},
                                   {":sortfield",  ArgInfo{Type::Symbol, false, "the field to sort results by" }},
                                   {":descending", ArgInfo{Type::Symbol, false,
                                            "whether to sort in descending order" }},
                                   {":maxnum",  ArgInfo{Type::Number, false,
                                            "maximum number of result (hint)" }},
                                   {":skip-dups",  ArgInfo{Type::Symbol, false,
                                            "whether to skip messages with duplicate message-ids" }},
                                   {":include-related",  ArgInfo{Type::Symbol, false,
                                            "whether to include other message related to matching ones" }}},
                           "query the database for messages",
                           [&](const auto& params){find_handler(params);}});

      cmap.emplace("help",
                   CommandInfo{
                           ArgMap{ {":command", ArgInfo{Type::Symbol, false,
                                                   "command to get information for" }},
                                   {":full", ArgInfo{Type::Symbol, false,
                                            "show full descriptions" }}},
                           "get information about one or all commands",
                           [&](const auto& params){help_handler(params);}});
      cmap.emplace("index",
                   CommandInfo{
                           ArgMap{ {":my-addresses", ArgInfo{Type::List, false, "list of 'my' addresses"}},
                                   {":cleanup",      ArgInfo{Type::Symbol, false,
                                                           "whether to remove stale messages from the store"}},
                                   {":lazy-check",   ArgInfo{Type::Symbol, false,
                                            "whether to avoid indexing up-to-date directories"}}},
                           "scan maildir for new/updated/removed messages",
                           [&](const auto& params){index_handler(params);}});

      cmap.emplace("move",
                   CommandInfo{
                           ArgMap{ {":docid",   ArgInfo{Type::Number, false, "document-id"}},
                                   {":msgid",   ArgInfo{Type::String, false, "message-id"}},
                                   {":flags",   ArgInfo{Type::String, false, "new flags for the message"}},
                                   {":maildir", ArgInfo{Type::String, false, "the target maildir" }},
                                   {":rename",  ArgInfo{Type::Symbol, false,  "change filename when moving" }},
                                   {":no-view", ArgInfo{Type::Symbol, false,
                                            "if set, do not hint at updating the view"}},},
                           "move messages and/or change their flags",

                           [&](const auto& params){move_handler(params);}});

      cmap.emplace("mkdir",
                   CommandInfo{
                           ArgMap{ {":path", ArgInfo{Type::String, true,
                                                   "location for the new maildir" }}},
                           "create a new maildir",
                          [&](const auto& params){mkdir_handler(params);}});
      cmap.emplace("ping",
                   CommandInfo{
                           ArgMap{ {":queries",    ArgInfo{Type::List, false,
                                                   "queries for which to get read/unread numbers"}},
                                   {":skip-dups",  ArgInfo{Type::Symbol, false,
                                            "whether to exclude messages with duplicate message-ids"}},},
                           "ping the mu-server and get information in response",
                          [&](const auto& params){ping_handler(params);}});

      cmap.emplace("quit",
                   CommandInfo{{},
                           "quit the mu server",
                           [&](const auto& params){quit_handler(params);}});

      cmap.emplace("remove",
                   CommandInfo{
                           ArgMap{ {":docid", ArgInfo{Type::Number, true,
                                                   "document-id for the message to remove" }}},
                           "remove a message from filesystem and database",
                          [&](const auto& params){remove_handler(params);}});

      cmap.emplace("sent",
                   CommandInfo{
                           ArgMap{ {":path", ArgInfo{Type::String, true,
                                            "path to the message file" }}
                           },
                           "tell mu about a message that was sent",
                          [&](const auto& params){sent_handler(params);}});

      cmap.emplace("view",
                   CommandInfo{
                           ArgMap{{":docid",           ArgInfo{Type::Number, false, "document-id"}},
                                   {":msgid",          ArgInfo{Type::String, false, "message-id"}},
                                   {":path",           ArgInfo{Type::String, false, "message filesystem path"}},
                                   {":mark-as-read",   ArgInfo{Type::Symbol, false,
                                            "mark message as read (if not already)"}},
                                   {":extract-images", ArgInfo{Type::Symbol, false,
                                            "whether to extract images for this messages (if any)"}},
                                   {":decrypt",        ArgInfo{Type::Symbol, false,
                                            "whether to decrypt encrypted parts (if any)" }},
                                   {":verify",         ArgInfo{Type::Symbol, false,
                                            "whether to verify signatures (if any)" }}

                           },
                           "view a message. exactly one of docid/msgid/path must be specified",
                           [&](const auto& params){view_handler(params);}});
      return cmap;
}

G_GNUC_PRINTF(2,3) static Sexp
make_error (Error::Code errcode, const char* frm, ...)
{
        char    *msg{};
        va_list ap;

        va_start (ap, frm);
        g_vasprintf (&msg, frm, ap);
        va_end (ap);

        Sexp::List err;
        err.add_prop(":error",   Sexp::make_number(static_cast<int>(errcode)));
        err.add_prop(":message", Sexp::make_string(msg));
        g_free (msg);

        return Sexp::make_list(std::move(err));
}

bool
Server::Private::invoke (const std::string& expr) noexcept
{
        if (!keep_going_)
                return false;

        try {
                auto call{Sexp::Sexp::make_parse(expr)};
                Command::invoke(command_map(), call);

        } catch (const Mu::Error& me) {
                output_sexp(make_error(me.code(), "%s", me.what()));
        } catch (const std::runtime_error& re) {
                output_sexp(make_error(Error::Code::Internal, "caught exception: %s", re.what()));
                keep_going_ = false;
        } catch (...) {
                output_sexp(make_error(Error::Code::Internal, "something went wrong: quiting"));
                keep_going_ = false;
        }

        return keep_going_;
}


static MuMsgOptions
message_options (const Parameters& params)
{
        const auto extract_images{get_bool_or(params, ":extract-images", false)};
        const auto decrypt{get_bool_or(params, ":decrypt", false)};
        const auto verify{get_bool_or(params, ":verify", false)};

        int opts{MU_MSG_OPTION_NONE};
        if (extract_images)
                opts |= MU_MSG_OPTION_EXTRACT_IMAGES;
        if (verify)
                opts |= MU_MSG_OPTION_VERIFY  | MU_MSG_OPTION_USE_AGENT;
        if (decrypt)
                opts |= MU_MSG_OPTION_DECRYPT | MU_MSG_OPTION_USE_AGENT;

        return (MuMsgOptions)opts;
}

/* 'add' adds a message to the database, and takes two parameters: 'path', which
 * is the full path to the message, and 'maildir', which is the maildir this
 * message lives in (e.g. "/inbox"). response with an (:info ...) message with
 * information about the newly added message (details: see code below)
 */
void
Server::Private::add_handler (const Parameters& params)
{
        auto path{get_string_or(params, ":path")};
        const auto docid{store().add_message(path)};

        Sexp::List expr;
        expr.add_prop(":info",  Sexp::make_symbol("add"));
        expr.add_prop(":path",  Sexp::make_string(path));
        expr.add_prop(":docid", Sexp::make_number(docid));

        output_sexp(Sexp::make_list(std::move(expr)));

        auto msg{store().find_message(docid)};
        if (!msg)
                throw Error(Error::Code::Store,
                            "failed to get message at %s (docid=%u)",
                            path.c_str(), docid);

        Sexp::List update;
        update.add_prop(":update", build_message_sexp(msg, docid, {}, MU_MSG_OPTION_VERIFY));
        output_sexp(Sexp::make_list(std::move(update)));
        mu_msg_unref(msg);
}


struct PartInfo {
        Sexp::List        attseq;
        MuMsgOptions      opts;
};

static void
each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo)
{
        /* exclude things that don't look like proper attachments, unless they're images */
        if (!mu_msg_part_maybe_attachment(part))
                return;

        GError *gerr{};
        char *cachefile = mu_msg_part_save_temp (
                msg, (MuMsgOptions)(pinfo->opts|MU_MSG_OPTION_OVERWRITE),
                part->index, &gerr);
        if (!cachefile)
                throw Error (Error::Code::File, &gerr, "failed to save part");

        Sexp::List pi;
        pi.add_prop(":file-name", Sexp::make_string(cachefile));
        pi.add_prop(":mime-type", Sexp::make_string(format("%s/%s",
                                                            part->type,
                                                            part->subtype)));
        pinfo->attseq.add(Sexp::make_list(std::move(pi)));
        g_free (cachefile);
}

/* 'compose' produces the un-changed *original* message sexp (ie., the message
 * to reply to, forward or edit) for a new message to compose). It takes two
 * parameters: 'type' with the compose type (either reply, forward or
 * edit/resend), and 'docid' for the message to reply to. Note, type:new does
 * not have an original message, and therefore does not need a docid
 *
 * In returns a (:compose <type> [:original <original-msg>] [:include] )
 * message (detals: see code below)
 *
 * Note ':include' t or nil determines whether to include attachments
 */
void
Server::Private::compose_handler (const Parameters& params)
{
        const auto ctype{get_symbol_or(params, ":type")};

        Sexp::List comp_lst;
        comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype)));

        if (ctype == "reply" || ctype == "forward" || ctype == "edit" || ctype == "resend") {

                GError *gerr{};
                const unsigned docid{(unsigned)get_int_or(params, ":docid")};
                auto msg{store().find_message(docid)};
                if (!msg)
                        throw Error{Error::Code::Store, &gerr, "failed to get message %u", docid};

                const auto opts{message_options(params)};
                comp_lst.add_prop(":original", build_message_sexp(msg, docid, {}, opts));

                if (ctype == "forward") {
                        PartInfo pinfo{};
                        pinfo.opts = opts;
                        mu_msg_part_foreach (msg, opts,
                                             (MuMsgPartForeachFunc)each_part, &pinfo);
                        if (!pinfo.attseq.empty())
                                comp_lst.add_prop (":include",
                                                   Sexp::make_list(std::move(pinfo.attseq)));
                }
                mu_msg_unref (msg);

        } else if (ctype != "new")
                throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'",
                            ctype.c_str());

        output_sexp (std::move(comp_lst));
}

void
Server::Private::contacts_handler (const Parameters& params)
{
        const auto personal  = get_bool_or(params,   ":personal");
        const auto afterstr  = get_string_or(params, ":after");
        const auto tstampstr = get_string_or(params, ":tstamp");

        const auto after{afterstr.empty() ? 0 :
                        g_ascii_strtoll(date_to_time_t_string(afterstr, true).c_str(), {}, 10)};
        const auto tstamp = g_ascii_strtoll (tstampstr.c_str(), NULL, 10);

        auto rank{0};
        Sexp::List contacts;
        store().contacts().for_each([&](const ContactInfo& ci) {

                rank++;

                /* since the last time we got some contacts */
                if (tstamp > ci.tstamp)
                        return;
                /* (maybe) only include 'personal' contacts */
                if (personal && !ci.personal)
                        return;
                /* only include newer-than-x contacts */
                if (after > ci.last_seen)
                        return;

                Sexp::List contact;
                contact.add_prop(":address", Sexp::make_string(ci.full_address));
                contact.add_prop(":rank",    Sexp::make_number(rank));

                contacts.add(Sexp::make_list(std::move(contact)));
        });

        Sexp::List seq;
        seq.add_prop(":contacts", Sexp::make_list(std::move(contacts)));
        seq.add_prop(":tstamp",   Sexp::make_string(format("%" G_GINT64_FORMAT,
                                                           g_get_monotonic_time())));
        /* dump the contacts cache as a giant sexp */
        output_sexp(std::move(seq));
}


static Sexp::List
save_part (MuMsg *msg, unsigned docid, unsigned index,
           MuMsgOptions opts, const Parameters& params)
{
        const auto path{get_string_or(params, ":path")};
        if (path.empty())
                throw Error{Error::Code::Command, "missing path"};

        GError *gerr{};
        if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | (int)MU_MSG_OPTION_OVERWRITE),
                               path.c_str(), index, &gerr))
                throw Error{Error::Code::File, &gerr, "failed to save part"};

        Sexp::List seq;
        seq.add_prop(":info", Sexp::make_symbol("save"));
        seq.add_prop(":message", Sexp::make_string(format("%s has been saved", path.c_str())));

        return seq;
}


static Sexp::List
open_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts)
{
        GError *gerr{};
        char *targetpath{mu_msg_part_get_cache_path (msg, opts, index, &gerr)};
        if (!targetpath)
                throw Error{Error::Code::File, &gerr, "failed to get cache-path"};

        if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING),
                                targetpath, index, &gerr)) {
                g_free(targetpath);
                throw Error{Error::Code::File, &gerr, "failed to save to cache-path"};
        }

        if (!mu_util_play (targetpath, TRUE,/*allow local*/
                           FALSE/*allow remote*/, &gerr)) {
                g_free(targetpath);
                throw Error{Error::Code::File, &gerr, "failed to play"};
        }

        Sexp::List seq;
        seq.add_prop(":info",    Sexp::make_symbol("open"));
        seq.add_prop(":message", Sexp::make_string(format("%s has been opened", targetpath)));
        g_free (targetpath);

        return seq;
}

static Sexp::List
temp_part (MuMsg *msg, unsigned docid, unsigned index,
           MuMsgOptions opts, const Parameters& params)
{
        const auto what{get_symbol_or(params, ":what")};
        if (what.empty())
                throw Error{Error::Code::Command, "missing 'what'"};

        const auto param{get_string_or(params, ":param")};

        GError *gerr{};
        char *path{mu_msg_part_get_cache_path (msg, opts, index, &gerr)};
        if (!path)
                throw Error{Error::Code::File, &gerr, "could not get cache path"};

        if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING),
                               path, index, &gerr)) {
                g_free(path);
                throw Error{Error::Code::File, &gerr, "saving failed"};
        }

        Sexp::List lst;
        lst.add_prop(":temp",  Sexp::make_string(path));
        lst.add_prop(":what",  Sexp::make_string(what));
        lst.add_prop(":docid", Sexp::make_number(docid));

        if (!param.empty())
                lst.add_prop(":param", Sexp::make_string(param));

        g_free(path);
        return lst;
}



/* 'extract' extracts some mime part from a message */
void
Server::Private::extract_handler (const Parameters& params)
{
        const auto docid{get_int_or(params, ":docid")};
        const auto index{get_int_or(params, ":index")};
        const auto opts{message_options(params)};

        auto msg{store().find_message(docid)};
        if (!msg)
                throw Error{Error::Code::Store, "failed to get message"};

        try {
                const auto action{get_symbol_or(params, ":action")};
                if (action == "save")
                        output_sexp(save_part (msg, docid, index, opts, params));
                else if (action == "open")
                        output_sexp(open_part (msg, docid, index, opts));
                else if (action == "temp")
                        output_sexp(temp_part (msg, docid, index, opts, params));
                else {
                        throw Error{Error::Code::InvalidArgument,
                                        "unknown action '%s'", action.c_str()};
                }

        } catch (...) {
                mu_msg_unref (msg);
                throw;
        }
}


/* get a *list* of all messages with the given message id */
static std::vector<Store::Id>
docids_for_msgid (const Query& q, const std::string& msgid, size_t max=100)
{
        if (msgid.size() > Store::MaxTermLength) {
                throw Error(Error::Code::InvalidArgument,
                                  "invalid message-id '%s'", msgid.c_str());
        }

        const auto xprefix{mu_msg_field_shortcut(MU_MSG_FIELD_ID_MSGID)};
        /*XXX this is a bit dodgy */
        auto tmp{g_ascii_strdown(msgid.c_str(), -1)};
        auto expr{g_strdup_printf("%c:%s", xprefix, tmp)};
        g_free(tmp);

        GError *gerr{};
        const auto res{q.run(expr, MU_MSG_FIELD_ID_NONE, QueryFlags::None, max)};
        g_free (expr);
        if (!res)
                throw Error(Error::Code::Store, &gerr, "failed to run msgid-query");
        else if (res->empty())
                throw Error(Error::Code::NotFound,
                            "could not find message(s) for msgid %s", msgid.c_str());

        std::vector<Store::Id> docids{};
        for(auto&& mi: *res)
                docids.emplace_back(mi.doc_id());

        return docids;
}

/*
 * creating a message object just to get a path seems a bit excessive maybe
 * mu_store_get_path could be added if this turns out to be a problem
 */
static std::string
path_from_docid (const Store& store, unsigned docid)
{
        auto msg{store.find_message(docid)};
        if (!msg)
                throw Error(Error::Code::Store, "could not get message from store");

        auto p{mu_msg_get_path(msg)};
        if (!p) {
                mu_msg_unref(msg);
                throw Error(Error::Code::Store,
                            "could not get path for message %u", docid);
        }

        std::string msgpath{p};
        mu_msg_unref (msg);

        return msgpath;
}


static std::vector<Store::Id>
determine_docids (const Query& q, const Parameters& params)
{
        auto docid{get_int_or(params, ":docid", 0)};
        const auto msgid{get_string_or(params, ":msgid")};

        if ((docid == 0) == msgid.empty())
                throw Error(Error::Code::InvalidArgument,
                            "precisely one of docid and msgid must be specified");

        if (docid != 0)
                return { (unsigned)docid };
        else
                return docids_for_msgid (q, msgid.c_str());
}


size_t
Server::Private::output_sexp (const QueryResults& qres)
{
        size_t n{};
        for (auto&& mi: qres) {
                ++n;
                auto msg{mi.floating_msg()};
                if (!msg)
                        continue;

                auto qm{mi.query_match()};
                output_sexp(build_message_sexp(msg, mi.doc_id(),
                                               qm, MU_MSG_OPTION_HEADERS_ONLY));
        }

        return n;
}


void
Server::Private::find_handler (const Parameters& params)
{
        const auto q{get_string_or(params,             ":query")};
        const auto threads{get_bool_or(params,         ":threads", false)};
        const auto sortfieldstr{get_symbol_or(params,  ":sortfield", "")};
        const auto descending{get_bool_or(params,      ":descending", false)};
        const auto maxnum{get_int_or(params,           ":maxnum", -1/*unlimited*/)};
        const auto skip_dups{get_bool_or(params,       ":skip-dups", false)};
        const auto include_related{get_bool_or(params, ":include-related", false)};

        MuMsgFieldId sort_field{MU_MSG_FIELD_ID_NONE};
        if (!sortfieldstr.empty()) {
                sort_field = mu_msg_field_id_from_name (
                        sortfieldstr.c_str() + 1, FALSE); // skip ':'
                if (sort_field == MU_MSG_FIELD_ID_NONE)
                        throw Error{Error::Code::InvalidArgument, "invalid sort field %s",
                                        sortfieldstr.c_str()};
        }

        auto qflags{QueryFlags::None};
        if (descending)
                qflags |= QueryFlags::Descending;
        if (skip_dups)
                qflags |= QueryFlags::SkipDuplicates;
        if (include_related)
                qflags |= QueryFlags::IncludeRelated;
        if (threads)
                qflags |= QueryFlags::Threading;

        auto qres{query().run(q, sort_field, qflags, maxnum)};
        if (!qres)
                throw Error(Error::Code::Query, "failed to run query");

        /* before sending new results, send an 'erase' message, so the frontend
         * knows it should erase the headers buffer. this will ensure that the
         * output of two finds will not be mixed. */
        {
                Sexp::List lst;
                lst.add_prop(":erase", Sexp::make_symbol("t"));
                output_sexp(std::move(lst));
        }

        {
                const auto foundnum{output_sexp (*qres)};
                Sexp::List lst;
                lst.add_prop(":found", Sexp::make_number(foundnum));
                output_sexp(std::move(lst));
        }
}

void
Server::Private::help_handler (const Parameters& params)
{
        const auto command{get_symbol_or(params, ":command", "")};
        const auto full{get_bool_or(params, ":full", !command.empty())};

        if (command.empty()) {
                std::cout << ";; Commands are s-expressions of the form\n"
                          << ";;   (<command-name> :param1 val1 :param2 val2 ...)\n"
                          << ";; For instance:\n;;  (help :command quit)\n"
                          << ";; to get detailed information about the 'quit'\n;;\n";
                std::cout << ";; The following commands are available:\n\n";
        }

        std::vector<std::string> names;
        for (auto&& name_cmd: command_map())
                names.emplace_back(name_cmd.first);
        std::sort(names.begin(), names.end());

        for (auto&& name: names) {
                const auto& info{command_map().find(name)->second};

                if (!command.empty() && name != command)
                        continue;

                if (!command.empty())
                        std::cout << ";;   " << format("%-10s -- %s\n", name.c_str(),
                                                       info.docstring.c_str());
                else
                        std::cout << ";;  " << name.c_str() << " -- "
                                  << info.docstring.c_str() << '\n';
                if (!full)
                        continue;

                for (auto&& argname: info.sorted_argnames()) {
                        const auto& arg{info.args.find(argname)};
                        std::cout << ";;        "
                                  << format("%-17s  : %-24s ", arg->first.c_str(),
                                            to_string(arg->second).c_str());
                        std::cout << "  " << arg->second.docstring << "\n";
                }
                std::cout << ";;\n";
        }
}

static Sexp::List
get_stats (const Indexer::Progress& stats, const std::string& state)
{
        Sexp::List lst;

        lst.add_prop(":info",       Sexp::make_symbol("index"));
        lst.add_prop(":status",     Sexp::make_symbol(std::string{state}));
        lst.add_prop(":processed",  Sexp::make_number(stats.processed));
        lst.add_prop(":updated",    Sexp::make_number(stats.updated));
        lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed));

        return lst;
}

void
Server::Private::index_handler (const Parameters& params)
{
        Mu::Indexer::Config conf{};
        conf.cleanup    = get_bool_or(params, ":cleanup");
        conf.lazy_check = get_bool_or(params, ":lazy-check");
	// ignore .noupdate with an empty store.
	conf.ignore_noupdate = store().empty();

        indexer().stop();

        indexer().start(conf);
        while (indexer().is_running()) {
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
                output_sexp(get_stats(indexer().progress(), "running"));
        }
        output_sexp(get_stats(indexer().progress(), "complete"));
}

void
Server::Private::mkdir_handler (const Parameters& params)
{
        const auto path{get_string_or(params, ":path")};

        GError *gerr{};
        if (!mu_maildir_mkdir(path.c_str(), 0755, FALSE, &gerr))
                throw Error{Error::Code::File, &gerr, "failed to create maildir"};

        Sexp::List lst;
        lst.add_prop(":info",    Sexp::make_string("mkdir"));
        lst.add_prop(":message", Sexp::make_string(format("%s has been created",
                                                          path.c_str())));

        output_sexp(std::move(lst));
}


static MuFlags
get_flags (const std::string& path, const std::string& flagstr)
{
        if (flagstr.empty())
                return MU_FLAG_NONE; /* ie., ignore flags */
        else {
                /* if there's a '+' or '-' sign in the string, it must
                 * be a flag-delta */
                if (strstr (flagstr.c_str(), "+") || strstr (flagstr.c_str(), "-")) {
                        auto oldflags = mu_maildir_get_flags_from_path (path.c_str());
                        return mu_flags_from_str_delta (flagstr.c_str(), oldflags, MU_FLAG_TYPE_ANY);
                } else
                        return  mu_flags_from_str (flagstr.c_str(), MU_FLAG_TYPE_ANY,
                                                   TRUE /*ignore invalid*/);
        }
}

Sexp::List
Server::Private::perform_move (Store::Id docid, MuMsg *msg, const std::string& maildirarg,
                               MuFlags flags, bool new_name, bool no_view)
{
        bool different_mdir{};
        auto maildir{maildirarg};
        if (maildir.empty()) {
                maildir = mu_msg_get_maildir (msg);
                different_mdir = false;
        } else /* are we moving to a different mdir, or is it just flags? */
                different_mdir = maildir != mu_msg_get_maildir(msg);

        GError* gerr{};
        if (!mu_msg_move_to_maildir (msg, maildir.c_str(), flags, TRUE, new_name, &gerr))
                throw Error{Error::Code::File, &gerr, "failed to move message"};

        /* after mu_msg_move_to_maildir, path will be the *new* path, and flags and maildir fields
         * will be updated as wel */
        if (!store_.update_message (msg, docid))
                throw Error{Error::Code::Store, "failed to store updated message"};

        Sexp::List seq;
        seq.add_prop(":update", build_message_sexp (msg, docid, {}, MU_MSG_OPTION_VERIFY));
        /* note, the :move t thing is a hint to the frontend that it
         * could remove the particular header */
        if (different_mdir)
                seq.add_prop(":move", Sexp::make_symbol("t"));
        if (!no_view)
                seq.add_prop(":maybe-view", Sexp::make_symbol("t"));

        return seq;
}

Sexp::List
Server::Private::move_docid (Store::Id docid, const std::string& flagstr,
                             bool new_name, bool no_view)
{
        if (docid == Store::InvalidId)
                throw Error{Error::Code::InvalidArgument, "invalid docid"};

        auto msg{store_.find_message(docid)};
        try {
                if (!msg)
                        throw Error{Error::Code::Store, "failed to get message from store"};

                const auto flags = flagstr.empty() ? mu_msg_get_flags (msg) :
                        get_flags (mu_msg_get_path(msg), flagstr);
                if (flags == MU_FLAG_INVALID)
                        throw Error{Error::Code::InvalidArgument, "invalid flags '%s'",
                                        flagstr.c_str()};

                auto lst = perform_move(docid, msg, "", flags, new_name, no_view);
                mu_msg_unref (msg);
                return lst;

        } catch (...) {
                if (msg)
                        mu_msg_unref (msg);
                throw;
        }
}

/*
 * 'move' moves a message to a different maildir and/or changes its
 * flags. parameters are *either* a 'docid:' or 'msgid:' pointing to
 * the message, a 'maildir:' for the target maildir, and a 'flags:'
 * parameter for the new flags.
 *
 * returns an (:update <new-msg-sexp>)
 *
 */
void
Server::Private::move_handler (const Parameters& params)
{
        auto maildir{get_string_or(params,       ":maildir")};
        const auto flagstr{get_string_or(params, ":flags")};
        const auto rename{get_bool_or (params,   ":rename")};
        const auto no_view{get_bool_or (params,  ":noupdate")};
        const auto docids{determine_docids (query(), params)};

        if (docids.size() > 1) {
                if (!maildir.empty()) // ie. duplicate message-ids.
                        throw Mu::Error{Error::Code::Store,
                                        "can't move multiple messages at the same time"};
                // multi.
                for (auto&& docid: docids)
                        output_sexp(move_docid(docid, flagstr, rename, no_view));
                return;
        }
        auto docid{docids.at(0)};

        GError *gerr{};
        auto msg{store().find_message(docid)};
        if (!msg)
                throw Error{Error::Code::InvalidArgument, &gerr,
                                "could not create message"};

        /* if maildir was not specified, take the current one */
        if (maildir.empty())
                maildir = mu_msg_get_maildir (msg);

        /* determine the real target flags, which come from the flags-parameter
         * we received (ie., flagstr), if any, plus the existing message
         * flags. */
        MuFlags flags{};
        if (!flagstr.empty())
                flags = get_flags (mu_msg_get_path(msg), flagstr.c_str());
        else
                flags = mu_msg_get_flags (msg);

        if (flags == MU_FLAG_INVALID) {
                mu_msg_unref(msg);
                throw Error{Error::Code::InvalidArgument, "invalid flags"};
        }

        try {
                output_sexp(perform_move(docid, msg, maildir, flags,
                                         rename, no_view));
        } catch (...) {
                mu_msg_unref(msg);
                throw;
        }

        mu_msg_unref(msg);
}

void
Server::Private::ping_handler (const Parameters& params)
{
        const auto storecount{store().size()};
        if (storecount == (unsigned)-1)
                throw Error{Error::Code::Store, "failed to read store"};

        const auto queries  = get_string_vec (params, ":queries");
        Sexp::List qresults;
        for (auto&& q: queries) {

                const auto count{query().count(q)};
                const auto unreadq{format("flag:unread AND (%s)", q.c_str())};
                const auto unread{query().count(unreadq)};

                Sexp::List lst;
                lst.add_prop(":query",  Sexp::make_string(q));
                lst.add_prop(":count",  Sexp::make_number(count));
                lst.add_prop(":unread", Sexp::make_number(unread));

                qresults.add(Sexp::make_list(std::move(lst)));
        }

        Sexp::List addrs;
        for (auto&& addr: store().metadata().personal_addresses)
                addrs.add(Sexp::make_string(addr));

        Sexp::List lst;
        lst.add_prop(":pong", Sexp::make_string("mu"));

        Sexp::List proplst;
        proplst.add_prop(":version",            Sexp::make_string(VERSION));
        proplst.add_prop(":personal-addresses", Sexp::make_list(std::move(addrs)));
        proplst.add_prop(":database-path",
                         Sexp::make_string(store().metadata().database_path));
        proplst.add_prop(":root-maildir",
                         Sexp::make_string(store().metadata().root_maildir));
        proplst.add_prop(":doccount",           Sexp::make_number(storecount));
        proplst.add_prop(":queries",            Sexp::make_list(std::move(qresults)));

        lst.add_prop(":props",   Sexp::make_list(std::move(proplst)));

        output_sexp(std::move(lst));
}

void
Server::Private::quit_handler (const Parameters& params)
{
        keep_going_ = false;
}


void
Server::Private::remove_handler (const Parameters& params)
{
        const auto docid{get_int_or(params, ":docid")};
        const auto path{path_from_docid (store(), docid)};

        if (::unlink (path.c_str()) != 0 && errno != ENOENT)
                throw Error(Error::Code::File, "could not delete %s: %s",
                                  path.c_str(), g_strerror (errno));

        if (!store().remove_message (path))
                g_warning("failed to remove message @ %s (%d) from store",
                          path.c_str(), docid);
        // act as if it worked.

        Sexp::List lst;
        lst.add_prop(":remove", Sexp::make_number(docid));

        output_sexp(std::move(lst));
}


void
Server::Private::sent_handler (const Parameters& params)
{
        const auto path{get_string_or(params, ":path")};
        const auto docid{store().add_message(path)};
        if (docid == Store::InvalidId)
                throw Error{Error::Code::Store, "failed to add path"};

        Sexp::List lst;
        lst.add_prop (":sent",  Sexp::make_symbol("t"));
        lst.add_prop (":path",  Sexp::make_string(path));
        lst.add_prop (":docid", Sexp::make_number(docid));

        output_sexp (std::move(lst));
}


bool
Server::Private::maybe_mark_as_read (MuMsg *msg, Store::Id docid)
{
        if (!msg)
                throw Error{Error::Code::Store, "missing message"};

        const auto oldflags{mu_msg_get_flags (msg)};
        const auto newflags{get_flags (mu_msg_get_path(msg), "+S-u-N")};
        if (oldflags == newflags)
                return false; // nothing to do.

        GError* gerr{};
        if (!mu_msg_move_to_maildir (msg,
                                     mu_msg_get_maildir (msg),
                                     newflags,
                                     TRUE,
                                     FALSE,/*new_name,*/
                                     &gerr))
                throw Error{Error::Code::File, &gerr, "failed to move message"};

        /* after mu_msg_move_to_maildir, path will be the *new* path, and flags and maildir fields
         * will be updated as wel */
        if (!store().update_message (msg, docid))
                throw Error{Error::Code::Store, "failed to store updated message"};

        /* send an update */
        Sexp::List update;
        update.add_prop(":update", build_message_sexp(msg, docid, {}, MU_MSG_OPTION_NONE));
        output_sexp(Sexp::make_list(std::move(update)));

        g_debug ("marked message %d as read => %s", docid, mu_msg_get_path(msg));

        return true;
}


bool
Server::Private::maybe_mark_msgid_as_read (const  Mu::Query& query, const char* msgid)
{
        if (!msgid)
                return false; // nothing to do.

        const auto docids{docids_for_msgid(query, std::string{msgid})};
        for (auto&& docid: docids) {
                MuMsg *msg = store().find_message(docid);
                if (!msg)
                        continue;
                try {
                        maybe_mark_as_read(msg, docid);
                } catch (...) {
                        mu_msg_unref(msg);
                        throw;
                }
        }

        return true;
}

void
Server::Private::view_handler (const Parameters& params)
{
        Store::Id docid{Store::InvalidId};
        const auto path{get_string_or(params,       ":path")};
        const auto mark_as_read{get_bool_or(params, ":mark-as-read")};

        GError *gerr{};
        MuMsg *msg{};

        if (!path.empty()) { /* only use for old view (embedded msgs) */
                docid = Store::InvalidId;
                msg   = mu_msg_new_from_file (path.c_str(), NULL, &gerr);
        } else {
                docid = determine_docids(query(), params).at(0);
                msg   = store().find_message(docid);
        }

        if (!msg)
                throw Error{Error::Code::Store, &gerr,
                        "failed to find message for view"};

        if (mark_as_read) {
                // maybe mark the main message as read.
                maybe_mark_as_read(msg, docid);
                /* maybe mark _all_ messsage with same message-id as read */
                maybe_mark_msgid_as_read(query(), mu_msg_get_msgid(msg));
        }

        Sexp::List seq;
        seq.add_prop(":view", build_message_sexp(
                             msg, docid, {}, message_options(params)));
        mu_msg_unref(msg);
        output_sexp (std::move(seq));
}

Server::Server(Store& store, Server::Output output):
        priv_{std::make_unique<Private>(store, output)}
{}

Server::~Server() = default;

bool
Server::invoke (const std::string& expr) noexcept
{
        return priv_->invoke(expr);
}
