/*
 * 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 <QDialog>
#include <QStringList>

#include "rulequery.h"
#include "tagrules.h"

class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QRadioButton;
class QScrollArea;
class QSplitter;
class QSpinBox;
class QTreeWidget;
class QTreeWidgetItem;
class QVBoxLayout;

/// Views and edits the shared auto-tagging rules.
///
/// Does NOT apply rules to existing mail. The counts are shown so a rule can be
/// judged before it runs, but the only thing that tags mail is the notmuch
/// post-new hook. Backfill is deliberately out of this version: a rule that is
/// safe against arrivals is not automatically safe unscoped, and the
/// confirmation story for a bulk write does not exist yet.
class TagRulesDialog : public QDialog
{
    Q_OBJECT

public:
    explicit TagRulesDialog(QWidget *parent = nullptr);

    /// Opens with one new rule already in the working list, selected, and the
    /// Add tags field focused. The rule is a pending edit like any other: it
    /// reaches the file on Save and is discarded on Cancel.
    ///
    /// The seed is a whole TagRule rather than a query string because item 78
    /// will seed from a sender and will want to set tags too.
    explicit TagRulesDialog(const TagRule &seed, QWidget *parent = nullptr);

    /// Appends `seed` to the working rules, selects it and focuses Add tags.
    /// Public because the dialog is single-instance and non-modal: a second
    /// Create tagging rule while it is open seeds the dialog already up
    /// rather than being dropped.
    void seedRule(const TagRule &seed);

    /// The queries whose message counts the dialog wants, in the order its
    /// rows appear. MainWindow hands these to the worker; the dialog never
    /// touches the database itself, because NotmuchWorker owns the only
    /// handle and notmuch permits one per process.
    QStringList countQueries() const;

    /// Maildir folder paths, for the Folder row's dropdown, relative to the
    /// database root. Supplied by the caller rather than read here: they come
    /// from a scan of the tree under notmuch's database.path, which only
    /// NotmuchWorker can answer for, and this dialog reaches neither it nor
    /// Config.
    ///
    /// Arrives AFTER the dialog is on screen, since the scan crosses to the
    /// worker on a queued call, so this refills the rows that already exist.
    void setFolders(const QStringList &folders);

    /// Test seams. The builder's state is otherwise reachable only through
    /// synthetic clicks on widgets whose geometry the offscreen platform does
    /// not guarantee.
    int rowCountForTest() const { return m_rows.size(); }
    QString queryLineForTest() const;

    /// Selects the rule at `index` as a click on the list would.
    void selectRuleForTest(int index);

    bool textModeForTest() const;
    void setRowValueForTest(int index, const QString &value);
    /// Runs the Save path without showing the dialog.
    void saveForTest() { onSave(); }

    /// Types into the query field and toggles the mode, so the refusal path
    /// can be reached without a synthetic click. The refusal reports through
    /// the warning label rather than a modal precisely so this is testable.
    void setQueryTextForTest(const QString &text);
    void setTextModeForTest(bool on);
    QString warningTextForTest() const;

    /// The warning's appearance and place, asserted as widget properties. A
    /// render probe cannot carry this: see "Rendering probes lie" in CLAUDE.md.
    QString warningStyleForTest() const;
    Qt::TextFormat warningTextFormatForTest() const;
    bool warningIsBelowTheRuleListForTest() const;

    /// Clicks the warning's dismiss button, so the test drives the same signal
    /// the user's click does rather than calling setWarning() behind it.
    void dismissWarningForTest();

    /// Types a name and commits it the way leaving the field does. The commit
    /// is the point: the sanitiser runs on editingFinished, so a test that
    /// only calls setText() asserts against a field nothing has processed.
    void setNameForTest(const QString &name);
    QString nameLineForTest() const;

    /// Adding and filling a rule the way the buttons do, so a test can drive
    /// the whole journey the user takes rather than only its last step.
    int ruleCountForTest() const;

    /// The selected rule's enabled flag, so the seeded default is asserted
    /// rather than assumed from TagRule's initialiser.
    bool currentRuleEnabledForTest() const;
    void addRuleForTest() { onAddRule(); }
    void setTagsForTest(const QString &tags);

    /// Whether the text-mode toggle would be on screen. A toggle that hides
    /// itself when switched on is a one-way trip, and asserting only on the
    /// checked STATE passes against that, since the state is still readable
    /// when the widget is not.
    bool textModeToggleIsReachableForTest() const;

    /// Width of one rule-list column. The geometry restore is asserted on the
    /// saved and reread VALUE rather than on the resulting frame: item 46
    /// records that the offscreen platform does not honour a window resize,
    /// so a test comparing frames there passes or fails for reasons that have
    /// nothing to do with the code.
    int columnWidthForTest(int column) const;
    void setColumnWidthForTest(int column, int width);

    /// The text and tooltip of one cell of the rule list, by top-level row.
    ///
    /// Reads the LIST rather than the editor form, which is the distinction
    /// item 102 is about: the note was always in the form and invisible until
    /// a rule was selected.
    QString listCellTextForTest(int row, int column) const;
    QString listCellToolTipForTest(int row, int column) const;

    /// The header labels, so a test can assert where a column landed without
    /// hardcoding the enum it is checking.
    QStringList listHeaderLabelsForTest() const;

    /// Repopulates the rule list, as adding or deleting a rule does. Exposed
    /// because a restored column width has to survive one of these, not only
    /// a close and reopen: `resizeColumnToContents` on every reload discarded
    /// the width the user had dragged.
    void reloadListForTest();

    /// Presses Preview, which is otherwise reachable only through a click on
    /// a button whose geometry the offscreen platform does not guarantee.
    void previewForTest();

    /// The height the condition-row editor asks for. This is what squeezed
    /// the rule list: a stretch factor only shares out space ABOVE each
    /// widget's minimum, so every row added here came out of the list.
    /// Measured at 120px for one row and 414px for eight before the scroll
    /// area capped it.
    ///
    /// Asserted on instead of the list's rendered height because the
    /// offscreen platform does not honour a window size, so the rendered
    /// height there is not evidence of anything (see CLAUDE.md). Note the
    /// size HINT, not minimumSizeHint: a QFormLayout's minimum does not track
    /// its rows and reads the same either way, which passed against the bug.
    int heightDemandedBelowListForTest() const;

    /// How tall the condition-row area may become. The scroll area caps it;
    /// without the cap the rows grow without bound and a long rule fills the
    /// window again, scrolling instead of squeezing. Asserted separately
    /// because removing the cap leaves heightDemandedBelowListForTest
    /// unchanged, so that measure alone does not cover it.
    int conditionAreaHeightForTest() const;

signals:
    /// Asks the owner to run countQueries() through the worker.
    void countsRequested();

    /// Asks the owner to run one rule's query in the main window, so the user
    /// can see WHICH mail a rule collects rather than how much.
    ///
    /// The query goes out exactly as stored: no `tag:new`, and no wrapping
    /// parentheses. The post-new hook adds both when it applies a rule, and a
    /// preview that copied them would match nothing outside a sync window,
    /// since `tag:new` is set only on mail that has just arrived.
    void previewRequested(const QString &query);

public slots:
    /// Corpus counts, positionally paired with countQueries().
    void setCounts(const QVector<int> &counts);

private slots:
    void onSelectionChanged();
    void onAddRule();
    void onCopyRule();
    void onDeleteRule();
    void applyEditsToCurrentRule();
    void onSave();

public slots:
    /// Saves the window's size and column widths on the way out. Overridden
    /// here rather than closeEvent because Save and Cancel do not send a
    /// close event at all, only the window manager's X button does.
    void done(int result) override;

private:
    void reloadList();
    void restoreUiState();
    void saveUiState();
    void showWarnings();

    /// The only way the warning label is written. An empty string hides it.
    /// One route in so the icon and the red styling cannot drift between the
    /// load path, the save refusal and the text-mode notice.
    void setWarning(const QString &text);
    int currentIndex() const;

    /// Writes one rule's summary onto its row. Shared by reloadList() and
    /// applyEditsToCurrentRule() so the two cannot render a rule differently.
    void fillItem(QTreeWidgetItem *item, const TagRule &rule) const;

    /// One builder row's widgets, so a row can be removed as a unit.
    struct Row
    {
        QWidget *container = nullptr;
        QComboBox *field = nullptr;
        QComboBox *op = nullptr;
        QLineEdit *value = nullptr;
        /// Shown in place of `value` for a Folder row. Never visible together.
        QComboBox *folder = nullptr;
    };

    /// Reads a row's value from whichever of its two widgets the field selects,
    /// so the read and write paths cannot disagree about where it lives.
    QString rowValue(const Row &row) const;
    void setRowValue(Row *row, const QString &value);

    Row *addRow(bool exclusion);
    void removeRow(bool exclusion, int index);
    void populateOperators(bool exclusion);
    void updateExclusionsVisibility();
    void syncQueryLine();

    void setTextMode(bool on);

    void rebuildRows(const RuleQuery &query);
    void applyTermToRow(Row *row, const RuleTerm &term);
    RuleQuery currentQueryFromRows() const;

    /// The query as parsed when the current rule was loaded. Task 10's save
    /// path compares against this to decide whether the stored string may be
    /// left alone.
    RuleQuery m_loadedQuery;

    QStringList m_folders;

    QList<Row> m_rows;
    QList<Row> m_exclusionRows;

    TagRules m_rules;
    QList<TagRule> m_working;   ///< Edited copy; written only on Save.

    /// True while reloadList() is repopulating the tree. The row it selects
    /// afterwards emits currentItemChanged, and onSelectionChanged() would
    /// then load a rule into the form while the form still holds the previous
    /// row's text, which applyEditsToCurrentRule() has no chance to flush.
    bool m_reloading = false;

    /// Each column is auto-sized to its contents ONCE, on its first fill.
    /// After that its width belongs to the user, whether it came from a
    /// restored header or from a drag, and resizeColumnToContents on every
    /// repopulate threw both away on the next add or delete. Two flags rather
    /// than one because the count column is filled later than the rest, by a
    /// reply from the worker.
    bool m_columnsSized = false;
    bool m_countColumnSized = false;

    QTreeWidget *m_list = nullptr;
    QWidget *m_warningBanner = nullptr;
    QPushButton *m_warningClose = nullptr;
    QLineEdit *m_id = nullptr;
    QLineEdit *m_add = nullptr;
    QLineEdit *m_remove = nullptr;
    QLineEdit *m_query = nullptr;
    QPlainTextEdit *m_note = nullptr;
    QSpinBox *m_stage = nullptr;
    QCheckBox *m_enabled = nullptr;
    QLabel *m_warningLabel = nullptr;
    QPushButton *m_saveButton = nullptr;

    QRadioButton *m_matchAll = nullptr;
    QRadioButton *m_matchAny = nullptr;
    QCheckBox *m_textMode = nullptr;
    QWidget *m_builder = nullptr;
    /// Scrolls the condition rows, so a rule with many of them cannot grow
    /// the editor without bound. Shown and hidden in place of m_builder for
    /// text mode: hiding the inner widget would leave an empty scroll area.
    QScrollArea *m_builderScroll = nullptr;
    /// Divides the rule list from the editor. The list had stretch 1 and was
    /// still squeezed, because a stretch factor only shares out space above
    /// each widget's minimum and the form's grew with every condition row.
    QSplitter *m_splitter = nullptr;
    QPushButton *m_previewButton = nullptr;
    QVBoxLayout *m_rowsLayout = nullptr;
    QVBoxLayout *m_exclusionsLayout = nullptr;
    QLabel *m_exclusionsHeader = nullptr;
    QPushButton *m_addExclusion = nullptr;
};
