// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#pragma once

#include <string>
#include <functional>
#include <memory>
#include <atomic>
#include <thread>
#include <mutex>
#include <set>
#include <vector>

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#endif

namespace dragonx {
namespace daemon {

/**
 * @brief Manages the embedded dragonxd process
 * 
 * Handles starting, stopping, and monitoring the dragonxd daemon process.
 * Ports functionality from Qt version's connection.cpp.
 */
class EmbeddedDaemon {
public:
    enum class State {
        Stopped,
        Starting,
        Running,
        Stopping,
        Error
    };
    
    using StateCallback = std::function<void(State, const std::string&)>;
    
    EmbeddedDaemon();
    ~EmbeddedDaemon();
    
    // Non-copyable
    EmbeddedDaemon(const EmbeddedDaemon&) = delete;
    EmbeddedDaemon& operator=(const EmbeddedDaemon&) = delete;
    
    /**
     * @brief Start the embedded dragonxd daemon
     * @param binary_path Path to dragonxd binary (auto-detect if empty)
     * @return true if process started successfully
     */
    bool start(const std::string& binary_path = "");
    
    /**
     * @brief Stop the running daemon
     * @param wait_ms Maximum milliseconds to wait for graceful shutdown
     */
    void stop(int wait_ms = 5000);
    
    /**
     * @brief Check if daemon process is running
     */
    bool isRunning() const;
    
    /**
     * @brief Get current daemon state
     */
    State getState() const { return state_; }

    /**
     * @brief Get daemon process memory usage (RSS) in MB
     * @return Memory usage in MB, or 0 if unavailable
     */
    double getMemoryUsageMB() const;
    
    /**
     * @brief Get last error message
     */
    // Copy under lock: last_error_ is written from the monitor thread (setState on an unexpected exit)
    // while the UI thread reads it — a reference would be a torn-read / use-after-free race (M-04).
    std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
    
    /**
     * @brief Get dragonxd process output (thread-safe copy)
     */
    std::string getOutput() const {
        std::lock_guard<std::mutex> lk(output_mutex_);
        return process_output_;
    }

    /**
     * @brief Get new output since a given offset (thread-safe).
     * Returns the new text and updates offset to the current size.
     */
    std::string getOutputSince(size_t& offset) const {
        std::lock_guard<std::mutex> lk(output_mutex_);
        if (offset >= process_output_.size()) {
            offset = process_output_.size();
            return {};
        }
        std::string result = process_output_.substr(offset);
        offset = process_output_.size();
        return result;
    }

    /**
     * @brief Get current output size (thread-safe, no copy)
     */
    size_t getOutputSize() const {
        std::lock_guard<std::mutex> lk(output_mutex_);
        return process_output_.size();
    }

    /**
     * @brief Get last N lines of daemon output (thread-safe snapshot)
     */
    std::vector<std::string> getRecentLines(int maxLines = 8) const;

    /**
     * @brief Extract the latest block height from daemon output (thread-safe).
     * Parses the last "height=N" from UpdateTip lines without copying
     * the entire output buffer.  Returns -1 if no UpdateTip found.
     */
    int getLastBlockHeight() const {
        std::lock_guard<std::mutex> lk(output_mutex_);
        // Search backwards from the end for "height="
        size_t pos = process_output_.rfind("height=");
        if (pos == std::string::npos) return -1;
        pos += 7; // skip "height="
        int h = 0;
        for (size_t i = pos; i < process_output_.size(); ++i) {
            char c = process_output_[i];
            if (c >= '0' && c <= '9') h = h * 10 + (c - '0');
            else break;
        }
        return h > 0 ? h : -1;
    }
    
    /**
     * @brief Whether start() detected an existing daemon on the RPC port.
     * When true the wallet should connect to it instead of showing an error.
     */
    bool externalDaemonDetected() const { return external_daemon_detected_; }
    // Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that
    // stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also
    // clears it on the port-free fall-through, but only if not short-circuited by an early guard.
    void clearExternalDaemonDetected() { external_daemon_detected_ = false; }

    /**
     * @brief Set callback for state changes
     */
    void setStateCallback(StateCallback cb) { state_callback_ = cb; }
    
    /**
     * @brief Find dragonxd binary in standard locations
     * @return Path to binary, or empty if not found
     */
    static std::string findDaemonBinary();

    /**
     * @brief Check whether anything is listening on the default RPC port.
     * Useful for detecting an externally-started daemon.
     */
    static bool isRpcPortInUse();
    
    /**
     * @brief Get the chain parameters for dragonxd
     * @return Command line arguments for dragonx chain
     */
    static std::vector<std::string> getChainParams();

    /**
     * @brief Set debug logging categories (appended as -debug= flags on next start)
     */
    void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
    const std::set<std::string>& getDebugCategories() const { return debug_categories_; }

    /**
     * @brief Set maximum peer connections (0 = use daemon default)
     */
    void setMaxConnections(int v) { max_connections_ = v; }
    void setStratumHosting(bool enabled, const std::string& allowIp) { stratum_enabled_ = enabled; stratum_allowip_ = allowIp; }

    /**
     * @brief Request a blockchain rescan on the next daemon start
     */
    void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
    bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }

    // Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
    // <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
    // filename in the datadir — the daemon rejects paths.
    void setWalletFile(const std::string& v) { wallet_file_ = v; }
    std::string walletFile() const { return wallet_file_; }

    /**
     * @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
     *        wallet transaction/note records and rebuilds them from the chain (keys are kept); the
     *        daemon implicitly rescans afterwards. One-shot, like the rescan flag.
     */
    void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
    bool zapOnNextStart() const { return zap_on_next_start_.load(); }

    // -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in
    // the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt.
    void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; }
    bool salvageOnNextStart() const { return salvage_on_next_start_.load(); }

    // -reindex: rebuild the block index + chainstate from the raw blocks (blk*.dat) on startup. One-shot,
    // consumed on the next start. Offered when the node aborts on an unreadable block database (a
    // daemon-vs-chaindata format mismatch after an update, or a corrupt index). It implies a wallet
    // rescan, so it's the block-DB analogue of -salvagewallet and coexists with the wallet-repair flags.
    void setReindexOnNextStart(bool v) { reindex_on_next_start_ = v; }
    bool reindexOnNextStart() const { return reindex_on_next_start_.load(); }

    /**
     * @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
     *        different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
     *        "migrate to a seed wallet" flow to mint a fresh mnemonic wallet in a throwaway datadir
     *        without touching the real one. Consumed on the next start(); later starts are normal.
     *        The caller must serialize this with start() (no concurrent starts).
     */
    void setNextStartOverride(const std::string& datadir, std::vector<std::string> extraArgs) {
        override_datadir_ = datadir;
        override_extra_args_ = std::move(extraArgs);
    }
    void clearNextStartOverride() { override_datadir_.clear(); override_extra_args_.clear(); }

    /**
     * @brief Skip the "default RPC port already in use → external daemon" bail in start().
     *        Set true only for an isolated instance running on its OWN (non-default) port
     *        alongside the main daemon (migrate-to-seed flow).
     */
    void setSkipPortCheck(bool v) { skip_port_check_ = v; }

    /**
     * @brief True while ANY dragonxd process is running (by process name), regardless of who started it.
     *        Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a
     *        graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the
     *        process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released
     *        the datadir before starting a replacement. Matches the daemon binary name on all platforms.
     */
    static bool isDaemonProcessRunning();

    /** Decision returned by evaluateDatadirLockGate(): whether start() may spawn now. */
    struct StartLockGateDecision {
        bool proceed = true;             // false => bail before spawning
        const char* errorMessage = "";   // set (a string literal) when proceed == false
    };

    /**
     * @brief Pure decision for start(): bail because a previous dragonxd still holds the
     *        shared datadir lock? Isolated instances (skip_port_check_ / an active -datadir
     *        override) are exempt — they run their own throwaway datadir+port and can coexist
     *        with the main daemon. Does no process/fs I/O itself (the caller does the probing),
     *        so it is directly unit-testable; defined inline so tests need only this header.
     */
    static StartLockGateDecision evaluateDatadirLockGate(bool skipPortCheck,
                                                         bool isolatedOverride,
                                                         bool stillRunningAfterWait)
    {
        if (skipPortCheck || isolatedOverride) return {true, ""};
        if (stillRunningAfterWait) {
            return {false,
                    "A previous dragonxd is still shutting down and holding the data "
                    "directory lock. Retrying shortly…"};
        }
        return {true, ""};
    }

    /** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
    static bool tcpPortInUse(int port);

    /** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
    int getCrashCount() const { return crash_count_.load(); }
    /** Reset crash counter (call on successful connection or manual restart) */
    void resetCrashCount() { crash_count_ = 0; }

private:
    void setState(State s, const std::string& message = "");
    void monitorProcess();
    void drainOutput();   // read any pending stdout into process_output_
    void appendOutput(const char* data, size_t len); // append + trim (caller holds output_mutex_)
    bool startProcess(const std::string& binary_path, const std::vector<std::string>& args);
    
    std::atomic<State> state_{State::Stopped};
    std::atomic<bool> external_daemon_detected_{false};
    std::string last_error_;
    mutable std::mutex error_mutex_;    // protects last_error_ (written by main + monitor threads)
    mutable std::mutex output_mutex_;  // protects process_output_
    std::string process_output_;
    StateCallback state_callback_;
    
    // Process handle
#ifdef _WIN32
    HANDLE process_handle_ = nullptr;
    HANDLE stdout_read_ = nullptr;  // pipe handle (Linux-style pipe, unused when tailing debug.log)
    std::string debug_log_path_;    // path to daemon's debug.log for output tailing
    size_t debug_log_offset_ = 0;   // read offset into debug.log
    std::string launch_cmd_;        // full command line used to launch daemon (for diagnostics)
    std::string launch_binary_;     // binary path used (for diagnostics)
    std::string launch_workdir_;    // working directory used (for diagnostics)
#else
    pid_t process_pid_ = 0;
    int stdout_fd_ = -1;
#endif
    
    std::thread monitor_thread_;
    std::atomic<bool> should_stop_{false};
    std::set<std::string> debug_categories_;
    int max_connections_ = 0;  // 0 = daemon default
    bool stratum_enabled_ = false;   // -stratum: host a RandomX pool (v1.3.0+; older daemons ignore it)
    std::string stratum_allowip_;    // -stratumallowip subnet (empty = daemon default: loopback only)
    std::string wallet_file_;  // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
    std::atomic<int> crash_count_{0};  // consecutive crash counter
    std::atomic<bool> rescan_on_next_start_{false};  // -rescan flag for next start
    std::atomic<bool> zap_on_next_start_{false};     // -zapwallettxes=2 flag for next start
    std::atomic<bool> salvage_on_next_start_{false}; // -salvagewallet flag for next start
    std::atomic<bool> reindex_on_next_start_{false}; // -reindex flag for next start (rebuild block DB)
    std::string override_datadir_;                   // one-shot: -datadir for the next start
    std::vector<std::string> override_extra_args_;   // one-shot: extra args for the next start
    bool skip_port_check_ = false;                   // isolated instance on a non-default port
};

} // namespace daemon
} // namespace dragonx
