#pragma once
#ifdef ESP_PLATFORM
#include <esp_ota_ops.h>
#endif
#include "ProtobufModule.h"
#include "meshUtils.h"
#include <sys/types.h>
#if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h"
#endif

/**
 * Datatype passed to Observers by AdminModule, to allow external handling of admin messages
 */
struct AdminModule_ObserverData {
    const meshtastic_AdminMessage *request;
    meshtastic_AdminMessage *response;
    AdminMessageHandleResult *result;
};

/**
 * Admin module for admin messages
 */
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<AdminModule_ObserverData *>
{
    friend class AdminModuleTestShim; // test/support/AdminModuleTestShim.h - native tests reach the private handlers/state

  public:
    /** Constructor
     * name is for debugging output
     */
    AdminModule();

  protected:
    /** Called to handle a particular incoming message

    @return true if you've guaranteed you've handled this message and no other handlers should be considered for it
    */
    virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *p) override;

  private:
    bool hasOpenEditTransaction = false;
    // Each deferred write restarts the clock, so this bounds the gap between writes, not the length
    // of the edit; a bulk import sends them milliseconds apart.
    static constexpr uint32_t EDIT_TRANSACTION_IDLE_MS = 60 * 1000;
    uint32_t editTransactionActivityMs = 0; // millis() of the last save this transaction deferred
    int deferredEditSegments = 0;           // segments that transaction has touched but not yet saved
    /// Retire an open edit transaction whose client stopped talking, persisting what it applied.
    void expireStaleEditTransaction();
#ifdef PIO_UNIT_TESTING
    int lastSaveWhatForTest = 0;
#endif

    uint8_t session_passkey[8] = {0};
    uint32_t session_time = 0;        // millis() when the current session passkey was issued
    bool sessionPasskeyValid = false; // separate flag: millis() 0 at boot is a valid issue time

    void saveChanges(int saveWhat, bool shouldReboot = true);

    /**
     * Getters
     *
     * Each of the NOINLINE ones below builds a whole meshtastic_AdminMessage (480 bytes) on the
     * stack. They are only ever reached from one case of handleReceivedProtobuf()'s switch, but
     * when the compiler inlines them the dispatcher's frame has to reserve a slot for every one of
     * them at once - measured at 3008 bytes on ESP32-S3, 37% of the 8 KB Arduino loopTask stack
     * that also has to carry PhoneAPI, the router, nanopb and LittleFS below it. Keeping them out
     * of line means only the request actually being served pays for its response buffer.
     * See issue #11237.
     */
    void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p);
    NOINLINE void handleGetOwner(const meshtastic_MeshPacket &req);
    NOINLINE void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType);
    NOINLINE void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType);
    NOINLINE void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex);
    NOINLINE void handleGetDeviceMetadata(const meshtastic_MeshPacket &req);
    NOINLINE void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req);
    NOINLINE void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req);
    NOINLINE void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req);
    /**
     * Setters
     */
    void handleSetOwner(const meshtastic_User &o);
    void handleSetChannel(const meshtastic_Channel &cc);

  protected:
    void handleSetConfig(const meshtastic_Config &c, bool fromOthers);

#ifdef PIO_UNIT_TESTING
  protected:
#else
  private:
#endif
    bool handleSetModuleConfig(const meshtastic_ModuleConfig &c);
    void handleSetChannel();

  public:
    void handleSetHamMode(const meshtastic_HamParameters &req);

    /// Note an admin request leaving this node for a remote, so that remote's response is
    /// accepted. Called from the client-to-mesh path (MeshService::handleToRadio).
    void noteOutgoingAdminRequest(const meshtastic_MeshPacket &p);

  private:
    // An admin response has no session passkey and its sender need not hold an admin key, so a
    // request we sent is the only thing vouching for it. Track each request independently (its own
    // expiry and pinned key) so a later one can't extend or relax an earlier one.
    static constexpr size_t kOutstandingAdminRequests = 8;
    static constexpr uint32_t kOutstandingAdminRequestMs = 300 * 1000; // same window as the session passkey
    struct OutstandingAdminRequest {
        NodeNum to;                 // 0 = free slot
        uint32_t requestId;         // our request's packet id; the response must echo it as request_id
        uint32_t sentAtMs;          // millis() when this request went out
        pb_size_t expectedResponse; // the one response variant this request authorizes
        uint8_t moduleConfigType;   // for get_module_config_request: which ModuleConfigType we asked
        uint8_t key[32];            // pinned destination key when the request goes out over PKC
        bool keyValid;
    };
    OutstandingAdminRequest outstandingAdminRequests[kOutstandingAdminRequests] = {};

    /// Whether a response (variant responseVariant, module-config subtype moduleConfigTag or 0)
    /// from mp.from answers a request we sent; consumes the matched request so it can't be replayed.
    bool responseIsSolicited(const meshtastic_MeshPacket &mp, pb_size_t responseVariant, pb_size_t moduleConfigTag);

    /// Offer an admin message we have no case for to the module API, and let observers (e.g. the UI)
    /// see every admin message. Both build a response on the stack, so like the getters above they
    /// stay out of line to keep handleReceivedProtobuf()'s frame small.
    NOINLINE void handleViaModuleApi(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r);
    NOINLINE void handleViaObservers(const meshtastic_AdminMessage *r);

    void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg);
    void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
    void reboot(int32_t seconds);

    void setPassKey(meshtastic_AdminMessage *res);
    bool checkPassKey(meshtastic_AdminMessage *res);

    bool messageIsResponse(const meshtastic_AdminMessage *r);
    bool messageIsRequest(const meshtastic_AdminMessage *r);
    void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3)));
    void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3)));
    void warnOnLoraPresetChange(const meshtastic_Config_LoRaConfig &oldLora, const meshtastic_Config_LoRaConfig &newLora);
    void warnOnChannelSet(const meshtastic_Channel &cc);

    // Channel-configuration warnings are coalesced into a single client notification.
    // queueChannelWarning() records one warning for a channel; while an edit transaction
    // is open (begin/commit_edit_settings) the warnings accumulate and flushChannelWarnings()
    // emits one combined message at commit. Outside a transaction the caller flushes
    // immediately, so a single channel/preset edit still produces a single message.
    void queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, const char *format, ...)
        __attribute__((format(printf, 5, 6)));
    // Emit the "licensed mode activated" notice, deferring to commit during an edit transaction
    // so repeated triggers (e.g. owner + several channels) produce a single message.
    void warnLicensedMode();
    void flushChannelWarnings();

    char pendingWarningText[250] = {};    // the lone queued message, used verbatim when only one fired
    uint32_t pendingWarningChannels = 0;  // bitmask of channel indices with a queued warning
    uint8_t pendingWarningCount = 0;      // number of queued warnings this transaction
    bool pendingWarningNameIssue = false; // any queued warning was about a channel name
    bool pendingWarningPskIssue = false;  // any queued warning was about a PSK
    bool pendingLicenseWarning = false;   // a licensed-mode notice is queued for this transaction
};

static constexpr const char *licensedModeMessage =
    "Licensed mode activated, removing admin channel and encryption from all channels";

static constexpr const char *licensedIdentityMigrationMessage =
    "Licensed signing requires an identity key; this node identity will change after key generation";

static constexpr const char *publicChannelPrecisionMessage =
    "Precise position is not allowed on a public (open / known-key) channel; reduced to coarse precision";

extern AdminModule *adminModule;

void disableBluetooth();
