// SPDX-FileCopyrightText: 2026 fuddlesworth
// SPDX-License-Identifier: LGPL-2.1-or-later

#pragma once

#include <phosphorconfig_export.h>

#include <QColor>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QString>
#include <QStringList>

#include <memory>

namespace PhosphorConfig {

class IGroupPathResolver;
struct Schema;

/// Scoped view into a single configuration group.
///
/// Obtained from IBackend::group() as a unique_ptr. Implementations may
/// restrict the number of concurrently live groups per backend (the bundled
/// JsonBackend enforces one-at-a-time — destroy the unique_ptr before asking
/// the backend for another group).
///
/// Groups support dot-path nesting: "Snapping.Behavior.ZoneSpan" addresses
/// a nested object tree in JsonBackend. Consumers that need other naming
/// semantics (e.g.
/// per-screen overrides keyed by "Prefix:ScreenId") can plug in an
/// IGroupPathResolver on the backend to intercept and rewrite group names
/// before they reach storage.
class PHOSPHORCONFIG_EXPORT IGroup
{
public:
    virtual ~IGroup() = default;

    // Typed reads — return defaultValue when the key is absent, present-but-wrong-type,
    // or otherwise unparseable. Concrete backends document the exact coercion rules.
    virtual QString readString(const QString& key, const QString& defaultValue = {}) const = 0;
    virtual int readInt(const QString& key, int defaultValue = 0) const = 0;
    virtual bool readBool(const QString& key, bool defaultValue = false) const = 0;
    virtual double readDouble(const QString& key, double defaultValue = 0.0) const = 0;
    virtual QColor readColor(const QString& key, const QColor& defaultValue = {}) const = 0;

    /// Write a string. Stored verbatim — no content-dependent reinterpretation.
    ///
    /// Callers that need to persist structured data (array/object) should go
    /// through @c writeJson, which preserves the native JSON type where the
    /// backend supports it (JsonBackend stores as a native array/object; a
    /// backend over a flat key/value store stringifies).
    virtual void writeString(const QString& key, const QString& value) = 0;
    virtual void writeInt(const QString& key, int value) = 0;
    virtual void writeBool(const QString& key, bool value) = 0;
    virtual void writeDouble(const QString& key, double value) = 0;
    virtual void writeColor(const QString& key, const QColor& value) = 0;

    /// Write a structured JSON value (array/object/scalar) natively where
    /// supported, or as a compact-JSON string for backends without a native
    /// representation.
    ///
    /// The default implementation serializes every value — including strings
    /// — to its compact JSON form before storing, so readJson can round-trip
    /// it by re-parsing. Strings land on disk quoted (@c "hello", not
    /// @c hello); consumers that need the raw string should go through
    /// @c writeString / @c readString directly. JsonBackend overrides this
    /// to keep the value as a native JSON node on disk.
    virtual void writeJson(const QString& key, const QJsonValue& value)
    {
        // QJsonDocument only accepts arrays/objects at the root. For scalar
        // values (including strings, so writeJson/readJson round-trip
        // correctly via a single uniform encoding) wrap in a one-element
        // array and strip the brackets; for arrays/objects emit the compact
        // form directly.
        if (value.isArray()) {
            writeString(key, QString::fromUtf8(QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact)));
            return;
        }
        if (value.isObject()) {
            writeString(key, QString::fromUtf8(QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact)));
            return;
        }
        QJsonArray wrapped;
        wrapped.append(value);
        const QByteArray raw = QJsonDocument(wrapped).toJson(QJsonDocument::Compact);
        writeString(key, QString::fromUtf8(raw.mid(1, raw.size() - 2)));
    }

    /// Read a structured JSON value. Returns @c defaultValue when the key
    /// is absent or unparseable.
    ///
    /// The default implementation calls @c readString and parses the result.
    /// Uses @c hasKey (not string emptiness) to distinguish an absent key
    /// from a present empty-string, so empty-string values written through
    /// @c writeJson round-trip correctly. JsonBackend overrides to return
    /// the native JSON node directly without a string round-trip.
    virtual QJsonValue readJson(const QString& key, const QJsonValue& defaultValue = {}) const
    {
        if (!hasKey(key)) {
            return defaultValue;
        }
        const QString raw = readString(key);
        // Wrap so QJsonDocument::fromJson accepts bare scalars.
        const QByteArray wrapped = QByteArray("[") + raw.toUtf8() + QByteArray("]");
        QJsonParseError err;
        const QJsonDocument doc = QJsonDocument::fromJson(wrapped, &err);
        if (err.error != QJsonParseError::NoError) {
            return defaultValue;
        }
        const QJsonArray arr = doc.array();
        if (arr.size() != 1) {
            return defaultValue;
        }
        return arr.at(0);
    }

    // Key management.
    virtual bool hasKey(const QString& key) const = 0;
    virtual void deleteKey(const QString& key) = 0;

    /// Enumerate scalar leaf keys directly under this group. Nested sub-groups
    /// (dot-path children in JsonBackend) are NOT included — consumers that
    /// want to purge stale keys without touching declared descendants can
    /// iterate this list against the schema. Order is implementation-defined.
    virtual QStringList keyList() const = 0;

    IGroup(const IGroup&) = delete;
    IGroup& operator=(const IGroup&) = delete;

protected:
    IGroup() = default;
};

/// Pluggable configuration backend.
///
/// Provides group-based access, persistence, and group enumeration.
/// Concrete implementation: JsonBackend (atomic-write JSON file). Legacy INI
/// files are not a backend — they are read once, through
/// @c QSettingsBackend::readConfigFromDisk, by a migration chain.
///
/// Root-level ("ungrouped") access is provided via readRootString /
/// writeRootString. How a backend stores root keys is
/// implementation-defined: JsonBackend keeps them under a configurable
/// root-group name (defaults to "General"), mirroring the [General] section
/// an INI file would use.
class PHOSPHORCONFIG_EXPORT IBackend
{
public:
    virtual ~IBackend() = default;

    /// Return a scoped view into the named group. Caller owns the pointer;
    /// destroy it before asking for another group on this backend.
    virtual std::unique_ptr<IGroup> group(const QString& name) = 0;

    /// Whether the in-memory document carries changes not yet committed to
    /// disk. A clean backend may be re-read from disk without losing state;
    /// a dirty one may not. The default @c true is deliberate: a backend
    /// that cannot answer must report dirty, so callers treating "clean" as
    /// the licence to reload never discard state they cannot see.
    virtual bool isDirty() const
    {
        return true;
    }

    /// Re-read configuration from disk, discarding any pending in-memory
    /// changes. Must not be called while a group view is live.
    ///
    /// Discarding is load-bearing, not incidental: callers use this to roll
    /// back after a failed write, so an implementation that instead FLUSHED
    /// the pending changes would persist the very mutations the caller is
    /// asking it to drop. A store that cannot drop unsaved writes therefore
    /// cannot implement IBackend — which is why @c QSettings, whose
    /// destructor flushes and which offers no discard API, is confined to the
    /// read-only @c QSettingsBackend::readConfigFromDisk helper.
    virtual void reparseConfiguration() = 0;

    /// Request that pending writes reach disk. No-op when nothing is dirty.
    /// Returns @c true on success (or when there was nothing to flush),
    /// @c false on an I/O error — backends log the reason before returning.
    ///
    /// @c true means "accepted", NOT "committed": a backend is free to satisfy
    /// this by scheduling the write (@c JsonBackend's Deferred sync policy
    /// debounces it), in which case the write has not happened yet and may
    /// still fail. Callers that must know the bytes landed — anything moving a
    /// baseline or reporting success to a user — call @c commit() instead.
    virtual bool sync() = 0;

    /// Flush pending writes to disk NOW and report whether they landed.
    /// Returns @c true when disk holds the current in-memory state on return
    /// (including when there was nothing to flush), @c false on an I/O error.
    /// Unlike @c sync() this never defers, so a @c true result is a commitment
    /// the caller can key state off.
    ///
    /// The default forwards to @c sync(), which is correct for any backend
    /// whose @c sync() already writes inline. Backends that can defer must
    /// override it to bypass their deferral.
    virtual bool commit()
    {
        return sync();
    }

    /// Delete an entire group and everything inside it. Intermediate
    /// parents are pruned if they become empty (dot-path groups only).
    virtual void deleteGroup(const QString& name) = 0;

    /// Read/write ungrouped (root-level) keys.
    virtual QString readRootString(const QString& key, const QString& defaultValue = {}) const = 0;
    virtual void writeRootString(const QString& key, const QString& value) = 0;
    virtual void removeRootKey(const QString& key) = 0;

    /// Enumerate every top-level group name. Dot-path groups are returned
    /// with their full path ("Snapping", "Snapping.Behavior", ...).
    /// Groups produced by a plugged-in IGroupPathResolver appear in the
    /// resolver's preferred external form.
    ///
    /// CONTRACT — reserved names. Whatever container an implementation uses to
    /// hold the ungrouped keys that @c readRootString / @c writeRootString /
    /// @c removeRootKey address must NOT appear in this list, nor may any of
    /// its descendants. Ungrouped keys are not addressable through @c group()
    /// and no caller can tell them apart from a real group by name, so a
    /// consumer that sweeps @c groupList() and calls @c deleteGroup on every
    /// name it does not recognise would wipe them. Enforcing it here rather
    /// than asking every consumer to know each backend's container name is what
    /// makes such a sweep safe.
    ///
    /// The same applies to any root key an @c IGroupPathResolver reserves: the
    /// resolver owns that subtree exclusively and enumerates it itself.
    ///
    /// Implementations: @c JsonBackend filters its @c rootGroupName (and the
    /// version stamp, and the resolver's reserved roots) out of the
    /// enumeration.
    virtual QStringList groupList() const = 0;

    // ── Schema-aware hooks (default no-op) ────────────────────────────────
    // Store consults these to wire a schema's resolver, version stamp, and
    // migration chain into the backend. Backends without a JSON-root concept
    // (in-memory test mocks) inherit the default no-ops; JsonBackend overrides.
    // Keeping the hooks on IBackend lets Store work against the polymorphic
    // interface instead of a dynamic_cast, and lets a future backend opt in
    // to schema participation by providing its own overrides.

    /// Install a group-path resolver. The caller shares ownership via
    /// shared_ptr so multiple Stores/backends can reference the same
    /// instance. Backends that have no resolver concept return without
    /// storing the pointer.
    virtual void setPathResolver(std::shared_ptr<IGroupPathResolver>)
    {
    }
    /// Currently-installed resolver, or @c nullptr when none is set (also
    /// the default for backends that ignore @c setPathResolver).
    virtual std::shared_ptr<IGroupPathResolver> pathResolver() const
    {
        return nullptr;
    }

    /// Install a "stamp on first sync" version annotation: if @p key is
    /// absent from the backend's root on the next flush, write
    /// @p key = @p version. Backends without a stamp concept ignore the
    /// call. Passing an empty key disables the stamp.
    virtual void setVersionStamp(const QString&, int)
    {
    }
    /// Currently-installed version stamp as @c {key, version}. Empty key
    /// means "no stamp installed" (also the default for backends that
    /// ignore @c setVersionStamp).
    virtual std::pair<QString, int> versionStamp() const
    {
        return {};
    }

    /// Apply @p schema's migration chain against the backend's in-memory
    /// state and commit any resulting version bump. Returns @c true when the
    /// backend supports schema migrations (even if no step applied — e.g.
    /// the on-disk version already matches). Returns @c false when the
    /// backend has no concept of a JSON-root snapshot; callers should log
    /// that declared migrations will not run. Default: no-op returning
    /// @c false.
    virtual bool applyMigration(const Schema&)
    {
        return false;
    }

    IBackend(const IBackend&) = delete;
    IBackend& operator=(const IBackend&) = delete;

protected:
    IBackend() = default;
};

} // namespace PhosphorConfig
