#pragma once

#include <cstdint>
#include <memory>
#include <string>

#include "command/command.h"
#include "eventide/async/async.h"
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/peer.h"
#include "eventide/serde/serde/raw_value.h"
#include "index/merged_index.h"
#include "index/project_index.h"
#include "semantic/relation_kind.h"
#include "server/compile_graph.h"
#include "server/config.h"
#include "server/worker_pool.h"
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"

#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"

namespace clice {

namespace et = eventide;
namespace protocol = et::ipc::protocol;

enum class CompletionContext { None, IncludeQuoted, IncludeAngled, Import };

struct PreambleCompletionContext {
    CompletionContext kind = CompletionContext::None;
    std::string prefix;
};

struct DocumentState {
    int version = 0;

    std::string text;

    std::uint64_t generation = 0;

    bool ast_dirty = true;

    /// Non-null while a compile is in flight. Other ensure_compiled() calls
    /// wait on this instead of sending duplicate compile requests.
    std::shared_ptr<et::event> compiling;
};

/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
///
/// Layer 1 (fast): compare each file's current mtime against build_at.
///   If all mtimes <= build_at, the artifact is fresh (zero I/O beyond stat).
///
/// Layer 2 (precise): for files whose mtime changed, re-hash their content
///   and compare against the stored hash.  If the hash matches, the file was
///   "touched" but not actually modified — skip the rebuild.
///
/// This avoids unnecessary recompilation from timestamp-only changes (e.g.
/// git checkout, touch, backup restore) while remaining cheap in the common
/// case where nothing changed.
struct DepsSnapshot {
    /// File path IDs interned via PathPool.
    llvm::SmallVector<std::uint32_t> path_ids;

    /// xxh3_64bits of file content at build time.
    llvm::SmallVector<std::uint64_t> hashes;

    /// time_t when this snapshot was captured.
    std::int64_t build_at = 0;
};

/// Cached PCH state for a single source file.
struct PCHState {
    /// Built PCH file path.
    std::string path;

    /// Preamble byte offset used when building.
    std::uint32_t bound = 0;

    /// xxh3 hash of preamble content.
    std::uint64_t hash = 0;

    /// Dependency snapshot for staleness detection.
    DepsSnapshot deps;

    /// Non-null while a build is in flight.
    std::shared_ptr<et::event> building;
};

/// Cached PCM state for a single module file.
struct PCMState {
    /// Built PCM file path.
    std::string path;

    /// Dependency snapshot for staleness detection.
    DepsSnapshot deps;
};

/// Information about a symbol at a given position.
struct SymbolInfo {
    /// Unique hash identifying this symbol across the project.
    index::SymbolHash hash = 0;

    /// Human-readable symbol name.
    std::string name;

    /// Symbol kind (function, class, variable, etc.).
    SymbolKind kind;

    /// URI of the file containing this symbol.
    std::string uri;

    /// Source range of the symbol's identifier.
    protocol::Range range;
};

enum class ServerLifecycle : std::uint8_t {
    Uninitialized,
    Initialized,
    Ready,
    ShuttingDown,
    Exited,
};

class MasterServer {
public:
    MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
    ~MasterServer();

    void register_handlers();

private:
    /// Event loop for scheduling async tasks.
    et::event_loop& loop;

    /// JSON-RPC peer for LSP communication.
    et::ipc::JsonPeer& peer;

    /// Pool of stateful/stateless worker processes.
    WorkerPool pool;

    /// Interning pool for file paths (path string -> uint32_t ID).
    PathPool path_pool;

    /// Current server lifecycle state.
    ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;

    /// Path to the clice binary itself.
    std::string self_path;

    /// Root directory of the opened workspace.
    std::string workspace_root;

    /// User/project configuration.
    CliceConfig config;

    /// Session-specific log directory (e.g. .clice/logs/2026-04-05_10-30-00/).
    std::string session_log_dir;

    /// Compilation database (compile_commands.json).
    CompilationDatabase cdb;

    /// Include/module dependency graph built from fast lexer scanning.
    DependencyGraph dependency_graph;

    /// Module compilation graph (lazy dependency resolution).
    std::unique_ptr<CompileGraph> compile_graph;

    /// path_id -> built PCM output path (set after successful module build).
    llvm::DenseMap<std::uint32_t, std::string> pcm_paths;

    /// path_id -> module name (for files that provide a module interface).
    llvm::DenseMap<std::uint32_t, std::string> path_to_module;

    /// path_id -> cached PCH state (path, preamble hash, deps, build event).
    llvm::DenseMap<std::uint32_t, PCHState> pch_states;

    /// path_id -> cached PCM state (path, deps).
    llvm::DenseMap<std::uint32_t, PCMState> pcm_states;

    // === Index state ===

    /// Global symbol table and path mapping for the project.
    index::ProjectIndex project_index;

    /// Per-file merged index shards (keyed by project-level path_id).
    llvm::DenseMap<std::uint32_t, index::MergedIndex> merged_indices;

    /// Files queued for background indexing (server-level path_ids from CDB).
    std::vector<std::uint32_t> index_queue;

    /// Index of next file to process in index_queue.
    std::size_t index_queue_pos = 0;

    /// Whether background indexing is currently in progress.
    bool indexing_active = false;

    /// Whether a background indexing coroutine has been scheduled (waiting on timer).
    bool indexing_scheduled = false;

    /// Timer for idle-triggered background indexing.
    std::shared_ptr<et::timer> index_idle_timer;

    /// path_id -> open document state (text, version, generation, dirty flag).
    llvm::DenseMap<std::uint32_t, DocumentState> documents;

    /// Per-file dependency snapshots from last successful AST compilation.
    llvm::DenseMap<std::uint32_t, DepsSnapshot> ast_deps;

    // === Helpers ===

    /// Convert a file:// URI to a local file path.
    std::string uri_to_path(const std::string& uri);

    /// Publish diagnostics to the LSP client.
    void publish_diagnostics(const std::string& uri,
                             int version,
                             const eventide::serde::RawValue& diagnostics_json);

    /// Clear diagnostics for a file (publish empty array).
    void clear_diagnostics(const std::string& uri);

    /// Pull-based compilation entry point. Ensures AST is up-to-date.
    et::task<bool> ensure_compiled(std::uint32_t path_id);

    /// Load CDB and build initial include/module dependency graph.
    et::task<> load_workspace();

    /// Fill compile arguments from CDB for a given file.
    bool fill_compile_args(llvm::StringRef path,
                           std::string& directory,
                           std::vector<std::string>& arguments);

    /// Build or reuse PCH for a source file. Returns true if PCH is available.
    et::task<bool> ensure_pch(std::uint32_t path_id,
                              llvm::StringRef path,
                              const std::string& text,
                              const std::string& directory,
                              const std::vector<std::string>& arguments);

    /// Compile module dependencies, build/reuse PCH, and fill PCM paths.
    /// Shared preparation step for ensure_compiled() and forward_stateless().
    et::task<bool> ensure_deps(std::uint32_t path_id,
                               llvm::StringRef path,
                               const std::string& text,
                               const std::string& directory,
                               const std::vector<std::string>& arguments,
                               std::pair<std::string, uint32_t>& pch,
                               std::unordered_map<std::string, std::string>& pcms);

    /// Schedule background indexing when idle.
    void schedule_indexing();

    /// Background indexing coroutine: picks files from queue and dispatches to workers.
    et::task<> run_background_indexing();

    /// Merge a TUIndex result into ProjectIndex and MergedIndex shards.
    void merge_index_result(const void* tu_index_data, std::size_t size);

    /// Persist index state to disk.
    void save_index();

    /// Load index state from disk.
    void load_index();

    /// Load PCH/PCM cache metadata from cache.json.
    void load_cache();

    /// Save PCH/PCM cache metadata to cache.json.
    void save_cache();

    /// Clean up stale cache files older than max_age_days.
    void cleanup_cache(int max_age_days = 7);

    // === Include/import completion (handled in master) ===

    /// Detect whether the cursor is on an #include or import line.
    PreambleCompletionContext detect_completion_context(const std::string& text, uint32_t offset);

    /// Complete #include paths by enumerating search directories.
    et::serde::RawValue complete_include(const PreambleCompletionContext& ctx,
                                         llvm::StringRef path);

    /// Complete import module names from the known module map.
    et::serde::RawValue complete_import(const PreambleCompletionContext& ctx);

    // === Feature request forwarding ===

    using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;

    /// Forward a simple stateful request (path-only worker params).
    template <typename WorkerParams>
    RawResult forward_stateful(const std::string& uri);

    /// Forward a stateful request with position-to-offset conversion.
    template <typename WorkerParams>
    RawResult forward_stateful(const std::string& uri, const protocol::Position& position);

    /// Forward a stateless request with document content and compile args.
    template <typename WorkerParams>
    RawResult forward_stateless(const std::string& uri, const protocol::Position& position);

    // === Index query helpers ===

    /// Query index for symbol relations (GoToDefinition, FindReferences, etc.).
    RawResult query_index_relations(const std::string& uri,
                                    const protocol::Position& position,
                                    RelationKind kind);

    /// Look up a symbol at a position, returning its hash, name, kind, and range.
    et::task<std::optional<SymbolInfo>>
        lookup_symbol_at_position(const std::string& uri, const protocol::Position& position);

    /// Find the definition location (uri + range) of a symbol by its hash.
    std::optional<protocol::Location> find_symbol_definition_location(index::SymbolHash hash);

    /// Convert clice::SymbolKind to LSP protocol::SymbolKind.
    static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);

    /// Build a CallHierarchyItem from a SymbolInfo.
    protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info);

    /// Build a TypeHierarchyItem from a SymbolInfo.
    protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info);

    /// Resolve SymbolInfo from a hierarchy item's stored data (symbol hash).
    et::task<std::optional<SymbolInfo>>
        resolve_hierarchy_item(const std::string& uri,
                               const protocol::Range& range,
                               const std::optional<protocol::LSPAny>& data);
};

}  // namespace clice


#include "server/stateless_worker.h"

#include <chrono>

#include "compile/compilation.h"
#include "eventide/async/async.h"
#include "eventide/ipc/json_codec.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "eventide/serde/json/serializer.h"
#include "eventide/serde/serde/raw_value.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "support/logging.h"

#include "llvm/Support/raw_ostream.h"

namespace clice {

namespace et = eventide;
using et::ipc::RequestResult;
using RequestContext = et::ipc::BincodePeer::RequestContext;

struct ScopedTimer {
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();

    long long ms() const {
        return std::chrono::duration_cast<std::chrono::milliseconds>(
                   std::chrono::steady_clock::now() - start)
            .count();
    }
};

static void fill_args(CompilationParams& cp,
                      const std::string& directory,
                      const std::vector<std::string>& arguments) {
    cp.directory = directory;
    for(auto& arg: arguments) {
        cp.arguments.push_back(arg.c_str());
    }
}

template <typename T>
static et::serde::RawValue to_raw(const T& value) {
    auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
    return et::serde::RawValue{json ? std::move(*json) : "null"};
}

int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) {
    logging::stderr_logger(worker_name, logging::options);
    if(!log_dir.empty()) {
        logging::file_logger(worker_name, log_dir, logging::options);
    }

    LOG_INFO("Starting stateless worker");

    et::event_loop loop;

    auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
    if(!transport_result) {
        LOG_ERROR("Failed to open stdio transport");
        return 1;
    }

    et::ipc::BincodePeer peer(loop, std::move(*transport_result));

    // === BuildPCH ===
    peer.on_request(
        [&](RequestContext& ctx,
            const worker::BuildPCHParams& params) -> RequestResult<worker::BuildPCHParams> {
            LOG_INFO("BuildPCH request: file={}", params.file);

            auto result = co_await et::queue([&]() -> worker::BuildPCHResult {
                ScopedTimer timer;

                CompilationParams cp;
                cp.kind = CompilationKind::Preamble;
                fill_args(cp, params.directory, params.arguments);
                cp.add_remapped_file(params.file, params.content, params.preamble_bound);

                // When output_path is set, write to .tmp then rename — avoids
                // Windows file-lock failures when the target is held by another
                // process.  When empty, fall back to a temporary file.
                std::string tmp_path;
                bool has_output = !params.output_path.empty();
                if(has_output) {
                    tmp_path = params.output_path + ".tmp";
                } else {
                    auto tmp = fs::createTemporaryFile("clice-pch", "pch");
                    if(!tmp) {
                        LOG_ERROR("BuildPCH: failed to create temp file");
                        return {false, "Failed to create temporary PCH file", ""};
                    }
                    tmp_path = *tmp;
                }
                cp.output_file = tmp_path;

                PCHInfo pch_info;
                auto unit = compile(cp, pch_info);
                bool success = unit.completed();

                // Extract error messages before destroying the unit.
                std::string errors;
                if(!success) {
                    for(auto& diag: unit.diagnostics()) {
                        if(diag.id.level >= DiagnosticLevel::Error) {
                            if(!errors.empty())
                                errors += "; ";
                            errors += diag.message;
                        }
                    }
                }

                // Destroy CompilationUnit to flush PCH file to disk
                // (EndSourceFile serializes the AST on destruction).
                unit = CompilationUnit(nullptr);

                if(success) {
                    std::string final_path;
                    if(has_output) {
                        auto ec = fs::rename(tmp_path, params.output_path);
                        if(ec) {
                            final_path = params.output_path;
                        } else {
                            LOG_WARN("BuildPCH: rename {} -> {} failed: {}",
                                     tmp_path,
                                     params.output_path,
                                     ec.error().message());
                            final_path = tmp_path;
                        }
                    } else {
                        final_path = tmp_path;
                    }
                    LOG_INFO("BuildPCH done: file={}, output={}, {}ms",
                             params.file,
                             final_path,
                             timer.ms());
                    worker::BuildPCHResult pch_result{true, "", std::move(final_path)};
                    pch_result.deps = pch_info.deps;
                    return pch_result;
                } else {
                    LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]",
                             params.file,
                             timer.ms(),
                             errors);
                    fs::remove(tmp_path);
                    return {false, errors.empty() ? "PCH compilation failed" : errors, ""};
                }
            });
            co_return result.value();
        });

    // === BuildPCM ===
    peer.on_request(
        [&](RequestContext& ctx,
            const worker::BuildPCMParams& params) -> RequestResult<worker::BuildPCMParams> {
            LOG_INFO("BuildPCM request: file={}, module={}", params.file, params.module_name);

            auto result = co_await et::queue([&]() -> worker::BuildPCMResult {
                ScopedTimer timer;

                CompilationParams cp;
                cp.kind = CompilationKind::ModuleInterface;
                fill_args(cp, params.directory, params.arguments);
                for(auto& [name, path]: params.pcms) {
                    cp.pcms.try_emplace(name, path);
                }

                std::string tmp_path;
                bool has_output = !params.output_path.empty();
                if(has_output) {
                    tmp_path = params.output_path + ".tmp";
                } else {
                    auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
                    if(!tmp) {
                        LOG_ERROR("BuildPCM: failed to create temp file");
                        return {false, "Failed to create temporary PCM file", ""};
                    }
                    tmp_path = *tmp;
                }
                cp.output_file = tmp_path;

                PCMInfo pcm_info;
                auto unit = compile(cp, pcm_info);
                bool success = unit.completed();

                std::string errors;
                if(!success) {
                    for(auto& diag: unit.diagnostics()) {
                        if(diag.id.level >= DiagnosticLevel::Error) {
                            if(!errors.empty())
                                errors += "; ";
                            errors += diag.message;
                        }
                    }
                }

                unit = CompilationUnit(nullptr);

                if(success) {
                    std::string final_path = tmp_path;
                    if(has_output) {
                        auto ec = fs::rename(tmp_path, params.output_path);
                        if(ec) {
                            final_path = params.output_path;
                        } else {
                            LOG_WARN("BuildPCM: rename {} -> {} failed: {}",
                                     tmp_path,
                                     params.output_path,
                                     ec.error().message());
                        }
                    }
                    LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
                    worker::BuildPCMResult pcm_result{true, "", std::move(final_path)};
                    pcm_result.deps = pcm_info.deps;
                    return pcm_result;
                } else {
                    LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]",
                             params.module_name,
                             timer.ms(),
                             errors);
                    fs::remove(tmp_path);
                    return {false, errors.empty() ? "PCM compilation failed" : errors, ""};
                }
            });
            co_return result.value();
        });

    // === Completion ===
    peer.on_request(
        [&](RequestContext& ctx,
            const worker::CompletionParams& params) -> RequestResult<worker::CompletionParams> {
            LOG_DEBUG("Completion request: path={}, offset={}", params.path, params.offset);

            auto result = co_await et::queue([&]() -> et::serde::RawValue {
                ScopedTimer timer;

                CompilationParams cp;
                cp.kind = CompilationKind::Completion;
                fill_args(cp, params.directory, params.arguments);
                if(!params.pch.first.empty()) {
                    cp.pch = params.pch;
                }
                for(auto& [name, path]: params.pcms) {
                    cp.pcms.try_emplace(name, path);
                }
                cp.add_remapped_file(params.path, params.text);
                cp.completion = {params.path, params.offset};

                auto items = feature::code_complete(cp);
                LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
                return to_raw(items);
            });
            co_return result.value();
        });

    // === SignatureHelp ===
    peer.on_request([&](RequestContext& ctx, const worker::SignatureHelpParams& params)
                        -> RequestResult<worker::SignatureHelpParams> {
        LOG_DEBUG("SignatureHelp request: path={}, offset={}", params.path, params.offset);

        auto result = co_await et::queue([&]() -> et::serde::RawValue {
            ScopedTimer timer;

            CompilationParams cp;
            cp.kind = CompilationKind::Completion;
            fill_args(cp, params.directory, params.arguments);
            if(!params.pch.first.empty()) {
                cp.pch = params.pch;
            }
            for(auto& [name, path]: params.pcms) {
                cp.pcms.try_emplace(name, path);
            }
            cp.add_remapped_file(params.path, params.text);
            cp.completion = {params.path, params.offset};

            auto help = feature::signature_help(cp);
            LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
            return to_raw(help);
        });
        co_return result.value();
    });

    // === Index ===
    peer.on_request([&](RequestContext& ctx,
                        const worker::IndexParams& params) -> RequestResult<worker::IndexParams> {
        LOG_INFO("Index request: file={}", params.file);

        auto result = co_await et::queue([&]() -> worker::IndexResult {
            ScopedTimer timer;

            CompilationParams cp;
            cp.kind = CompilationKind::Indexing;
            fill_args(cp, params.directory, params.arguments);
            for(auto& [name, path]: params.pcms) {
                cp.pcms.try_emplace(name, path);
            }

            auto unit = compile(cp);

            if(!unit.completed()) {
                LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
                return {false, "Index compilation failed", ""};
            }

            auto tu_index = index::TUIndex::build(unit);

            std::string serialized;
            llvm::raw_string_ostream os(serialized);
            tu_index.serialize(os);

            LOG_INFO("Index done: file={}, {} symbols, {}ms",
                     params.file,
                     tu_index.symbols.size(),
                     timer.ms());
            return {true, "", std::move(serialized)};
        });
        co_return result.value();
    });

    LOG_INFO("Stateless worker ready, waiting for requests");
    loop.schedule(peer.run());
    auto ret = loop.run();
    LOG_INFO("Stateless worker exiting with code {}", ret);
    return ret;
}

}  // namespace clice


#include "server/worker_pool.h"

#include <csignal>
#include <string>

#include "eventide/ipc/transport.h"
#include "support/logging.h"

namespace clice {

namespace {

/// Coroutine that drains a worker's stderr pipe.
/// Workers write their own log files, so this only captures unexpected output
/// (crash stacktraces, assertion failures, etc.) that bypasses spdlog.
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
    std::string buffer;
    while(true) {
        auto result = co_await stderr_pipe.read();
        if(!result.has_value()) {
            break;
        }
        auto& chunk = result.value();
        if(chunk.empty())
            break;

        buffer += chunk;

        std::size_t pos = 0;
        while(true) {
            auto nl = buffer.find('\n', pos);
            if(nl == std::string::npos)
                break;
            auto line = buffer.substr(pos, nl - pos);
            if(!line.empty()) {
                LOG_DEBUG("{} {}", prefix, line);
            }
            pos = nl + 1;
        }
        buffer.erase(0, pos);
    }

    if(!buffer.empty()) {
        LOG_DEBUG("{} {}", prefix, buffer);
    }
}

}  // namespace

bool WorkerPool::spawn_worker(const std::string& self_path,
                              bool stateful,
                              std::uint64_t memory_limit) {
    auto& workers = stateful ? stateful_workers : stateless_workers;
    auto worker_index = workers.size();
    std::string worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(worker_index);

    et::process::options opts;
    opts.file = self_path;
    if(stateful) {
        opts.args = {self_path,
                     "--mode",
                     "stateful-worker",
                     "--worker-memory-limit",
                     std::to_string(memory_limit)};
    } else {
        opts.args = {self_path, "--mode", "stateless-worker"};
    }

    opts.args.push_back("--worker-name");
    opts.args.push_back(worker_name);

    if(!log_dir_.empty()) {
        opts.args.push_back("--log-dir");
        opts.args.push_back(log_dir_);
    }

    opts.streams = {
        et::process::stdio::pipe(true, false),  // stdin: child reads
        et::process::stdio::pipe(false, true),  // stdout: child writes
        et::process::stdio::pipe(false, true),  // stderr: child writes
    };

    auto result = et::process::spawn(opts, loop);
    if(!result) {
        LOG_ERROR("Failed to spawn {} worker: {}",
                  stateful ? "stateful" : "stateless",
                  result.error().message());
        return false;
    }

    auto& spawn = *result;

    // StreamTransport: input = child's stdout (parent reads), output = child's stdin (parent
    // writes)
    auto transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
                                                                std::move(spawn.stdin_pipe));
    auto peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));

    // Schedule stderr log collection
    std::string prefix = "[" + worker_name + "]";
    loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));

    workers.push_back(WorkerProcess{
        .proc = std::move(spawn.proc),
        .peer = std::move(peer),
        .owned_documents = 0,
    });

    auto& w = workers.back();
    w.peer->set_logger(
        [prefix](et::ipc::LogLevel, std::string_view msg) {
            LOG_INFO("{} ipc: {}", prefix, msg);
        },
        et::ipc::LogLevel::debug);
    loop.schedule(w.peer->run());

    return true;
}

bool WorkerPool::start(const WorkerPoolOptions& options) {
    log_dir_ = options.log_dir;

    for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
        if(!spawn_worker(options.self_path, false, 0)) {
            return false;
        }
    }

    for(std::uint32_t i = 0; i < options.stateful_count; ++i) {
        if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) {
            return false;
        }
    }

    // Register evicted notification handler for each stateful worker
    for(std::size_t i = 0; i < stateful_workers.size(); ++i) {
        stateful_workers[i].peer->on_notification([this](const worker::EvictedParams& params) {
            if(on_evicted) {
                on_evicted(params.path);
            }
        });
    }

    LOG_INFO("WorkerPool started: {} stateless, {} stateful workers",
             stateless_workers.size(),
             stateful_workers.size());
    return true;
}

et::task<> WorkerPool::stop() {
    LOG_INFO("WorkerPool stopping...");

    // Close output pipes to signal workers to exit gracefully
    for(auto& w: stateless_workers) {
        w.peer->close_output();
    }
    for(auto& w: stateful_workers) {
        w.peer->close_output();
    }

    // Send SIGTERM to all workers
    for(auto& w: stateless_workers) {
        w.proc.kill(SIGTERM);
    }
    for(auto& w: stateful_workers) {
        w.proc.kill(SIGTERM);
    }

    // Wait for all worker processes to exit
    for(auto& w: stateless_workers) {
        co_await w.proc.wait();
    }
    for(auto& w: stateful_workers) {
        co_await w.proc.wait();
    }

    LOG_INFO("WorkerPool stopped");
}

std::size_t WorkerPool::assign_worker(std::uint32_t path_id) {
    auto it = owner.find(path_id);
    if(it != owner.end()) {
        // Already assigned; touch LRU
        auto lru_it = owner_lru_index.find(path_id);
        if(lru_it != owner_lru_index.end()) {
            owner_lru.erase(lru_it->second);
        }
        owner_lru.push_front(path_id);
        owner_lru_index[path_id] = owner_lru.begin();
        return it->second;
    }

    // New assignment: pick the least-loaded worker
    auto selected = pick_least_loaded();
    owner[path_id] = selected;
    stateful_workers[selected].owned_documents++;
    owner_lru.push_front(path_id);
    owner_lru_index[path_id] = owner_lru.begin();
    return selected;
}

std::size_t WorkerPool::pick_least_loaded() {
    std::size_t best = 0;
    for(std::size_t i = 1; i < stateful_workers.size(); ++i) {
        if(stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
            best = i;
        }
    }
    return best;
}

void WorkerPool::remove_owner(std::uint32_t path_id) {
    auto it = owner.find(path_id);
    if(it == owner.end())
        return;

    auto worker_idx = it->second;
    stateful_workers[worker_idx].owned_documents--;
    owner.erase(it);

    auto lru_it = owner_lru_index.find(path_id);
    if(lru_it != owner_lru_index.end()) {
        owner_lru.erase(lru_it->second);
        owner_lru_index.erase(lru_it);
    }
}

void WorkerPool::clear_owner(std::size_t worker_index) {
    llvm::SmallVector<std::uint32_t> to_remove;
    for(auto& [pid, widx]: owner) {
        if(widx == worker_index) {
            to_remove.push_back(pid);
        }
    }
    for(auto pid: to_remove) {
        remove_owner(pid);
    }
}

}  // namespace clice


#include "syntax/scan.h"

#include <deque>

#include "syntax/lexer.h"

#include "llvm/ADT/StringSet.h"
#include "llvm/Support/MemoryBuffer.h"
#include "clang/Basic/DiagnosticOptions.h"
#include "clang/Basic/FileEntry.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Lex/PPCallbacks.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Lex/PreprocessorOptions.h"
#include "clang/Tooling/CompilationDatabase.h"

namespace clice {

ScanResult scan(llvm::StringRef content) {
    namespace dds = clang::dependency_directives_scan;

    ScanResult result;

    llvm::SmallVector<dds::Token> tokens;
    llvm::SmallVector<dds::Directive> directives;

    if(clang::scanSourceForDependencyDirectives(content, tokens, directives)) {
        return result;
    }

    // Most source files have 10-30 includes; pre-allocate to avoid reallocs.
    result.includes.reserve(std::min<std::size_t>(directives.size(), 32));

    int conditional_depth = 0;

    for(auto& dir: directives) {
        switch(dir.Kind) {
            case dds::pp_if:
            case dds::pp_ifdef:
            case dds::pp_ifndef: {
                conditional_depth++;
                break;
            }
            case dds::pp_endif: {
                if(conditional_depth > 0) {
                    conditional_depth--;
                }
                break;
            }
            case dds::pp_elif:
            case dds::pp_elifdef:
            case dds::pp_elifndef:
            case dds::pp_else: {
                break;
            }
            case dds::pp_include:
            case dds::pp_include_next:
            case dds::pp___include_macros: {
                // Find the header token (string_literal or header_name).
                for(auto& tok: dir.Tokens) {
                    if(tok.is(clang::tok::header_name) || tok.is(clang::tok::string_literal)) {
                        auto name = content.substr(tok.Offset, tok.Length);
                        // Strip <> or "" delimiters.
                        if(name.size() >= 2) {
                            bool angled = name.front() == '<';
                            ScanResult::IncludeInfo info;
                            info.path = std::string(name.substr(1, name.size() - 2));
                            info.conditional = conditional_depth > 0;
                            info.is_angled = angled;
                            info.is_include_next = dir.Kind == dds::pp_include_next;
                            result.includes.push_back(std::move(info));
                        }
                        break;
                    }
                }
                break;
            }
            case dds::cxx_module_decl:
            case dds::cxx_export_module_decl: {
                if(conditional_depth > 0) {
                    result.need_preprocess = true;
                    return result;
                }

                // Collect module name from tokens: skip keywords, then
                // collect identifiers, '.', ':'.
                std::string module_name;
                bool seen_module_keyword = false;
                for(auto& tok: dir.Tokens) {
                    if(!seen_module_keyword) {
                        if(tok.is(clang::tok::raw_identifier)) {
                            auto spelling = content.substr(tok.Offset, tok.Length);
                            if(spelling == "module") {
                                seen_module_keyword = true;
                            }
                        }
                        continue;
                    }
                    if(tok.is(clang::tok::raw_identifier)) {
                        module_name += content.substr(tok.Offset, tok.Length);
                    } else if(tok.is(clang::tok::period)) {
                        module_name += '.';
                    } else if(tok.is(clang::tok::colon)) {
                        module_name += ':';
                    }
                }

                result.module_name = std::move(module_name);
                result.is_interface_unit = (dir.Kind == dds::cxx_export_module_decl);
                break;
            }
            default: {
                break;
            }
        }
    }

    return result;
}

namespace {

class ScanDirectivesGetter : public clang::DependencyDirectivesGetter {
public:
    ScanDirectivesGetter(SharedScanCache* cache, clang::FileManager& file_mgr) :
        cache(cache), file_mgr(&file_mgr) {}

    std::unique_ptr<clang::DependencyDirectivesGetter>
        cloneFor(clang::FileManager& new_file_mgr) override {
        return std::make_unique<ScanDirectivesGetter>(cache, new_file_mgr);
    }

    std::optional<llvm::ArrayRef<clang::dependency_directives_scan::Directive>>
        operator()(clang::FileEntryRef file) override {
        auto path = file.getFileEntry().tryGetRealPathName();
        if(path.empty()) {
            path = file.getName();
        }

        // Check cache first.
        if(cache) {
            auto it = cache->entries.find(path);
            if(it != cache->entries.end()) {
                return llvm::ArrayRef(it->second.directives);
            }
        }

        // Read the file content.
        auto buffer = file_mgr->getBufferForFile(file);
        if(!buffer) {
            return std::nullopt;
        }

        auto source = (*buffer)->getBuffer().str();

        // Create entry in its final location first, then scan into it.
        // Directive::Tokens are ArrayRefs pointing into the tokens SmallVector,
        // so the entry must not be moved after scanning.
        SharedScanCache::CachedEntry* entry_ptr;
        if(cache) {
            auto [it, _] = cache->entries.try_emplace(path);
            entry_ptr = &it->second;
        } else {
            local_entries.emplace_back();
            entry_ptr = &local_entries.back();
        }

        entry_ptr->source = std::move(source);

        if(clang::scanSourceForDependencyDirectives(entry_ptr->source,
                                                    entry_ptr->tokens,
                                                    entry_ptr->directives)) {
            // Scan failed — remove the entry.
            if(cache) {
                cache->entries.erase(path);
            } else {
                local_entries.pop_back();
            }
            return std::nullopt;
        }

        return llvm::ArrayRef(entry_ptr->directives);
    }

private:
    SharedScanCache* cache;
    clang::FileManager* file_mgr;
    std::deque<SharedScanCache::CachedEntry> local_entries;
};

/// PPCallbacks for precise mode: single ScanResult with accurate
/// conditional tracking via preprocessor callbacks.
class PreciseScanPPCallbacks : public clang::PPCallbacks {
public:
    explicit PreciseScanPPCallbacks(ScanResult& result) : result(result) {}

    void InclusionDirective(clang::SourceLocation,
                            const clang::Token& include_tok,
                            llvm::StringRef file_name,
                            bool is_angled,
                            clang::CharSourceRange,
                            clang::OptionalFileEntryRef file,
                            llvm::StringRef,
                            llvm::StringRef,
                            const clang::Module*,
                            bool,
                            clang::SrcMgr::CharacteristicKind) override {
        bool not_found = !file.has_value();
        std::string resolved_path;
        if(file) {
            resolved_path = file->getFileEntry().tryGetRealPathName().str();
        } else {
            resolved_path = file_name.str();
        }

        ScanResult::IncludeInfo info;
        info.path = std::move(resolved_path);
        info.conditional = conditional_depth > 0;
        info.not_found = not_found;
        info.is_angled = is_angled;
        info.is_include_next =
            include_tok.getIdentifierInfo() &&
            include_tok.getIdentifierInfo()->getPPKeywordID() == clang::tok::pp_include_next;
        result.includes.push_back(std::move(info));
    }

    void If(clang::SourceLocation, clang::SourceRange, ConditionValueKind) override {
        conditional_depth++;
    }

    void Ifdef(clang::SourceLocation, const clang::Token&, const clang::MacroDefinition&) override {
        conditional_depth++;
    }

    void Ifndef(clang::SourceLocation,
                const clang::Token&,
                const clang::MacroDefinition&) override {
        conditional_depth++;
    }

    void Endif(clang::SourceLocation, clang::SourceLocation) override {
        if(conditional_depth > 0) {
            conditional_depth--;
        }
    }

    void moduleImport(clang::SourceLocation,
                      clang::ModuleIdPath names,
                      const clang::Module*) override {
        std::string name;
        for(auto& part: names) {
            if(!name.empty()) {
                name += '.';
            }
            name += part.getIdentifierInfo()->getName();
        }
        result.modules.emplace_back(std::move(name));
    }

private:
    ScanResult& result;
    int conditional_depth = 0;
};

/// Create and configure a CompilerInstance for scanning.
/// If content is non-empty, it is used as remapped source for the main file.
std::unique_ptr<clang::CompilerInstance>
    create_scan_instance(llvm::ArrayRef<const char*> arguments,
                         llvm::StringRef directory,
                         llvm::StringRef content,
                         llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs) {
    clang::DiagnosticOptions diag_opts;
    auto diag_engine = clang::CompilerInstance::createDiagnostics(*vfs,
                                                                  diag_opts,
                                                                  new clang::IgnoringDiagConsumer(),
                                                                  true);

    std::unique_ptr<clang::CompilerInvocation> invocation;

    bool is_cc1 = arguments.size() >= 2 && llvm::StringRef(arguments[1]) == "-cc1";
    if(is_cc1) {
        invocation = std::make_unique<clang::CompilerInvocation>();
        if(!clang::CompilerInvocation::CreateFromArgs(*invocation,
                                                      llvm::ArrayRef(arguments).drop_front(2),
                                                      *diag_engine,
                                                      arguments[0])) {
            return nullptr;
        }
    } else {
        clang::CreateInvocationOptions options = {
            .Diags = diag_engine,
            .VFS = vfs,
            .ProbePrecompiled = false,
        };
        invocation = clang::createInvocation(arguments, options);
        if(!invocation) {
            return nullptr;
        }
    }

    invocation->getFrontendOpts().DisableFree = false;
    invocation->getFileSystemOpts().WorkingDir = directory.str();

    if(!content.empty()) {
        auto& inputs = invocation->getFrontendOpts().Inputs;
        if(!inputs.empty()) {
            auto main_file = inputs[0].getFile();
            // Use an overlay VFS to inject the remapped content. This ensures
            // both the preprocessor and the DependencyDirectivesGetter see it.
            auto overlay = llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(vfs);
            auto mem_fs = llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>();
            mem_fs->addFile(main_file, 0, llvm::MemoryBuffer::getMemBufferCopy(content, main_file));
            overlay->pushOverlay(std::move(mem_fs));
            vfs = std::move(overlay);
        }
    }

    auto instance = std::make_unique<clang::CompilerInstance>(std::move(invocation));
    instance->createDiagnostics(*vfs, new clang::IgnoringDiagConsumer(), true);
    instance->getDiagnostics().setSuppressAllDiagnostics(true);
    instance->createFileManager(vfs);

    return instance;
}

}  // namespace

ScanResult scan_precise(llvm::ArrayRef<const char*> arguments,
                        llvm::StringRef directory,
                        llvm::StringRef content,
                        SharedScanCache* cache,
                        llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs) {
    ScanResult result;

    if(!vfs) {
        vfs = llvm::vfs::createPhysicalFileSystem();
    }

    auto instance = create_scan_instance(arguments, directory, content, vfs);
    if(!instance) {
        return result;
    }

    auto getter = std::make_unique<ScanDirectivesGetter>(cache, instance->getFileManager());
    instance->setDependencyDirectivesGetter(std::move(getter));

    if(!instance->createTarget()) {
        return result;
    }

    auto action = std::make_unique<clang::PreprocessOnlyAction>();

    if(!action->BeginSourceFile(*instance, instance->getFrontendOpts().Inputs[0])) {
        return result;
    }

    instance->getPreprocessor().addPPCallbacks(std::make_unique<PreciseScanPPCallbacks>(result));

    if(auto error = action->Execute()) {
        llvm::consumeError(std::move(error));
    }

    action->EndSourceFile();

    // Get module name from preprocessor.
    auto& pp = instance->getPreprocessor();
    if(pp.isInNamedModule()) {
        result.module_name = pp.getNamedModuleName();
        result.is_interface_unit = pp.isInNamedInterfaceUnit();
    }

    return result;
}

ScanResult scan_module_decl(llvm::ArrayRef<const char*> arguments,
                            llvm::StringRef directory,
                            llvm::StringRef content,
                            SharedScanCache* cache,
                            llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs) {
    ScanResult result;

    if(!vfs) {
        vfs = llvm::vfs::createPhysicalFileSystem();
    }

    auto instance = create_scan_instance(arguments, directory, content, vfs);
    if(!instance) {
        return result;
    }

    auto getter = std::make_unique<ScanDirectivesGetter>(cache, instance->getFileManager());
    instance->setDependencyDirectivesGetter(std::move(getter));

    if(!instance->createTarget()) {
        return result;
    }

    auto action = std::make_unique<clang::PreprocessOnlyAction>();

    if(!action->BeginSourceFile(*instance, instance->getFrontendOpts().Inputs[0])) {
        return result;
    }

    // Instead of action->Execute() which processes the entire file,
    // manually lex tokens and stop as soon as the module declaration is found.
    auto& pp = instance->getPreprocessor();
    pp.EnterMainSourceFile();

    clang::Token tok;
    do {
        pp.Lex(tok);
        if(pp.isInNamedModule()) {
            result.module_name = pp.getNamedModuleName();
            result.is_interface_unit = pp.isInNamedInterfaceUnit();
            break;
        }
    } while(tok.isNot(clang::tok::eof));

    action->EndSourceFile();

    return result;
}

std::uint32_t compute_preamble_bound(llvm::StringRef content) {
    auto result = compute_preamble_bounds(content);
    if(result.empty()) {
        return 0;
    } else {
        return result.back();
    }
}

std::vector<std::uint32_t> compute_preamble_bounds(llvm::StringRef content) {
    std::vector<std::uint32_t> result;

    Lexer lexer(content, true, nullptr, false);

    while(true) {
        auto token = lexer.advance();
        if(token.is_eof()) {
            break;
        }

        if(token.is_at_start_of_line) {
            if(token.kind == clang::tok::hash) {
                /// For preprocessor directive, consume the whole directive.
                lexer.advance_until(clang::tok::eod);
                auto last = lexer.last();

                /// Append the token before the eod.
                result.push_back(last.range.end);
            } else if(token.is_identifier() && token.text(content) == "module") {
                /// If we encounter a module keyword at the start of a line, it may be
                /// a module declaration or global module fragment.
                auto next = lexer.next();

                if(next.kind == clang::tok::semi) {
                    /// If next token is `;`, it is a global module fragment.
                    /// we just continue.
                    lexer.advance();

                    /// Append it to bounds.
                    result.push_back(next.range.end);
                } else {
                    break;
                }
            } else {
                break;
            }
        }
    }

    return result;
}

/// Check if a preprocessor #include/#import directive line is complete.
static bool is_include_directive_complete(llvm::StringRef directive) {
    if(directive.contains('"')) {
        auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7);
        return after_keyword.count('"') >= 2;
    }
    if(directive.contains('<')) {
        return directive.contains('>');
    }
    // No " or < — might be a macro (#include FOO) or just incomplete (#include ).
    auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7).ltrim();
    return !after_keyword.empty();
}

/// Check if a C++20 module statement line (import/export module) is complete.
/// A complete statement must end with ';'.
static bool is_module_statement_complete(llvm::StringRef trimmed) {
    return trimmed.rtrim().ends_with(";");
}

bool is_preamble_complete(llvm::StringRef content, std::uint32_t bound) {
    auto preamble = content.substr(0, bound);

    while(!preamble.empty()) {
        auto [line, rest] = preamble.split('\n');
        preamble = rest;

        auto trimmed = line.ltrim();

        // Preprocessor directive: #include or #import
        if(trimmed.starts_with("#")) {
            auto directive = trimmed.drop_front(1).ltrim();
            if(directive.starts_with("include") || directive.starts_with("import")) {
                if(!is_include_directive_complete(directive)) {
                    return false;
                }
            }
            continue;
        }

        // C++20 module statements: import, export module, export import
        // Check word boundary to avoid matching identifiers like "important".
        auto is_keyword = [](llvm::StringRef s, llvm::StringRef keyword) {
            return s.starts_with(keyword) &&
                   (s.size() == keyword.size() || !llvm::isAlnum(s[keyword.size()]));
        };
        if(is_keyword(trimmed, "import") || is_keyword(trimmed, "export")) {
            if(!is_module_statement_complete(trimmed)) {
                return false;
            }
        }
    }

    return true;
}

}  // namespace clice


#pragma once

#include <cstdint>
#include <string>
#include <vector>

#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "clang/Lex/DependencyDirectivesScanner.h"

namespace clice {

struct ScanResult {
    /// Module name (empty if not a module unit).
    std::string module_name;

    /// Whether this is an interface unit (has `export module`).
    bool is_interface_unit = false;

    /// Whether module declaration is inside conditional directive,
    /// signaling caller should fall back to preprocessor-based scan.
    bool need_preprocess = false;

    struct IncludeInfo {
        /// Resolved file path (fuzzy/precise scan) or raw header name (lexer scan).
        std::string path;

        /// Whether this include is inside a conditional directive context.
        bool conditional = false;

        /// Whether the included file was not found during resolution.
        bool not_found = false;

        /// Whether this is an angled include (<...>) vs quoted ("...").
        bool is_angled = false;

        /// Whether this is an #include_next directive.
        bool is_include_next = false;
    };

    /// Include file names.
    /// From lexer scan these are the raw header names;
    /// from preprocessor scan these are resolved file paths.
    std::vector<IncludeInfo> includes;

    /// Dependent module names.
    std::vector<std::string> modules;
};

/// Shared cache for dependency directives across multiple scan invocations.
struct SharedScanCache {
    struct CachedEntry {
        /// The source content of the file (kept alive for token references).
        std::string source;

        /// Scanned tokens.
        llvm::SmallVector<clang::dependency_directives_scan::Token> tokens;

        /// Scanned directives (referencing tokens above).
        llvm::SmallVector<clang::dependency_directives_scan::Directive> directives;
    };

    /// path -> cached scan result.
    llvm::StringMap<CachedEntry> entries;
};

/// Quick lexer-based scan for module name and include file names.
/// If module declaration is inside #if/#ifdef, sets need_preprocess=true
/// and module_name will be empty.
ScanResult scan(llvm::StringRef content);

/// Precise preprocessing-based scan. Keeps all directives including #define
/// and conditionals. Used for lazy module dependency resolution.
ScanResult scan_precise(llvm::ArrayRef<const char*> arguments,
                        llvm::StringRef directory,
                        llvm::StringRef content = {},
                        SharedScanCache* cache = nullptr,
                        llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs = nullptr);

/// Lightweight preprocessor-based fallback for resolving module declarations
/// inside conditional directives (#if/#ifdef). When the quick `scan()` detects
/// `need_preprocess=true`, this function runs clang's preprocessor to evaluate
/// the conditions and extract the actual module name.
///
/// Much cheaper than `scan_precise()`: stops lexing as soon as the module
/// declaration is found, so it only processes the file preamble (global module
/// fragment + conditionals around the module declaration). Only populates
/// `module_name` and `is_interface_unit` in the returned ScanResult.
ScanResult scan_module_decl(llvm::ArrayRef<const char*> arguments,
                            llvm::StringRef directory,
                            llvm::StringRef content = {},
                            SharedScanCache* cache = nullptr,
                            llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs = nullptr);

/// Compute preamble bound (moved from compile/preamble).
std::uint32_t compute_preamble_bound(llvm::StringRef content);

std::vector<std::uint32_t> compute_preamble_bounds(llvm::StringRef content);

/// Check if the preamble region contains only syntactically complete directives.
/// Returns false if any #include/#import has an unclosed "" or <>, or any
/// C++20 module statement (import/export) is missing a trailing ';',
/// indicating the user is still typing and PCH/PCM rebuild should be deferred.
bool is_preamble_complete(llvm::StringRef content, std::uint32_t bound);

}  // namespace clice


#include "test/test.h"
#include "syntax/scan.h"

namespace clice::testing {
namespace {

TEST_SUITE(Scan) {

// === scan() tests ===

TEST_CASE(BasicIncludes) {
    auto result = scan(R"(
#include <vector>
#include "foo/bar.h"
int x = 1;
)");

    ASSERT_EQ(result.includes.size(), 2u);
    EXPECT_EQ(result.includes[0].path, "vector");
    EXPECT_TRUE(result.includes[0].is_angled);
    EXPECT_FALSE(result.includes[0].conditional);
    EXPECT_EQ(result.includes[1].path, "foo/bar.h");
    EXPECT_FALSE(result.includes[1].is_angled);
    EXPECT_FALSE(result.includes[1].conditional);
    EXPECT_TRUE(result.module_name.empty());
}

TEST_CASE(ConditionalIncludes) {
    auto result = scan(R"(
#include <always.h>
#ifdef FOO
#include <conditional.h>
#endif
#include <after.h>
)");

    ASSERT_EQ(result.includes.size(), 3u);
    EXPECT_EQ(result.includes[0].path, "always.h");
    EXPECT_FALSE(result.includes[0].conditional);
    EXPECT_EQ(result.includes[1].path, "conditional.h");
    EXPECT_TRUE(result.includes[1].conditional);
    EXPECT_EQ(result.includes[2].path, "after.h");
    EXPECT_FALSE(result.includes[2].conditional);
}

TEST_CASE(NestedConditionals) {
    auto result = scan(R"(
#ifdef A
#ifdef B
#include <nested.h>
#endif
#include <outer.h>
#endif
#include <top.h>
)");

    ASSERT_EQ(result.includes.size(), 3u);
    EXPECT_EQ(result.includes[0].path, "nested.h");
    EXPECT_TRUE(result.includes[0].conditional);
    EXPECT_EQ(result.includes[1].path, "outer.h");
    EXPECT_TRUE(result.includes[1].conditional);
    EXPECT_EQ(result.includes[2].path, "top.h");
    EXPECT_FALSE(result.includes[2].conditional);
}

TEST_CASE(ModuleDeclaration) {
    auto result = scan(R"(
module;
#include
export module my.module;
)");

    EXPECT_EQ(result.module_name, "my.module");
    EXPECT_TRUE(result.is_interface_unit);
    EXPECT_FALSE(result.need_preprocess);
    ASSERT_EQ(result.includes.size(), 1u);
    EXPECT_EQ(result.includes[0].path, "header.h");
    EXPECT_TRUE(result.includes[0].is_angled);
}

TEST_CASE(ModulePartition) {
    auto result = scan(R"(
module my.module:part;
)");

    EXPECT_EQ(result.module_name, "my.module:part");
    EXPECT_FALSE(result.is_interface_unit);
}

TEST_CASE(ModuleImplementation) {
    auto result = scan(R"(
module my.module;
)");

    EXPECT_EQ(result.module_name, "my.module");
    EXPECT_FALSE(result.is_interface_unit);
}

TEST_CASE(ConditionalModule) {
    auto result = scan(R"(
#ifdef USE_MODULES
export module foo;
#endif
)");

    EXPECT_TRUE(result.module_name.empty());
    EXPECT_TRUE(result.need_preprocess);
}

TEST_CASE(GlobalModuleFragment) {
    auto result = scan(R"(
module;
export module test;
)");

    EXPECT_EQ(result.module_name, "test");
    EXPECT_TRUE(result.is_interface_unit);
}

TEST_CASE(EmptyContent) {
    auto result = scan("");
    EXPECT_TRUE(result.includes.empty());
    EXPECT_TRUE(result.module_name.empty());
    EXPECT_FALSE(result.need_preprocess);
}

TEST_CASE(NoDirectives) {
    auto result = scan(R"(
int main() {
    return 0;
}
)");

    EXPECT_TRUE(result.includes.empty());
    EXPECT_TRUE(result.module_name.empty());
    EXPECT_FALSE(result.is_interface_unit);
    EXPECT_FALSE(result.need_preprocess);
}

// === scan_precise() tests ===

TEST_CASE(PreciseBasic) {
    auto vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
    auto main_path = TestVFS::path("main.cpp");
    vfs->add("main.cpp", R"(
#include "header.h"
int main() {}
)");
    vfs->add("header.h", R"(
#pragma once
int x = 1;
)");

    auto args = std::vector<const char*>{"clang++", "-std=c++20", main_path.c_str()};
    auto result = scan_precise(args, TestVFS::root(), {}, nullptr, vfs);

    ASSERT_EQ(result.includes.size(), 1u);
    EXPECT_FALSE(result.includes[0].not_found);
    EXPECT_FALSE(result.includes[0].conditional);
}

TEST_CASE(PreciseConditionalWithDefine) {
    auto vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
    auto main_path = TestVFS::path("main.cpp");
    vfs->add("main.cpp", R"(
#define USE_FOO
#ifdef USE_FOO
#include "foo.h"
#endif
#ifndef USE_FOO
#include "bar.h"
#endif
)");
    vfs->add("foo.h");
    vfs->add("bar.h");

    auto args = std::vector<const char*>{"clang++", "-std=c++20", main_path.c_str()};
    auto result = scan_precise(args, TestVFS::root(), {}, nullptr, vfs);

    // Precise mode evaluates conditionals: only foo.h should be included.
    ASSERT_EQ(result.includes.size(), 1u);
    EXPECT_TRUE(result.includes[0].conditional);
    EXPECT_TRUE(result.includes[0].path.find("foo.h") != std::string::npos);
}

TEST_CASE(PreciseWithContent) {
    auto vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
    auto main_path = TestVFS::path("main.cpp");
    vfs->add("main.cpp");
    vfs->add("header.h");

    auto args = std::vector<const char*>{"clang++", "-std=c++20", main_path.c_str()};
    auto result = scan_precise(args, TestVFS::root(), R"(#include "header.h")", nullptr, vfs);

    ASSERT_EQ(result.includes.size(), 1u);
    EXPECT_FALSE(result.includes[0].not_found);
}

};  // TEST_SUITE(Scan)

TEST_SUITE(PreambleBound) {

TEST_CASE(Empty) {
    EXPECT_EQ(compute_preamble_bound(""), 0u);
}

TEST_CASE(NoDirectives) {
    EXPECT_EQ(compute_preamble_bound("int x = 1;"), 0u);
}

TEST_CASE(SingleInclude) {
    llvm::StringRef src = R"(
#include <vector>
int x;
)";
    auto bound = compute_preamble_bound(src);
    EXPECT_TRUE(bound > 0u);
    EXPECT_TRUE(bound <= src.find("int"));
}

TEST_CASE(MultipleDirectives) {
    llvm::StringRef src = R"(
#include <vector>
#include <string>
#define FOO 1
int x;
)";
    auto bound = compute_preamble_bound(src);
    EXPECT_TRUE(bound > src.find("#define"));
}

TEST_CASE(GlobalModuleFragment) {
    llvm::StringRef src = R"(
module;
#include <vector>
export module foo;
)";
    auto bound = compute_preamble_bound(src);
    EXPECT_TRUE(bound > 0u);
    EXPECT_TRUE(bound < src.size());
}

TEST_CASE(BoundsVector) {
    llvm::StringRef src = R"(
#include
#include
int x;
)";
    auto bounds = compute_preamble_bounds(src);
    ASSERT_EQ(bounds.size(), 2u);
    EXPECT_TRUE(bounds[0] < bounds[1]);
}

TEST_CASE(BoundsWithModuleFragment) {
    llvm::StringRef src = R"(
module;
#include
#include
export module foo;
)";
    auto bounds = compute_preamble_bounds(src);
    // module; + two #include = 3 bounds.
    ASSERT_EQ(bounds.size(), 3u);
    EXPECT_TRUE(bounds[0] < bounds[1]);
    EXPECT_TRUE(bounds[1] < bounds[2]);
}

TEST_CASE(StopsAtCode) {
    llvm::StringRef src = R"(
#include
int x;
#include
)";
    auto bounds = compute_preamble_bounds(src);
    ASSERT_EQ(bounds.size(), 1u);
}

TEST_CASE(ConditionalDirectives) {
    llvm::StringRef src = R"(
#ifndef GUARD
#define GUARD
#include
#endif
int x;
)";
    auto bound = compute_preamble_bound(src);
    EXPECT_TRUE(bound > src.find("#endif"));
}

};  // TEST_SUITE(PreambleBound)

TEST_SUITE(PreambleComplete) {

// --- #include completeness ---

TEST_CASE(CompleteQuotedInclude) {
    llvm::StringRef content = "#include \"foo.h\"\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

TEST_CASE(CompleteAngledInclude) {
    llvm::StringRef content = "#include <vector>\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

TEST_CASE(IncompleteQuotedInclude) {
    llvm::StringRef content = "#include \"foo\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_FALSE(is_preamble_complete(content, bound));
}

TEST_CASE(IncompleteAngledInclude) {
    llvm::StringRef content = "#include <sys/\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_FALSE(is_preamble_complete(content, bound));
}

TEST_CASE(IncludeWithNoPath) {
    llvm::StringRef content = "#include \nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_FALSE(is_preamble_complete(content, bound));
}

TEST_CASE(IncludeMacroUsage) {
    llvm::StringRef content = "#include FOO\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

TEST_CASE(MultipleIncludesAllComplete) {
    llvm::StringRef content = "#include <vector>\n#include \"foo.h\"\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

TEST_CASE(MultipleIncludesLastIncomplete) {
    llvm::StringRef content = "#include <vector>\n#include \"foo\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_FALSE(is_preamble_complete(content, bound));
}

// --- C++20 module statements ---
// Note: compute_preamble_bound does not include import/export lines in its
// bound, so we pass manual bounds covering the relevant lines.

TEST_CASE(CompleteImport) {
    llvm::StringRef content = "import std;\nint x;";
    // Bound covers "import std;\n".
    EXPECT_TRUE(is_preamble_complete(content, 12));
}

TEST_CASE(ImportMissingSemicolon) {
    llvm::StringRef content = "import std\nint x;";
    // Bound covers "import std\n".
    EXPECT_FALSE(is_preamble_complete(content, 11));
}

TEST_CASE(ImportWithNothing) {
    llvm::StringRef content = "import \nint x;";
    // Bound covers "import \n".
    EXPECT_FALSE(is_preamble_complete(content, 8));
}

TEST_CASE(CompleteExportModule) {
    llvm::StringRef content = "export module foo;\nint x;";
    // Bound covers "export module foo;\n".
    EXPECT_TRUE(is_preamble_complete(content, 19));
}

TEST_CASE(ExportModuleMissingSemicolon) {
    llvm::StringRef content = "export module foo\nint x;";
    // Bound covers "export module foo\n".
    EXPECT_FALSE(is_preamble_complete(content, 18));
}

TEST_CASE(CompleteExportImport) {
    llvm::StringRef content = "export import std;\nint x;";
    // Bound covers "export import std;\n".
    EXPECT_TRUE(is_preamble_complete(content, 19));
}

// --- Edge cases ---

TEST_CASE(EmptyPreamble) {
    llvm::StringRef content = "int x;";
    EXPECT_TRUE(is_preamble_complete(content, 0));
}

TEST_CASE(NonImportIncludeLinesIgnored) {
    llvm::StringRef content = "#define FOO 1\n#ifdef BAR\n#endif\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

TEST_CASE(ImportantDoesNotMatchImport) {
    // "important" starts with "import" but should NOT be treated as an import.
    llvm::StringRef content = "#include <vector>\nint x;";
    auto bound = compute_preamble_bound(content);
    // Manually test with content that has "important" within the preamble region.
    // Since compute_preamble_bound won't include non-directive lines, we test
    // is_preamble_complete directly with a crafted bound.
    llvm::StringRef crafted = "important = 1;\n";
    EXPECT_TRUE(is_preamble_complete(crafted, crafted.size()));
}

TEST_CASE(PreprocessorDirectivesIgnored) {
    llvm::StringRef content = "#ifdef FOO\n#define BAR 1\n#endif\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

TEST_CASE(MixedIncludeAndImportAllComplete) {
    llvm::StringRef content = "#include <vector>\nimport std;\nint x;";
    auto bound = compute_preamble_bound(content);
    EXPECT_TRUE(is_preamble_complete(content, bound));
}

};  // TEST_SUITE(PreambleComplete)

}  // namespace
}  // namespace clice::testing


#include "


#pragma once
int my_func();


#pragma once
int nested_func();


s


t


d


unexpected namespace name 'std': expected expression

expected ';' after expression


expected ';' after expression


c


o


u


H


e


l


w


r


n