/*******************************************************************************
 * Copyright (c) 2026, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#pragma once

#include <string>
#include <vector>
#include <deque>
#include <set>
#include <memory>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <atomic>
#include <utility>
#include <uuidp.h>

#include <paritypp/server.h>
#include <paritypp/client.h>
#include <paritypp/auth.h>

#include "session.h"
#include "backend.h"
#include "authdb.h"

namespace authdb {

    struct ClusterNode {
        std::string address;
        int         port;
    };

    // Session data uses store_id=1 on the parity server (multi-store).
    static constexpr uint8_t SESSION_STORE_ID = 1;

    struct ClusterConfig {
        std::vector<ClusterNode> peers;
        std::string              bind_address = "0.0.0.0";
        int                      port = 4433;
        std::string              cert_file;
        std::string              key_file;
        std::string              client_name;
        std::string              client_key;
        std::string              store_path;   // file-backed store directory (empty = memory only)
        size_t                   data_blocks  = 2;  // k: data shards
        size_t                   parity_blocks = 1; // m: parity shards (fault tolerance)
    };

    // --- Session serialization ---

    struct SessionBlock {
        static std::vector<uint8_t> serialize(const SessionData &sess);
        static bool deserialize(const std::vector<uint8_t> &data,
                                uuid::uuid &sid, uuid::uuid &uid, uuid::uuid &did,
                                std::vector<uuid::uuid> &members,
                                std::string &username,
                                std::vector<std::pair<uuid::uuid, bool>> &gpo_results,
                                int64_t &created_at);
    };

    uint64_t sidGroupId(const uuid::uuid &sid);

    // --- Data operation replication ---

    enum class OpType : uint8_t {
        Create = 1,
        Edit   = 2,
        Delete = 3
    };

    // Serialized data operation for cluster transport
    // Wire format:
    //   [1B op_type][4B domain_len][domain_bytes]
    //   [16B ruid][4B data_type]
    //   [4B record_count]
    //   for each record:
    //     [255B fieldname][4B storage][4B datasize][datasize bytes]
    struct DataOpBlock {
        static std::vector<uint8_t> serialize(OpType op, const std::string &domain,
                                               const uuid::uuid &uid, int dataType,
                                               const AuthData *records);
        static bool deserialize(const std::vector<uint8_t> &data,
                                OpType &op, std::string &domain,
                                uuid::uuid &uid, int &dataType,
                                std::vector<AuthData::Record> &records);
    };

    // Cluster manager: distributes sessions and data operations across peer nodes
    class Cluster {
    public:
        Cluster();
        ~Cluster();

        // Initialize with config. Must be called before start().
        void init(const ClusterConfig &cfg);

        // Start the cluster server in a background thread
        void start();

        // Stop the cluster
        void stop();

        bool isRunning() const { return running_; }

        // Block until at least k nodes are reachable (up to timeout_s seconds).
        // Returns number of nodes online.
        size_t waitForPeers(int timeout_s = 10);

        // Accessors for backend use
        paritypp::block_store &getStore() { return *store_; }
        std::unique_ptr<paritypp::client> &getClient() { return pclient_; }
        std::unique_ptr<paritypp::client> &getReadClient() { return read_client_; }
        std::unique_ptr<paritypp::client> &getSessionReadClient() { return session_read_client_; }

        // Vacuum all cluster nodes (acquires cluster-wide scrub lock)
        size_t vacuumAllNodes();

        // Push a session to the cluster (after local addSession)
        void pushSession(const SessionData &sess);

        // Queue a session for background push (used when degraded or push fails)
        void queuePush(const SessionData &sess);

        // Queue a backend data push for background replication
        void queueDataPush(uint64_t dgid, std::vector<uint8_t> buf);

        // Queue a data remove for background processing
        void queueDataRemove(uint64_t dgid);

        // Remove a session from the cluster by SID
        void removeSessionBySid(const uuid::uuid &session_id);

        // Check if a session still exists in the cluster by SID
        bool hasSessionBySid(const uuid::uuid &session_id);

        // Local-only check: fetch session from session_store_ without network calls
        bool fetchSessionLocalBySid(const uuid::uuid &session_id,
                                     uuid::uuid &sid, uuid::uuid &uid, uuid::uuid &did,
                                     std::vector<uuid::uuid> &members,
                                     std::string &username,
                                     std::vector<std::pair<uuid::uuid, bool>> &gpo_results);

        // Fetch a session from the cluster by session ID (SID)
        bool fetchSessionBySid(const uuid::uuid &session_id,
                               uuid::uuid &sid, uuid::uuid &uid, uuid::uuid &did,
                               std::vector<uuid::uuid> &members,
                               std::string &username,
                               std::vector<std::pair<uuid::uuid, bool>> &gpo_results);

        // Replicate a data operation (create/edit/delete) to peers
        void replicateOp(OpType op, const std::string &domain,
                         const uuid::uuid &uid, int dataType,
                         const AuthData *records = nullptr);

        // List all sessions from the dedicated session store (local + imported).
        struct SessionInfo {
            uuid::uuid sid, uid, did;
            std::vector<uuid::uuid> members;
            std::string username;
            std::vector<std::pair<uuid::uuid, bool>> gpo_results;
            int64_t created_at{0};
        };
        void listAllSessions(std::vector<SessionInfo> &out);

        bool isDegraded() const { return degraded_; }
        bool isCritical() const { return critical_; }

        // Incremented on cluster recovery — backends check this to force refresh
        uint64_t getRecoveryEpoch() const { return recovery_epoch_.load(); }

        // Force all domain backends to re-fetch from cluster on next access
        void forceSync() { recovery_epoch_.fetch_add(1); }

        // Scrub: verify and repair all data groups across the cluster.
        // For each group, checks that every node has its blocks. Missing blocks
        // are fetched from a peer that has them and re-replicated.
        // Returns {groups_checked, groups_repaired, groups_failed}.
        struct ScrubResult {
            size_t groups_checked  = 0;
            size_t groups_repaired = 0;
            size_t groups_failed   = 0;
        };
        ScrubResult scrub();

        // Try to acquire the scrub/rebalance lock (non-blocking).
        // Returns a unique_lock that owns the lock on success, or doesn't own it.
        std::unique_lock<std::mutex> tryScrubLock() {
            return std::unique_lock<std::mutex>(scrub_mutex_, std::try_to_lock);
        }

        bool isScrubRunning() const { return scrub_running_.load(); }

        // Number of pending (not yet durably stored) pushes in the async queues.
        size_t pendingSessionPushes() const {
            std::lock_guard<std::mutex> lk(const_cast<std::mutex&>(push_mutex_));
            return push_queue_.size();
        }
        size_t pendingDataPushes() const {
            std::lock_guard<std::mutex> lk(const_cast<std::mutex&>(push_mutex_));
            return data_push_queue_.size();
        }

        // Signal that all domain backends have been loaded into cache.
        // Scrub will not remove orphaned groups until this is set.
        void setDomainsReady() { domains_ready_ = true; }
        bool domainsReady() const { return domains_ready_.load(); }

    private:
        ClusterConfig cfg_;
        std::atomic<bool> running_{false};
        std::atomic<bool> degraded_{false};
        std::atomic<bool> critical_{false};
        std::atomic<bool> domains_ready_{false};
        std::atomic<uint64_t> recovery_epoch_{0};
        std::thread server_thread_;
        std::thread health_monitor_thread_;
        std::thread push_worker_thread_;
        std::mutex monitor_mutex_;
        std::condition_variable monitor_cv_;
        std::mutex push_mutex_;
        std::condition_variable push_cv_;
        std::mutex scrub_mutex_;  // prevents concurrent scrub / rebalance
        std::atomic<bool> scrub_running_{false};

        struct PendingPush {
            std::vector<uint8_t> data;
            uint64_t sid_gid;
            int retries{0};
        };
        std::deque<PendingPush> push_queue_;

        struct PendingDataPush {
            uint64_t dgid;
            std::vector<uint8_t> buf;
            bool is_remove{false};
            int retries{0};
        };
        std::deque<PendingDataPush> data_push_queue_;

        std::shared_ptr<paritypp::block_store> store_;
        std::shared_ptr<paritypp::memory_block_store> session_store_; // sessions only (store_id=1)
        std::shared_ptr<paritypp::auth_store> auth_;
        std::unique_ptr<paritypp::server> server_;

        std::unique_ptr<paritypp::client> pclient_;

        // Dedicated client for scrub/rebalance — avoids blocking pclient_
        // (used by push_worker and health monitor) during long scrub operations
        std::unique_ptr<paritypp::client> scrub_client_;

        // Dedicated client for domain data reads — avoids contention with
        // pclient_ which is used by push_worker (store) and health monitor
        std::unique_ptr<paritypp::client> read_client_;

        // Dedicated client for session writes — used by push_worker only
        std::unique_ptr<paritypp::client> session_client_;

        // Dedicated client for session reads — avoids contention with
        // push_worker holding session_client_->mutex_ during store()
        std::unique_ptr<paritypp::client> session_read_client_;

        void server_loop();
        void health_monitor_loop();
        void push_worker_loop();
        void session_cleanup_loop();

        std::thread session_cleanup_thread_;
        static constexpr int64_t SESSION_TTL_SECONDS = 24 * 3600; // 24h
    };

    // Global cluster instance
    extern Cluster *g_Cluster;
};
