/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * 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.
 */

#pragma once

#include <QObject>
#include <QStringList>
#include <QVector>

#include "types.h"

struct _notmuch_database;
typedef struct _notmuch_database notmuch_database_t;

/// Owns the only notmuch database handle in the process.
///
/// libnotmuch is not thread-safe and queries over a large database block, so
/// this object lives on its own thread and the UI reaches it only through
/// queued signals. No notmuch pointer ever leaves this class.
class NotmuchWorker : public QObject
{
    Q_OBJECT
public:
    /// notmuchConfigPath may be empty, in which case notmuch resolves its own
    /// config and therefore its own database.path.
    explicit NotmuchWorker(const QString &notmuchConfigPath, QObject *parent = nullptr);
    ~NotmuchWorker() override;

    /// Threads emitted per threadsReady() signal.
    static constexpr int kBatchSize = 200;

    /// The sort orders offered to the user.
    ///
    /// Two, not four. notmuch also has NOTMUCH_SORT_MESSAGE_ID and
    /// NOTMUCH_SORT_UNSORTED, and neither is an order a human wants. Sorting
    /// by sender or subject is deliberately absent: notmuch cannot do it, so
    /// the model would have to sort after results arrive, which fights the
    /// batching that makes a 10k-thread query paint immediately.
    enum SortOrder {
        NewestFirst,
        OldestFirst,
    };
    Q_ENUM(SortOrder)

public slots:
    /// Runs a query. generation lets the UI discard results from a superseded
    /// query without the worker needing to know about cancellation.
    void runQuery(const QString &query, quint64 generation,
                  SortOrder sort = NewestFirst);

    /// Loads the messages of one thread, oldest first. matchQuery is the
    /// user's current query; messages matching it render expanded, the rest
    /// as stubs.
    void loadThread(const QString &threadId, const QString &matchQuery,
                    quint64 generation);

    /// Loads a thread as a reply TREE, for the message rows in the list.
    ///
    /// Separate from loadThread rather than replacing it, for a reason that is
    /// not stylistic: loadThread walks notmuch_query_search_messages, and a
    /// message obtained that way returns NULL from
    /// notmuch_message_get_replies (notmuch.h:1617-1628), so that walk cannot
    /// produce reply depth at all. The tree has to come from
    /// notmuch_thread_get_toplevel_messages instead. The message pane still
    /// wants the flat list; only the list wants the tree.
    ///
    /// matchQuery is accepted for signature symmetry with loadThread and is
    /// deliberately unused: see the comment on the walk in the .cpp.
    void loadThreadTree(const QString &threadId, const QString &matchQuery,
                        quint64 generation);

    /// Loads ONE message, for a message row selected in the list.
    ///
    /// Emits messageLoaded with an empty vector when the id is unknown, which
    /// is an ordinary race after a reindex rather than an error worth
    /// reporting.
    void loadMessage(const QString &messageId, quint64 generation);

    /// Applies tag changes. Opens the database read-write, applies, and closes
    /// immediately: notmuch's write lock is exclusive process-wide, so holding
    /// it would block the user's cron `notmuch new`.
    void applyTags(const TagChange &change);

    /// Batch tagging over whole threads. The UI holds thread ids, not message
    /// ids, for rows it has not opened, so the resolution happens here where
    /// the database handle lives. This is the path the archive/flag/delete
    /// actions use on a multi-row selection.
    void applyTagsToThreads(const QStringList &threadIds,
                            const QStringList &add,
                            const QStringList &remove,
                            const QString &description);

    /// Every tag in the database, sorted. Feeds query bar completion, which
    /// cannot offer tag names it has no way to enumerate. Called at startup,
    /// after a sync, and after a tag mutation introduces an unknown tag.
    void requestAllTags(quint64 generation);

    /// Thread counts for the placeholder pane's helper lines, one per query,
    /// answered in the order asked. Counts rather than results: the pane says
    /// how much there is, and clicking a line runs the query properly.
    ///
    /// Requested when the pane is about to go blank rather than kept fresh in
    /// the background. A count goes stale the moment a tag is edited, and
    /// refreshing one nobody is looking at is work for nothing.
    void requestCounts(const QStringList &queries, quint64 generation);

    /// Database-level facts for the Maildir overview (item 34): total messages,
    /// total threads, and the number of tags.
    ///
    /// **Messages, not threads**, which is what distinguishes this from
    /// requestCounts above. That one answers "how many rows will this query
    /// produce" and counts threads to match the list; this one describes the
    /// database, where the message total is the number a user means by "how
    /// much mail is in here".
    ///
    /// Counting every message is not free on a large database, so this is
    /// called when the dialog is opened and never on a timer.
    void requestDatabaseStats(quint64 generation);

signals:
    void threadsReady(const QVector<ThreadSummary> &threads, quint64 generation);
    void queryFinished(int totalThreads, quint64 generation);
    void threadLoaded(const QVector<MessageRef> &messages, quint64 generation);
    void threadTreeLoaded(const QVector<MessageNode> &nodes,
                          quint64 generation);
    void messageLoaded(const QVector<MessageRef> &messages, quint64 generation);
    void tagsApplied(const TagChange &change);
    void allTagsReady(const QStringList &tags, quint64 generation);

    /// One entry per requested query, in the order they were asked for. A query
    /// notmuch rejects yields -1 rather than dropping the entry, so the
    /// positional correspondence the caller relies on always holds.
    void countsReady(const QVector<int> &counts, quint64 generation);

    /// Fields left at -1 are ones notmuch could not answer, which the dialog
    /// renders as unknown rather than as zero.
    void databaseStatsReady(const DatabaseStats &stats, quint64 generation);

    void errorOccurred(const QString &message);

private:
    bool openReadOnly();
    void close();

    QByteArray configPathArg() const;

    QString m_configPath;
    notmuch_database_t *m_db = nullptr;
};
