//! Server-initiated requests for the operator-facing mock (WOR-1407).
//!
//! A request/response server can never originate traffic, so the client/host
//! conformance lane (WOR-1365) had nothing to assert: you cannot test how a
//! client services `sampling/createMessage`, `elicitation/create`, or
//! `roots/list` if the server under test never sends one. A mock tool can now
//! declare a trigger; calling it makes the serve loop send that request to the
//! client first (on a negative id, so it never collides with the client's own
//! positive request ids) and defer the tool's own response until the client
//! replies. Split out so `mock/mod.rs` stays under the module-size guard.

use anyhow::Result;
use serde_json::{json, Value};
use tokio::io::AsyncWrite;

use super::dispatch::dispatch;
use super::manifest::{MockManifest, MockTool};
use super::{id_key, write_line};

/// A server-initiated request the mock sent to the client and is waiting on.
///
/// Holds enough to release the original `tools/call` once the client's reply
/// lands: the negative id we put on the wire (matched against the reply), plus
/// the original call's id and params so the canned tool response can be
/// dispatched after the round trip.
pub(super) struct PendingServerRequest {
    /// Negative id of the server-initiated request, matched against the
    /// client's reply.
    pub server_id: Value,
    /// Id of the deferred `tools/call`, answered once the client replies.
    pub call_id: Value,
    /// Params of the deferred `tools/call`, replayed through the dispatcher to
    /// build the canned response after the round trip completes.
    pub params: Value,
}

/// Which server-initiated request a triggered tool fires before answering.
pub(super) enum ServerTrigger {
    /// `sampling/createMessage` with the tool's declared params.
    Sampling(Value),
    /// `elicitation/create` with the tool's declared params.
    Elicitation(Value),
    /// `roots/list` (no params).
    Roots,
}

/// The trigger a tool declares, if any. Sampling wins over elicitation wins
/// over roots when more than one is set, so the precedence is deterministic.
pub(super) fn server_trigger_for(tool: &MockTool) -> Option<ServerTrigger> {
    if let Some(params) = &tool.sampling {
        Some(ServerTrigger::Sampling(params.clone()))
    } else if let Some(params) = &tool.elicitation {
        Some(ServerTrigger::Elicitation(params.clone()))
    } else if tool.roots_list {
        Some(ServerTrigger::Roots)
    } else {
        None
    }
}

/// Resolve a `tools/call`'s tool and return its trigger, if it declares one.
pub(super) fn trigger_for_call(manifest: &MockManifest, params: &Value) -> Option<ServerTrigger> {
    let name = params.get("name").and_then(Value::as_str)?;
    let tool = manifest.tools.iter().find(|t| t.name == name)?;
    server_trigger_for(tool)
}

/// Write a server-initiated request to the client on the given (negative) id.
pub(super) async fn emit_server_request<W: AsyncWrite + Unpin>(
    output: &mut W,
    server_id: &Value,
    trigger: &ServerTrigger,
) -> Result<()> {
    let (method, params) = match trigger {
        ServerTrigger::Sampling(p) => ("sampling/createMessage", p.clone()),
        ServerTrigger::Elicitation(p) => ("elicitation/create", p.clone()),
        ServerTrigger::Roots => ("roots/list", json!({})),
    };
    let request = json!({
        "jsonrpc": "2.0",
        "id": server_id,
        "method": method,
        "params": params,
    });
    write_line(output, &request).await
}

/// Match a client's reply against the pending table and, on a hit, release the
/// deferred `tools/call` by dispatching its canned response. An unsolicited or
/// duplicate reply (no matching pending id) is ignored, the way a real server
/// would drop a reply to a request it never sent.
pub(super) async fn resolve_server_reply<W: AsyncWrite + Unpin>(
    reply_id: &Value,
    manifest: &MockManifest,
    pending: &mut Vec<PendingServerRequest>,
    output: &mut W,
) -> Result<()> {
    let key = id_key(reply_id);
    let Some(idx) = pending.iter().position(|p| id_key(&p.server_id) == key) else {
        return Ok(());
    };
    let req = pending.remove(idx);
    let response = dispatch(manifest, "tools/call", &req.params, req.call_id);
    write_line(output, &response).await
}
