// Copyright (c) 2025 [caomengxuan666]

#pragma once

/// @file cxxmcp/protocol/prompt.hpp
/// @brief Prompt discovery and rendering payloads.
///
/// Prompts are server-defined templates retrieved through `prompts/get`.
/// Listing returns prompt metadata and argument definitions; getting a prompt
/// returns concrete messages that can be supplied to a model.

#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "cxxmcp/core/result.hpp"
#include "cxxmcp/protocol/tool.hpp"
#include "cxxmcp/protocol/types.hpp"
#include "cxxmcp/protocol/types_reflect.hpp"

namespace mcp::protocol {

/// @brief Roles allowed on a prompt message.
enum class PromptMessageRole {
  /// Message from the user.
  User,
  /// Message from the assistant (model).
  Assistant,
};

/// @brief Converts a prompt message role to its MCP wire string.
inline std::string_view prompt_message_role_to_string(PromptMessageRole role) {
  switch (role) {
    case PromptMessageRole::User:
      return "user";
    case PromptMessageRole::Assistant:
      return "assistant";
  }
  return "user";
}

/// @brief Parses a prompt message role string.
/// @return Parsed role, or nullopt for unsupported strings.
inline std::optional<PromptMessageRole> prompt_message_role_from_string(
    std::string_view value) {
  if (value == "user") {
    return PromptMessageRole::User;
  }
  if (value == "assistant") {
    return PromptMessageRole::Assistant;
  }
  return std::nullopt;
}

/// @brief Checks whether a string is a valid prompt message role.
inline bool is_valid_prompt_message_role(std::string_view value) {
  return prompt_message_role_from_string(value).has_value();
}

/// @brief Argument accepted by a prompt template.
struct PromptArgument {
  /// Optional human-readable display title.
  std::string title;
  /// Stable argument name used as a key in `prompts/get` arguments.
  std::string name;
  /// Optional human-readable description.
  std::string description;
  /// Whether the caller must provide this argument.
  bool required = false;
  /// Whether `required` was explicitly present on the wire or configured.
  bool required_present = false;
  /// Optional annotations for model or client presentation.
  Json annotations = Json::object();
  /// Optional `_meta` extension object preserved on the wire.
  std::optional<Json> meta;
  /// Unknown JSON members preserved for forward-compatible round trips.
  Json extensions = Json::object();
};

/// @brief Prompt descriptor returned by `prompts/list`.
struct Prompt {
  /// Optional human-readable display title.
  std::string title;
  /// Stable prompt name used by `prompts/get`.
  std::string name;
  /// Optional human-readable description.
  std::string description;
  /// Prompt arguments accepted by this prompt.
  std::vector<PromptArgument> arguments;
  /// Optional icon descriptors for client presentation.
  std::vector<Icon> icons;
  /// Optional annotations for model or client presentation.
  Json annotations = Json::object();
  /// Optional `_meta` extension object preserved on the wire.
  std::optional<Json> meta;
  /// Unknown JSON members preserved for forward-compatible round trips.
  Json extensions = Json::object();
};

/// @brief Fluent builder for prompt descriptors.
class PromptBuilder {
 public:
  explicit PromptBuilder(std::string name) { prompt_.name = std::move(name); }

  PromptBuilder& title(std::string value) {
    prompt_.title = std::move(value);
    return *this;
  }

  PromptBuilder& description(std::string value) {
    prompt_.description = std::move(value);
    return *this;
  }

  PromptBuilder& argument(std::string name, bool required = false,
                          std::string description = {}) {
    PromptArgument argument;
    argument.name = std::move(name);
    argument.description = std::move(description);
    argument.required = required;
    argument.required_present = true;
    prompt_.arguments.push_back(std::move(argument));
    return *this;
  }

  PromptBuilder& argument(PromptArgument argument) {
    prompt_.arguments.push_back(std::move(argument));
    return *this;
  }

  PromptBuilder& icon(Icon value) {
    prompt_.icons.push_back(std::move(value));
    return *this;
  }

  PromptBuilder& annotations(Json value) {
    prompt_.annotations = std::move(value);
    return *this;
  }

  PromptBuilder& meta(Json value) {
    prompt_.meta = std::move(value);
    return *this;
  }

  PromptBuilder& extension(std::string name, Json value) {
    prompt_.extensions[std::move(name)] = std::move(value);
    return *this;
  }

  Prompt build() { return std::move(prompt_); }

 private:
  Prompt prompt_;
};

/// @brief Creates a fluent builder for advertised MCP prompt metadata.
inline PromptBuilder prompt_definition(std::string name) {
  return PromptBuilder(std::move(name));
}

/// @brief Result object for `prompts/list`.
struct PromptsListResult {
  /// Prompts available to the caller.
  std::vector<Prompt> prompts;
  /// Optional cursor for retrieving the next page.
  std::optional<std::string> next_cursor;
  /// Optional `_meta` extension object preserved on the wire.
  std::optional<Json> meta;
  /// Unknown JSON members preserved for forward-compatible round trips.
  Json extensions = Json::object();
  /// Cache time-to-live hint in milliseconds (SEP-2549).
  std::optional<std::int64_t> ttl_ms;
  /// Cache scope hint: "public" or "private" (SEP-2549).
  std::optional<std::string> cache_scope;
};

/// @brief Parameters for `prompts/get`.
struct PromptsGetParams {
  /// Prompt name matching a Prompt descriptor.
  std::string name;
  /// JSON object keyed by prompt argument name.
  Json arguments = Json::object();
  /// Optional `_meta` extension object preserved on the wire.
  std::optional<Json> meta;
  /// Unknown JSON members preserved for forward-compatible round trips.
  Json extensions = Json::object();
};

/// @brief One rendered prompt message.
struct PromptMessage {
  /// Message role understood by the receiving model or peer.
  std::string role;
  /// Message content block.
  ContentBlock content;
  /// Optional `_meta` extension object preserved on the wire.
  std::optional<Json> meta;
  /// Unknown JSON members preserved for forward-compatible round trips.
  Json extensions = Json::object();

  /// @brief Creates a text-only prompt message for the given role.
  static PromptMessage text(std::string role, std::string value) {
    PromptMessage message;
    message.role = std::move(role);
    message.content = ContentBlock::text_content(std::move(value));
    return message;
  }
};

/// @brief Result object for `prompts/get`.
struct PromptsGetResult {
  /// Optional rendered prompt description.
  std::string description;
  /// Ordered messages produced by the prompt.
  std::vector<PromptMessage> messages;
  /// Optional `_meta` extension object preserved on the wire.
  std::optional<Json> meta;
  /// Result type discriminator. "input_required" for MRTR (SEP-2322).
  /// Missing means the result is complete.
  std::optional<std::string> result_type;
  /// Server input requests for MRTR (SEP-2322).
  std::optional<Json> input_requests;
  /// Opaque request state for MRTR retries (SEP-2322).
  std::optional<std::string> request_state;
  /// Unknown JSON members preserved for forward-compatible round trips.
  Json extensions = Json::object();
};

/// @brief Builds an InvalidRequest error for prompt JSON validation failures.
inline core::Error prompt_json_error(std::string message) {
  return core::Error{
      static_cast<int>(ErrorCode::InvalidRequest), std::move(message), {}};
}

/// @brief Serializes a prompt argument descriptor.
inline Json prompt_argument_to_json(const PromptArgument& argument) {
  Json json = Json::object();
  if (!argument.title.empty()) {
    json["title"] = argument.title;
  }
  json["name"] = argument.name;
  if (!argument.description.empty()) {
    json["description"] = argument.description;
  }
  if (argument.required_present || argument.required) {
    json["required"] = argument.required;
  }
  if (!argument.annotations.empty()) {
    json["annotations"] = argument.annotations;
  }
  if (argument.meta.has_value()) {
    json["_meta"] = *argument.meta;
  }
  append_json_extensions(json, argument.extensions);
  return json;
}

/// @brief Parses a prompt argument descriptor.
/// @return Parsed argument or validation error.
inline core::Result<PromptArgument> prompt_argument_from_json(
    const Json& json) {
  if (!json.is_object()) {
    return mcp::core::unexpected(
        prompt_json_error("prompt argument must be an object"));
  }
  if (json.contains("title")) {
    if (!json.at("title").is_string()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt argument title must be a string"));
    }
  }
  if (!json.contains("name") || !json.at("name").is_string()) {
    return mcp::core::unexpected(
        prompt_json_error("prompt argument requires a string name"));
  }

  PromptArgument argument;
  if (json.contains("title")) {
    argument.title = json.at("title").get<std::string>();
  }
  argument.name = json.at("name").get<std::string>();
  if (json.contains("description")) {
    if (!json.at("description").is_string()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt argument description must be a string"));
    }
    argument.description = json.at("description").get<std::string>();
  }
  if (json.contains("required")) {
    if (!json.at("required").is_boolean()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt argument required must be a boolean"));
    }
    argument.required = json.at("required").get<bool>();
    argument.required_present = true;
  }
  if (json.contains("annotations")) {
    if (!json.at("annotations").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt argument annotations must be an object"));
    }
    argument.annotations = json.at("annotations");
  }
  if (json.contains("_meta")) {
    if (!json.at("_meta").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt argument _meta must be an object"));
    }
    argument.meta = json.at("_meta");
  }
  argument.extensions = collect_json_extensions(
      json,
      {"title", "name", "description", "required", "annotations", "_meta"});
  return argument;
}

/// @brief Serializes a prompt descriptor.
inline Json prompt_to_json(const Prompt& prompt) {
  Json json = Json::object();
  if (!prompt.title.empty()) {
    json["title"] = prompt.title;
  }
  json["name"] = prompt.name;
  if (!prompt.description.empty()) {
    json["description"] = prompt.description;
  }
  if (!prompt.arguments.empty()) {
    json["arguments"] = Json::array();
    for (const auto& argument : prompt.arguments) {
      json["arguments"].push_back(prompt_argument_to_json(argument));
    }
  }
  if (!prompt.icons.empty()) {
    json["icons"] = Json::array();
    for (const auto& icon : prompt.icons) {
      json["icons"].push_back(icon_to_json(icon));
    }
  }
  if (!prompt.annotations.empty()) {
    json["annotations"] = prompt.annotations;
  }
  if (prompt.meta.has_value()) {
    json["_meta"] = *prompt.meta;
  }
  append_json_extensions(json, prompt.extensions);
  return json;
}

/// @brief Parses a prompt descriptor.
/// @return Parsed prompt or validation error.
inline core::Result<Prompt> prompt_from_json(const Json& json) {
  if (!json.is_object()) {
    return mcp::core::unexpected(prompt_json_error("prompt must be an object"));
  }
  if (json.contains("title")) {
    if (!json.at("title").is_string()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt title must be a string"));
    }
  }
  if (!json.contains("name") || !json.at("name").is_string()) {
    return mcp::core::unexpected(
        prompt_json_error("prompt requires a string name"));
  }

  Prompt prompt;
  if (json.contains("title")) {
    prompt.title = json.at("title").get<std::string>();
  }
  prompt.name = json.at("name").get<std::string>();
  if (json.contains("description")) {
    if (!json.at("description").is_string()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt description must be a string"));
    }
    prompt.description = json.at("description").get<std::string>();
  }
  if (json.contains("arguments")) {
    if (!json.at("arguments").is_array()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt arguments must be an array"));
    }
    for (const auto& item : json.at("arguments")) {
      const auto argument = prompt_argument_from_json(item);
      if (!argument) {
        return mcp::core::unexpected(argument.error());
      }
      prompt.arguments.push_back(*argument);
    }
  }
  if (json.contains("icons")) {
    if (!json.at("icons").is_array()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt icons must be an array"));
    }
    for (const auto& item : json.at("icons")) {
      const auto icon = icon_from_json(item);
      if (!icon.has_value()) {
        return mcp::core::unexpected(
            prompt_json_error("prompt icon is invalid"));
      }
      prompt.icons.push_back(*icon);
    }
  }
  if (json.contains("annotations")) {
    if (!json.at("annotations").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt annotations must be an object"));
    }
    prompt.annotations = json.at("annotations");
  }
  if (json.contains("_meta")) {
    if (!json.at("_meta").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt _meta must be an object"));
    }
    prompt.meta = json.at("_meta");
  }
  prompt.extensions = collect_json_extensions(
      json, {"title", "name", "description", "arguments", "icons",
             "annotations", "_meta"});
  return prompt;
}

/// @brief Serializes a `prompts/list` result.
inline Json prompts_list_result_to_json(const PromptsListResult& result) {
  Json json = Json::object();
  json["prompts"] = Json::array();
  for (const auto& prompt : result.prompts) {
    json["prompts"].push_back(prompt_to_json(prompt));
  }
  if (result.next_cursor.has_value()) {
    json["nextCursor"] = *result.next_cursor;
  }
  if (result.meta.has_value()) {
    json["_meta"] = *result.meta;
  }
  if (result.ttl_ms.has_value()) {
    json["ttlMs"] = *result.ttl_ms;
  }
  if (result.cache_scope.has_value()) {
    json["cacheScope"] = *result.cache_scope;
  }
  append_json_extensions(json, result.extensions);
  return json;
}

/// @brief Parses a `prompts/list` result.
/// @return Parsed result or validation error.
inline core::Result<PromptsListResult> prompts_list_result_from_json(
    const Json& json) {
  if (!json.is_object()) {
    return mcp::core::unexpected(
        prompt_json_error("prompts/list result must be an object"));
  }
  if (!json.contains("prompts") || !json.at("prompts").is_array()) {
    return mcp::core::unexpected(
        prompt_json_error("prompts/list result requires a prompts array"));
  }

  PromptsListResult result;
  for (const auto& item : json.at("prompts")) {
    const auto prompt = prompt_from_json(item);
    if (!prompt) {
      return mcp::core::unexpected(prompt.error());
    }
    result.prompts.push_back(*prompt);
  }
  if (json.contains("nextCursor")) {
    if (!json.at("nextCursor").is_string()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/list nextCursor must be a string"));
    }
    result.next_cursor = json.at("nextCursor").get<std::string>();
  }
  if (json.contains("_meta")) {
    if (!json.at("_meta").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/list result _meta must be an object"));
    }
    result.meta = json.at("_meta");
  }
  result.extensions =
      collect_json_extensions(json, {"prompts", "nextCursor", "_meta"});
  return result;
}

/// @brief Serializes `prompts/get` params.
inline Json prompts_get_params_to_json(const PromptsGetParams& params) {
  Json json = Json::object();
  json["name"] = params.name;
  if (!params.arguments.empty()) {
    json["arguments"] = params.arguments;
  }
  if (params.meta.has_value()) {
    json["_meta"] = *params.meta;
  }
  append_json_extensions(json, params.extensions);
  return json;
}

/// @brief Parses `prompts/get` params.
/// @return Parsed params or validation error.
inline core::Result<PromptsGetParams> prompts_get_params_from_json(
    const Json& json) {
  if (!json.is_object()) {
    return mcp::core::unexpected(
        prompt_json_error("prompts/get params must be an object"));
  }
  if (!json.contains("name") || !json.at("name").is_string()) {
    return mcp::core::unexpected(
        prompt_json_error("prompts/get params require a string name"));
  }

  PromptsGetParams params;
  params.name = json.at("name").get<std::string>();
  if (json.contains("arguments")) {
    if (!json.at("arguments").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/get arguments must be an object"));
    }
    params.arguments = json.at("arguments");
  }
  if (json.contains("_meta")) {
    if (!json.at("_meta").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/get _meta must be an object"));
    }
    params.meta = json.at("_meta");
  }
  params.extensions =
      collect_json_extensions(json, {"name", "arguments", "_meta"});
  return params;
}

/// @brief Serializes one rendered prompt message.
inline Json prompt_message_to_json(const PromptMessage& message) {
  Json json = Json::object();
  json["role"] = message.role;
  json["content"] = content_block_to_json(message.content);
  if (message.meta.has_value()) {
    json["_meta"] = *message.meta;
  }
  append_json_extensions(json, message.extensions);
  return json;
}

/// @brief Parses one rendered prompt message.
/// @return Parsed message or validation error.
inline core::Result<PromptMessage> prompt_message_from_json(const Json& json) {
  if (!json.is_object()) {
    return mcp::core::unexpected(
        prompt_json_error("prompt message must be an object"));
  }
  if (!json.contains("role") || !json.at("role").is_string()) {
    return mcp::core::unexpected(
        prompt_json_error("prompt message requires a string role"));
  }
  if (!json.contains("content")) {
    return mcp::core::unexpected(
        prompt_json_error("prompt message requires content"));
  }

  const auto content = content_block_from_json(json.at("content"));
  if (!content) {
    return mcp::core::unexpected(content.error());
  }

  PromptMessage message;
  message.role = json.at("role").get<std::string>();
  message.content = *content;
  if (json.contains("_meta")) {
    if (!json.at("_meta").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompt message _meta must be an object"));
    }
    message.meta = json.at("_meta");
  }
  message.extensions =
      collect_json_extensions(json, {"role", "content", "_meta"});
  return message;
}

/// @brief Serializes a `prompts/get` result.
inline Json prompts_get_result_to_json(const PromptsGetResult& result) {
  Json json = Json::object();
  if (result.result_type.has_value()) {
    json["resultType"] = *result.result_type;
  }
  if (!result.description.empty()) {
    json["description"] = result.description;
  }
  if (!result.messages.empty()) {
    json["messages"] = Json::array();
    for (const auto& message : result.messages) {
      json["messages"].push_back(prompt_message_to_json(message));
    }
  }
  if (result.meta.has_value()) {
    json["_meta"] = *result.meta;
  }
  if (result.input_requests.has_value()) {
    json["inputRequests"] = *result.input_requests;
  }
  if (result.request_state.has_value()) {
    json["requestState"] = *result.request_state;
  }
  append_json_extensions(json, result.extensions);
  return json;
}

/// @brief Parses a `prompts/get` result.
/// @return Parsed result or validation error.
inline core::Result<PromptsGetResult> prompts_get_result_from_json(
    const Json& json) {
  if (!json.is_object()) {
    return mcp::core::unexpected(
        prompt_json_error("prompts/get result must be an object"));
  }
  if (!json.contains("messages") && !json.contains("resultType") &&
      !json.contains("inputRequests")) {
    return mcp::core::unexpected(prompt_json_error(
        "prompts/get result requires messages, resultType, or inputRequests"));
  }

  PromptsGetResult result;
  if (json.contains("resultType")) {
    result.result_type = json.at("resultType").get<std::string>();
  }
  if (json.contains("description")) {
    if (!json.at("description").is_string()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/get description must be a string"));
    }
    result.description = json.at("description").get<std::string>();
  }
  if (json.contains("messages")) {
    if (!json.at("messages").is_array()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/get messages must be an array"));
    }
    for (const auto& item : json.at("messages")) {
      const auto message = prompt_message_from_json(item);
      if (!message) {
        return mcp::core::unexpected(message.error());
      }
      result.messages.push_back(*message);
    }
  }
  if (json.contains("_meta")) {
    if (!json.at("_meta").is_object()) {
      return mcp::core::unexpected(
          prompt_json_error("prompts/get result _meta must be an object"));
    }
    result.meta = json.at("_meta");
  }
  if (json.contains("inputRequests")) {
    result.input_requests = json.at("inputRequests");
  }
  if (json.contains("requestState")) {
    result.request_state = json.at("requestState").get<std::string>();
  }
  result.extensions = collect_json_extensions(
      json, {"description", "messages", "_meta", "resultType", "inputRequests",
             "requestState"});
  return result;
}

}  // namespace mcp::protocol
