#include "server/master_server.h"

#include <algorithm>
#include <format>
#include <string>
#include <type_traits>
#include <variant>

#include "semantic/symbol_kind.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "support/logging.h"

#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/meta/enum.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"

namespace clice {

namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
namespace refl = kota::meta;
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::JsonPeer::RequestContext;
using serde_raw = kota::codec::RawValue;

/// Serialize a value to a JSON RawValue using LSP config.
template <typename T>
static serde_raw to_raw(const T& value) {
    auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
    return serde_raw{json ? std::move(*json) : "null"};
}

MasterServer::MasterServer(kota::event_loop& loop,
                           kota::ipc::JsonPeer& peer,
                           std::string self_path) :
    loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
    indexer(loop,
            workspace,
            sessions,
            pool,
            compiler,
            [this](uint32_t proj_path_id) {
                // Bridge project-level path_id to server-level path_id.
                // The two PathPools may assign different IDs to the same path.
                auto path = workspace.project_index.path_pool.path(proj_path_id);
                auto server_id = workspace.path_pool.intern(path);
                return sessions.contains(server_id);
            }),
    self_path(std::move(self_path)) {}

MasterServer::~MasterServer() = default;

void MasterServer::load_workspace() {
    if(workspace_root.empty())
        return;

    auto& cfg = workspace.config.project;

    if(!cfg.cache_dir.empty()) {
        auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
        if(ec) {
            LOG_WARN("Failed to create cache directory {}: {}",
                     std::string_view(cfg.cache_dir),
                     ec.message());
        } else {
            LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
        }

        for(auto* subdir: {"cache/pch", "cache/pcm"}) {
            auto dir = path::join(cfg.cache_dir, subdir);
            if(auto ec2 = llvm::sys::fs::create_directories(dir))
                LOG_WARN("Failed to create {}: {}", dir, ec2.message());
        }

        workspace.cleanup_cache();
        workspace.load_cache();
    }

    // Discover compile_commands.json: configured paths first, then auto-scan.
    std::string cdb_path;
    for(auto& configured: cfg.compile_commands_paths) {
        // Each entry can be a file or a directory containing compile_commands.json.
        if(llvm::sys::fs::is_directory(configured)) {
            auto candidate = path::join(configured, "compile_commands.json");
            if(llvm::sys::fs::exists(candidate)) {
                cdb_path = std::move(candidate);
                break;
            }
        } else if(llvm::sys::fs::exists(configured)) {
            cdb_path = configured;
            break;
        } else {
            LOG_WARN("Configured compile_commands_path not found: {}", configured);
        }
    }

    // Auto-scan: workspace root + all immediate subdirectories.
    if(cdb_path.empty()) {
        auto try_candidate = [&](llvm::StringRef dir) -> bool {
            auto candidate = path::join(dir, "compile_commands.json");
            if(llvm::sys::fs::exists(candidate)) {
                cdb_path = std::move(candidate);
                return true;
            }
            return false;
        };

        if(!try_candidate(workspace_root)) {
            std::error_code ec;
            for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
                it.increment(ec)) {
                if(it->type() == llvm::sys::fs::file_type::directory_file) {
                    if(try_candidate(it->path()))
                        break;
                }
            }
        }
    }

    if(cdb_path.empty()) {
        LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
        peer.send_notification(protocol::LogMessageParams{
            protocol::MessageType::Warning,
            std::format("No compile_commands.json found in workspace {}", workspace_root)});
        return;
    }

    auto count = workspace.cdb.load(cdb_path);
    LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);

    auto report = scan_dependency_graph(workspace.cdb,
                                        workspace.path_pool,
                                        workspace.dep_graph,
                                        /*cache=*/nullptr,
                                        [this](llvm::StringRef path,
                                               std::vector<std::string>& append,
                                               std::vector<std::string>& remove) {
                                            workspace.config.match_rules(path, append, remove);
                                        });
    workspace.dep_graph.build_reverse_map();

    auto unresolved = report.includes_found - report.includes_resolved;
    double accuracy =
        report.includes_found > 0
            ? 100.0 * static_cast<double>(report.includes_resolved) / report.includes_found
            : 100.0;
    LOG_INFO(
        "Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves",
        report.elapsed_ms,
        report.total_files,
        report.source_files,
        report.header_files,
        report.total_edges,
        report.includes_resolved,
        report.includes_found,
        accuracy,
        report.waves);
    if(unresolved > 0)
        LOG_WARN("{} unresolved includes", unresolved);

    workspace.build_module_map();
    indexer.load(cfg.index_dir);

    if(*cfg.enable_indexing) {
        for(auto& entry: workspace.cdb.get_entries()) {
            auto file = workspace.cdb.resolve_path(entry.file);
            auto server_id = workspace.path_pool.intern(file);
            indexer.enqueue(server_id);
        }
        indexer.schedule();
    }

    compiler.init_compile_graph();
}

void MasterServer::register_handlers() {
    using StringVec = std::vector<std::string>;

    peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
                        -> RequestResult<protocol::InitializeParams> {
        if(lifecycle != ServerLifecycle::Uninitialized) {
            co_return kota::outcome_error(protocol::Error{"Server already initialized"});
        }

        auto& init = params.lsp__initialize_params;
        if(init.root_uri.has_value()) {
            workspace_root = uri_to_path(*init.root_uri);
        }

        // Capture initializationOptions as raw JSON for config loading.
        if(init.initialization_options.has_value()) {
            auto json =
                kota::codec::json::to_json<kota::ipc::lsp_config>(*init.initialization_options);
            if(json)
                init_options_json = std::move(*json);
        }

        lifecycle = ServerLifecycle::Initialized;
        LOG_INFO("Initialized with workspace: {}", workspace_root);

        protocol::InitializeResult result;
        auto& caps = result.capabilities;

        caps.text_document_sync = protocol::TextDocumentSyncOptions{
            .open_close = true,
            .change = protocol::TextDocumentSyncKind::Incremental,
            .save = protocol::variant<protocol::boolean, protocol::SaveOptions>{true},
        };
        caps.workspace = protocol::WorkspaceOptions{};
        caps.workspace->workspace_folders = protocol::WorkspaceFoldersServerCapabilities{
            .supported = true,
            .change_notifications = true,
        };

        caps.hover_provider = true;
        caps.completion_provider = protocol::CompletionOptions{
            .trigger_characters = StringVec{".", "<", ">", ":", "\"", "/", "*"},
        };
        caps.signature_help_provider = protocol::SignatureHelpOptions{
            .trigger_characters = StringVec{"(", ")", "{", "}", "<", ">", ","},
        };
        /// FIXME: In the future, we would support work done progress.
        caps.declaration_provider = protocol::DeclarationOptions{
            .work_done_progress = false,
        };
        caps.definition_provider = protocol::DefinitionOptions{
            .work_done_progress = false,
        };
        caps.implementation_provider = protocol::ImplementationOptions{
            .work_done_progress = false,
        };
        caps.type_definition_provider = protocol::TypeDefinitionOptions{
            .work_done_progress = false,
        };
        caps.references_provider = protocol::ReferenceOptions{
            .work_done_progress = false,
        };
        caps.document_symbol_provider = true;
        caps.document_link_provider = protocol::DocumentLinkOptions{};
        caps.code_action_provider = true;
        caps.folding_range_provider = true;
        caps.inlay_hint_provider = true;
        caps.call_hierarchy_provider = true;
        caps.type_hierarchy_provider = true;
        caps.workspace_symbol_provider = true;

        protocol::SemanticTokensOptions sem_opts;
        {
            auto lower_first = [](std::string_view name) -> std::string {
                std::string s(name);
                if(!s.empty()) {
                    s[0] = static_cast<char>(std::tolower(static_cast<unsigned char>(s[0])));
                }
                return s;
            };

            auto to_names = [&](auto names) {
                return std::ranges::to<std::vector>(names | std::views::transform(lower_first));
            };

            sem_opts.legend = protocol::SemanticTokensLegend{
                to_names(refl::reflection<SymbolKind::Kind>::member_names),
                to_names(refl::reflection<SymbolModifiers::Kind>::member_names),
            };
        }
        sem_opts.full = true;
        result.capabilities.semantic_tokens_provider = std::move(sem_opts);

        protocol::ServerInfo info;
        info.name = "clice";
        info.version = "0.1.0";
        result.server_info = std::move(info);

        co_return result;
    });

    peer.on_notification([this](const protocol::InitializedParams& params) {
        // Config priority: initializationOptions > clice.toml > defaults.
        // Load the workspace config (with defaults applied) first, then overlay
        // any initializationOptions on top so fields not mentioned in the JSON
        // keep the values from clice.toml — kotatsu's deserializer only touches
        // fields that are present in the input.
        std::string config_warning;
        workspace.config = Config::load_from_workspace(workspace_root, &config_warning);
        if(!config_warning.empty())
            peer.send_notification(
                protocol::LogMessageParams{protocol::MessageType::Warning, config_warning});
        if(!init_options_json.empty()) {
            if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
                LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
                peer.send_notification(protocol::LogMessageParams{
                    protocol::MessageType::Warning,
                    std::format("Failed to apply initializationOptions: {}",
                                ov.error().to_string())});
            } else {
                // Re-run apply_defaults so overridden strings get workspace
                // substitution and `compiled_rules` is rebuilt if `rules`
                // changed. Defaults are gated on zero/empty sentinels, so
                // existing values from the overlay are preserved.
                workspace.config.apply_defaults(workspace_root);
                LOG_INFO("Applied initializationOptions overlay");
            }
            init_options_json.clear();
        }

        auto& cfg = workspace.config.project;

        if(!cfg.logging_dir.empty()) {
            auto now = std::chrono::system_clock::now();
            auto pid = llvm::sys::Process::getProcessId();
            auto session_dir =
                path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
            logging::file_logger("master", session_dir, logging::options);
            session_log_dir = session_dir;
        }

        LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
                 cfg.stateful_worker_count.value,
                 cfg.stateless_worker_count.value,
                 *cfg.idle_timeout_ms);

        WorkerPoolOptions pool_opts;
        pool_opts.self_path = self_path;
        pool_opts.stateful_count = cfg.stateful_worker_count;
        pool_opts.stateless_count = cfg.stateless_worker_count;
        pool_opts.worker_memory_limit = cfg.worker_memory_limit;
        pool_opts.log_dir = session_log_dir;
        if(!pool.start(pool_opts)) {
            LOG_ERROR("Failed to start worker pool");
            peer.send_notification(protocol::LogMessageParams{protocol::MessageType::Error,
                                                              "Failed to start worker pool"});
            return;
        }

        lifecycle = ServerLifecycle::Ready;

        compiler.on_indexing_needed = [this]() {
            indexer.schedule();
        };

        indexer.set_peer(&peer);
        indexer.set_max_concurrency(cfg.stateless_worker_count.value);

        load_workspace();
    });

    peer.on_request(
        [this](RequestContext& ctx,
               const protocol::ShutdownParams& params) -> RequestResult<protocol::ShutdownParams> {
            lifecycle = ServerLifecycle::ShuttingDown;
            LOG_INFO("Shutdown requested");
            co_return nullptr;
        });

    peer.on_notification([this](const protocol::ExitParams& params) {
        lifecycle = ServerLifecycle::Exited;
        LOG_INFO("Exit notification received");

        indexer.save(workspace.config.project.index_dir);
        workspace.save_cache();

        loop.schedule([this]() -> kota::task<> {
            co_await pool.stop();
            loop.stop();
        }());
    });

    /// Document lifecycle — handled directly by MasterServer.

    peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {
        if(lifecycle != ServerLifecycle::Ready)
            return;

        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);

        auto [it, inserted] = sessions.try_emplace(path_id);
        auto& session = it->second;
        if(!inserted) {
            // DenseMap tombstone may retain stale data — reset to a fresh Session.
            session = Session{};
        }
        session.path_id = path_id;
        session.version = params.text_document.version;
        session.text = params.text_document.text;
        session.generation++;

        LOG_DEBUG("didOpen: {} (v{})", path, params.text_document.version);
    });

    peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) {
        if(lifecycle != ServerLifecycle::Ready)
            return;

        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);

        auto it = sessions.find(path_id);
        if(it == sessions.end())
            return;

        auto& session = it->second;
        session.version = params.text_document.version;

        for(auto& change: params.content_changes) {
            std::visit(
                [&](auto& c) {
                    using T = std::remove_cvref_t<decltype(c)>;
                    if constexpr(std::is_same_v<T,
                                                protocol::TextDocumentContentChangeWholeDocument>) {
                        session.text = c.text;
                    } else {
                        auto& range = c.range;
                        lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16);
                        auto start = mapper.to_offset(range.start);
                        auto end = mapper.to_offset(range.end);
                        if(start && end && *start <= *end) {
                            session.text.replace(*start, *end - *start, c.text);
                        }
                    }
                },
                change);
        }

        session.generation++;
        session.ast_dirty = true;

        LOG_DEBUG("didChange: path={} version={} gen={}",
                  path,
                  session.version,
                  session.generation);

        worker::DocumentUpdateParams update;
        update.path = path;
        update.version = session.version;
        pool.notify_stateful(path_id, update);
    });

    peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) {
        if(lifecycle != ServerLifecycle::Ready)
            return;

        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);

        workspace.on_file_closed(path_id);
        pool.notify_stateful(path_id, worker::EvictParams{path});

        // Clear diagnostics for the closed file.
        protocol::PublishDiagnosticsParams diag_params;
        diag_params.uri = params.text_document.uri;
        peer.send_notification(diag_params);

        sessions.erase(path_id);

        indexer.enqueue(path_id);
        indexer.schedule();

        LOG_DEBUG("didClose: {}", path);
    });

    peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
        if(lifecycle != ServerLifecycle::Ready)
            return;

        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);

        auto dirtied = workspace.on_file_saved(path_id);
        for(auto dirty_id: dirtied) {
            if(auto sit = sessions.find(dirty_id); sit != sessions.end()) {
                sit->second.ast_dirty = true;
            } else {
                indexer.enqueue(dirty_id);
            }
        }

        // Invalidate header contexts for sessions whose host is this file.
        for(auto& [hdr_id, session]: sessions) {
            if(session.header_context && session.header_context->host_path_id == path_id) {
                session.header_context.reset();
                session.ast_dirty = true;
            }
        }

        indexer.schedule();

        LOG_DEBUG("didSave: {}", path);
    });

    /// Feature requests — stateful forwarding.

    peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult {
        auto path = uri_to_path(params.text_document_position_params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        if(sit == sessions.end())
            co_await kota::fail("Document not open");
        co_return co_await compiler.forward_query(worker::QueryKind::Hover,
                                                  sit->second,
                                                  params.text_document_position_params.position);
    });

    peer.on_request([this](RequestContext& ctx,
                           const protocol::SemanticTokensParams& params) -> RawResult {
        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        if(sit == sessions.end())
            co_await kota::fail("Document not open");
        co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
    });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult {
            auto path = uri_to_path(params.text_document.uri);
            auto path_id = workspace.path_pool.intern(path);
            auto sit = sessions.find(path_id);
            if(sit == sessions.end())
                co_await kota::fail("Document not open");
            co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
                                                      sit->second,
                                                      {},
                                                      params.range);
        });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult {
            auto path = uri_to_path(params.text_document.uri);
            auto path_id = workspace.path_pool.intern(path);
            auto sit = sessions.find(path_id);
            if(sit == sessions.end())
                co_await kota::fail("Document not open");
            co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second);
        });

    peer.on_request([this](RequestContext& ctx,
                           const protocol::DocumentSymbolParams& params) -> RawResult {
        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        if(sit == sessions.end())
            co_await kota::fail("Document not open");
        co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
    });

    peer.on_request([this](RequestContext& ctx,
                           const protocol::DocumentLinkParams& params) -> RawResult {
        auto path = uri_to_path(params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        if(sit == sessions.end())
            co_await kota::fail("Document not open");
        auto& session = sit->second;
        auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
        if(!result.has_value())
            co_return serde_raw{"null"};
        // Merge document links from PCH if available.
        auto& links = result.value();
        // Re-lookup session after co_await since iterators may be invalidated.
        auto sit2 = sessions.find(path_id);
        if(sit2 != sessions.end() && sit2->second.pch_ref) {
            auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
            if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
                auto& pch_json = pch_it->second.document_links_json;
                // Merge two JSON arrays.
                if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
                    // "[a,b]" + "[c,d]" -> "[a,b,c,d]"
                    links.data.pop_back();  // remove trailing ']'
                    links.data += ',';
                    links.data.append(pch_json.begin() + 1, pch_json.end());  // skip '['
                } else {
                    links.data = pch_json;
                }
            }
        }
        co_return std::move(links);
    });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
            auto path = uri_to_path(params.text_document.uri);
            auto path_id = workspace.path_pool.intern(path);
            auto sit = sessions.find(path_id);
            if(sit == sessions.end())
                co_await kota::fail("Document not open");
            co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
        });

    /// Helper: resolve URI to path, path_id, and Session pointer.
    auto resolve_uri = [this](const std::string& uri) {
        struct Result {
            std::string path;
            std::uint32_t path_id;
            Session* session;
        };
        auto path = uri_to_path(uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        Session* session = (sit != sessions.end()) ? &sit->second : nullptr;
        return Result{std::move(path), path_id, session};
    };

    auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) {
        auto [path, path_id, session] = resolve_uri(uri);
        return indexer.lookup_symbol(uri, path, pos, session);
    };

    auto query_at = [this, resolve_uri](const std::string& uri,
                                        const protocol::Position& pos,
                                        RelationKind kind) -> std::vector<protocol::Location> {
        auto [path, path_id, session] = resolve_uri(uri);
        return indexer.query_relations(path, pos, kind, session);
    };

    auto resolve_item =
        [this,
         resolve_uri](const std::string& uri,
                      const protocol::Range& range,
                      const std::optional<protocol::LSPAny>& data) -> std::optional<SymbolInfo> {
        auto [path, path_id, session] = resolve_uri(uri);
        return indexer.resolve_hierarchy_item(uri, path, range, data, session);
    };

    /// Feature requests — index-based with AST fallback.

    peer.on_request([this, query_at](RequestContext& ctx,
                                     const protocol::DefinitionParams& params) -> RawResult {
        auto& uri = params.text_document_position_params.text_document.uri;
        auto& pos = params.text_document_position_params.position;

        auto result = query_at(uri, pos, RelationKind::Definition);
        if(!result.empty()) {
            co_return to_raw(result);
        }

        auto path = uri_to_path(uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        if(sit == sessions.end())
            co_await kota::fail("Document not open");
        co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
                                                  sit->second,
                                                  pos);
    });

    peer.on_request([this, query_at](RequestContext& ctx,
                                     const protocol::ReferenceParams& params) -> RawResult {
        auto& uri = params.text_document_position_params.text_document.uri;
        auto& pos = params.text_document_position_params.position;

        auto locations = query_at(uri, pos, RelationKind::Reference);

        if(params.context.include_declaration) {
            auto defs = query_at(uri, pos, RelationKind::Definition);
            locations.insert(locations.end(),
                             std::make_move_iterator(defs.begin()),
                             std::make_move_iterator(defs.end()));
        }

        if(locations.empty())
            co_return serde_raw{"null"};
        co_return to_raw(locations);
    });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::TypeDefinitionParams& params) -> RawResult {
            co_return serde_raw{"null"};
        });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::ImplementationParams& params) -> RawResult {
            co_return serde_raw{"null"};
        });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::DeclarationParams& params) -> RawResult {
            co_return serde_raw{"null"};
        });

    /// Feature requests — stateless forwarding.

    peer.on_request(
        [this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult {
            auto path = uri_to_path(params.text_document_position_params.text_document.uri);
            auto path_id = workspace.path_pool.intern(path);
            auto sit = sessions.find(path_id);
            if(sit == sessions.end())
                co_await kota::fail("Document not open");
            auto pause = indexer.scoped_pause();
            auto result =
                co_await compiler.handle_completion(params.text_document_position_params.position,
                                                    sit->second);
            co_return std::move(result);
        });

    peer.on_request([this](RequestContext& ctx,
                           const protocol::SignatureHelpParams& params) -> RawResult {
        auto path = uri_to_path(params.text_document_position_params.text_document.uri);
        auto path_id = workspace.path_pool.intern(path);
        auto sit = sessions.find(path_id);
        if(sit == sessions.end())
            co_await kota::fail("Document not open");
        auto pause = indexer.scoped_pause();
        auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
                                                      params.text_document_position_params.position,
                                                      sit->second);
        co_return std::move(result);
    });

    /// Hierarchy queries — index-based.

    peer.on_request(
        [this, lookup_at](RequestContext& ctx,
                          const protocol::CallHierarchyPrepareParams& params) -> RawResult {
            auto& uri = params.text_document_position_params.text_document.uri;
            auto& pos = params.text_document_position_params.position;

            auto info = lookup_at(uri, pos);
            if(!info)
                co_return serde_raw{"null"};
            if(!(info->kind == SymbolKind::Function || info->kind == SymbolKind::Method))
                co_return serde_raw{"null"};

            std::vector<protocol::CallHierarchyItem> items;
            items.push_back(Indexer::build_call_hierarchy_item(*info));
            co_return to_raw(items);
        });

    peer.on_request([this, resolve_item](
                        RequestContext& ctx,
                        const protocol::CallHierarchyIncomingCallsParams& params) -> RawResult {
        auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
        if(!info)
            co_await kota::fail("Failed to resolve call hierarchy item");
        auto results = indexer.find_incoming_calls(info->hash);
        co_return to_raw(results);
    });

    peer.on_request([this, resolve_item](
                        RequestContext& ctx,
                        const protocol::CallHierarchyOutgoingCallsParams& params) -> RawResult {
        auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
        if(!info)
            co_await kota::fail("Failed to resolve call hierarchy item");
        auto results = indexer.find_outgoing_calls(info->hash);
        co_return to_raw(results);
    });

    peer.on_request(
        [this, lookup_at](RequestContext& ctx,
                          const protocol::TypeHierarchyPrepareParams& params) -> RawResult {
            auto& uri = params.text_document_position_params.text_document.uri;
            auto& pos = params.text_document_position_params.position;

            auto info = lookup_at(uri, pos);
            if(!info)
                co_return serde_raw{"null"};
            if(!(info->kind == SymbolKind::Class || info->kind == SymbolKind::Struct ||
                 info->kind == SymbolKind::Enum || info->kind == SymbolKind::Union))
                co_return serde_raw{"null"};

            std::vector<protocol::TypeHierarchyItem> items;
            items.push_back(Indexer::build_type_hierarchy_item(*info));
            co_return to_raw(items);
        });

    peer.on_request(
        [this, resolve_item](RequestContext& ctx,
                             const protocol::TypeHierarchySupertypesParams& params) -> RawResult {
            auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
            if(!info)
                co_await kota::fail("Failed to resolve type hierarchy item");
            auto results = indexer.find_supertypes(info->hash);
            co_return to_raw(results);
        });

    peer.on_request(
        [this, resolve_item](RequestContext& ctx,
                             const protocol::TypeHierarchySubtypesParams& params) -> RawResult {
            auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
            if(!info)
                co_await kota::fail("Failed to resolve type hierarchy item");
            auto results = indexer.find_subtypes(info->hash);
            co_return to_raw(results);
        });

    peer.on_request(
        [this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult {
            auto results = indexer.search_symbols(params.query);
            co_return to_raw(results);
        });

    /// clice/ extension commands.

    peer.on_request(
        "clice/queryContext",
        [this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult {
            auto path = uri_to_path(params.uri);
            auto path_id = workspace.path_pool.intern(path);
            int offset_val = std::max(0, params.offset.value_or(0));
            constexpr int page_size = 10;

            ext::QueryContextResult result;
            std::vector<ext::ContextItem> all_items;

            auto hosts = workspace.dep_graph.find_host_sources(path_id);
            for(auto host_id: hosts) {
                auto host_path = workspace.path_pool.resolve(host_id);
                auto host_cdb = workspace.cdb.lookup(host_path, {.suppress_logging = true});
                if(host_cdb.empty())
                    continue;
                auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path));
                if(!host_uri_opt)
                    continue;
                ext::ContextItem item;
                item.label = llvm::sys::path::filename(host_path).str();
                item.description = std::string(host_path);
                item.uri = host_uri_opt->str();
                all_items.push_back(std::move(item));
            }

            if(hosts.empty()) {
                auto entries = workspace.cdb.lookup(path, {.suppress_logging = true});
                for(std::size_t i = 0; i < entries.size(); ++i) {
                    auto& cmd = entries[i];
                    auto argv = cmd.to_argv();
                    std::string desc;
                    for(std::size_t j = 0; j < argv.size(); ++j) {
                        llvm::StringRef a(argv[j]);
                        if(a.starts_with("-D") || a.starts_with("-O") || a.starts_with("-std=") ||
                           a.starts_with("-g")) {
                            if(!desc.empty())
                                desc += ' ';
                            desc += argv[j];
                            if((a == "-D" || a == "-O") && j + 1 < argv.size()) {
                                desc += argv[++j];
                            }
                        }
                    }
                    if(desc.empty())
                        desc = std::format("config #{}", i);

                    auto uri_opt = lsp::URI::from_file_path(std::string(path));
                    if(!uri_opt)
                        continue;
                    ext::ContextItem item;
                    item.label = desc;
                    item.description = cmd.resolved.directory.str();
                    item.uri = uri_opt->str();
                    all_items.push_back(std::move(item));
                }
            }

            result.total = static_cast<int>(all_items.size());
            int end = std::min(offset_val + page_size, static_cast<int>(all_items.size()));
            for(int i = offset_val; i < end; ++i) {
                result.contexts.push_back(std::move(all_items[i]));
            }
            co_return to_raw(result);
        });

    peer.on_request(
        "clice/currentContext",
        [this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult {
            auto path = uri_to_path(params.uri);
            auto path_id = workspace.path_pool.intern(path);

            ext::CurrentContextResult result;
            auto sit = sessions.find(path_id);
            if(sit != sessions.end() && sit->second.active_context) {
                auto ctx_path = workspace.path_pool.resolve(*sit->second.active_context);
                auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path));
                if(ctx_uri_opt) {
                    ext::ContextItem item;
                    item.label = llvm::sys::path::filename(ctx_path).str();
                    item.description = std::string(ctx_path);
                    item.uri = ctx_uri_opt->str();
                    result.context = std::move(item);
                }
            }
            co_return to_raw(result);
        });

    peer.on_request(
        "clice/switchContext",
        [this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult {
            auto path = uri_to_path(params.uri);
            auto path_id = workspace.path_pool.intern(path);
            auto context_path = uri_to_path(params.context_uri);
            auto context_path_id = workspace.path_pool.intern(context_path);

            ext::SwitchContextResult result;

            auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true});
            if(context_cdb.empty()) {
                result.success = false;
                co_return to_raw(result);
            }

            auto sit = sessions.find(path_id);
            if(sit == sessions.end()) {
                result.success = false;
                co_return to_raw(result);
            }

            sit->second.active_context = context_path_id;
            sit->second.header_context.reset();
            sit->second.pch_ref.reset();
            sit->second.ast_deps.reset();
            sit->second.ast_dirty = true;

            result.success = true;
            co_return to_raw(result);
        });
}

}  // namespace clice
