# frozen_string_literal: true

module MCP
  class ServerContext
    attr_reader :cancellation

    # The SEP-2575 per-request envelope (`MCP::RequestEnvelope`) when the request was classified as modern;
    # `nil` on legacy requests.
    attr_reader :envelope

    # SEP-2322 multi round-trip retry fields, present when the client re-issued the request after
    # an `input_required` result: `input_responses` maps the keys of the earlier `inputRequests` to
    # the client's answers, and `request_state` is the opaque continuation string echoed back byte-exactly.
    # Both are `nil` on a first-round request. Only handlers that opt in to `server_context:` can read them
    # (the same access model as the envelope readers).
    attr_reader :input_responses, :request_state

    def initialize(context, progress:, notification_target:, related_request_id: nil, cancellation: nil, envelope: nil,
      input_responses: nil, request_state: nil)
      @context = context
      @progress = progress
      @notification_target = notification_target
      @related_request_id = related_request_id
      @cancellation = cancellation
      @envelope = envelope
      @input_responses = input_responses
      @request_state = request_state
    end

    # Reads one entry of {#input_responses} by its `inputRequests` key, tolerating symbol or string keys.
    def input_response(key)
      return unless @input_responses.is_a?(Hash)

      value = @input_responses[key.to_sym]
      value.nil? ? @input_responses[key.to_s] : value
    end

    def cancelled?
      !!@cancellation&.cancelled?
    end

    def raise_if_cancelled!
      @cancellation&.raise_if_cancelled!
    end

    # Whether the current request follows the stateless modern lifecycle (SEP-2575).
    def modern?
      !@envelope.nil?
    end

    # Client identity for the current request. Modern requests carry it in the `_meta` envelope;
    # legacy sessions fall back to the state stored by `initialize`. The envelope always wins
    # because servers MUST NOT infer identity from prior requests.
    def client_info
      return @envelope.client_info if @envelope

      @notification_target.client if @notification_target.respond_to?(:client)
    end

    # Client capabilities for the current request, with the same envelope-first resolution as {#client_info}.
    def client_capabilities
      return @envelope.client_capabilities if @envelope

      @notification_target.client_capabilities if @notification_target.respond_to?(:client_capabilities)
    end

    # The protocol version the current request was made with. `nil` on legacy requests,
    # where the version is a session-level negotiation result rather than per-request data.
    def protocol_version
      @envelope&.protocol_version
    end

    # Guards the current request on a declared client capability (SEP-2575). `path` names nested capability keys,
    # e.g. `require_client_capability!(:elicitation, :form)`. Raises `Server::MissingRequiredClientCapabilityError`
    # (JSON-RPC error `-32021` with `data: { requiredCapabilities: ... }`) when the capability was not declared.
    def require_client_capability!(*path)
      raise ArgumentError, "at least one capability key is required" if path.empty?

      declared = client_capabilities
      value = path.reduce(declared) do |acc, key|
        break unless acc.is_a?(Hash)

        symbol_value = acc[key.to_sym]
        symbol_value.nil? ? acc[key.to_s] : symbol_value
      end
      return unless value.nil?

      required = path.reverse.inject({}) { |acc, key| { key.to_sym => acc } }
      raise Server::MissingRequiredClientCapabilityError, required
    end

    # Reports progress for the current tool operation.
    # The notification is automatically scoped to the originating session.
    #
    # @param progress [Numeric] Current progress value.
    # @param total [Numeric, nil] Total expected value.
    # @param message [String, nil] Human-readable status message.
    def report_progress(progress, total: nil, message: nil)
      @progress.report(progress, total: total, message: message)
    end

    # Sends a log message notification scoped to the originating session.
    #
    # @param data [Object] The log data to send.
    # @param level [String] Log level (e.g., `"debug"`, `"info"`, `"error"`).
    # @param logger [String, nil] Logger name.
    # @deprecated MCP Logging (`logging/setLevel` and `notifications/message`)
    #   is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
    #   Use stderr or OpenTelemetry instead.
    def notify_log_message(data:, level:, logger: nil)
      return unless @notification_target

      # Modern requests opt in to logging per request (SEP-2575): without `io.modelcontextprotocol/logLevel` in `_meta`,
      # the server MUST NOT send any `notifications/message` for the request, and an insufficient level drops
      # the message the same way. Session- or server-level gating still applies downstream on delegation.
      if @envelope
        threshold = @envelope.log_level && LoggingMessageNotification.new(level: @envelope.log_level)
        return unless threshold&.valid_level? && threshold.should_notify?(level)
      end

      @notification_target.notify_log_message(data: data, level: level, logger: logger, related_request_id: @related_request_id)
    end

    # Sends a resource updated notification scoped to the originating session.
    #
    # @param uri [String] The URI of the updated resource.
    def notify_resources_updated(uri:)
      return unless @notification_target

      @notification_target.notify_resources_updated(uri: uri)
    end

    # Delegates to the session so the request is scoped to the originating client.
    # The originating request id is stamped as `related_request_id`, satisfying
    # SEP-2260's requirement that server-to-client requests be associated with
    # the client request being processed.
    # @deprecated MCP Roots (`roots/list` and
    #   `notifications/roots/list_changed`) is deprecated as of MCP protocol
    #   version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
    #   server configuration, or environment variables instead.
    def list_roots(timeout: nil)
      if @notification_target.respond_to?(:list_roots)
        @notification_target.list_roots(related_request_id: @related_request_id, **timeout_kwarg(timeout))
      else
        raise NoMethodError, "undefined method 'list_roots' for #{self}"
      end
    end

    # Sends a `ping` request to the originating client to verify it is still responsive.
    # Per the MCP spec, the client MUST respond promptly with an empty result.
    #
    # @return [Hash] An empty hash on success.
    # @raise [Server::ValidationError] If the response `result` is not a Hash.
    # @raise [NoMethodError] If the session does not support sending pings.
    #
    # @example
    #   def self.call(server_context:)
    #     server_context.ping # => {}
    #     # ...
    #   end
    #
    # @see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping
    def ping(timeout: nil)
      if @notification_target.respond_to?(:ping)
        @notification_target.ping(related_request_id: @related_request_id, **timeout_kwarg(timeout))
      else
        raise NoMethodError, "undefined method 'ping' for #{self}"
      end
    end

    # Delegates to the session so the request is scoped to the originating client.
    # Falls back to `@context` (via `method_missing`) when `@notification_target`
    # does not support sampling.
    #
    # The originating request id is stamped as `related_request_id` and cannot be
    # overridden by callers (the literal keyword after `**kwargs` wins),
    # satisfying SEP-2260's requirement that server-to-client requests be
    # associated with the client request being processed.
    # @deprecated MCP Sampling (`sampling/createMessage`) is deprecated as of
    #   MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
    #   APIs instead.
    def create_sampling_message(**kwargs)
      if @notification_target.respond_to?(:create_sampling_message)
        @notification_target.create_sampling_message(**kwargs, related_request_id: @related_request_id)
      elsif @context.respond_to?(:create_sampling_message)
        @context.create_sampling_message(**kwargs, related_request_id: @related_request_id)
      else
        raise NoMethodError, "undefined method 'create_sampling_message' for #{self}"
      end
    end

    # Delegates to the session so the request is scoped to the originating client.
    # Falls back to `@context` (via `method_missing`) when `@notification_target`
    # does not support elicitation.
    # The originating request id is stamped as a non-overridable
    # `related_request_id`, as with `create_sampling_message` (SEP-2260).
    def create_form_elicitation(**kwargs)
      if @notification_target.respond_to?(:create_form_elicitation)
        @notification_target.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
      elsif @context.respond_to?(:create_form_elicitation)
        @context.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
      else
        raise NoMethodError, "undefined method 'create_form_elicitation' for #{self}"
      end
    end

    # Delegates to the session so the request is scoped to the originating client.
    # Falls back to `@context` when `@notification_target` does not support URL mode elicitation.
    # The originating request id is stamped as a non-overridable `related_request_id`,
    # as with `create_sampling_message` (SEP-2260).
    def create_url_elicitation(**kwargs)
      if @notification_target.respond_to?(:create_url_elicitation)
        @notification_target.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
      elsif @context.respond_to?(:create_url_elicitation)
        @context.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
      else
        raise NoMethodError, "undefined method 'create_url_elicitation' for #{self}"
      end
    end

    # Delegates to the session so the notification is scoped to the originating client.
    def notify_elicitation_complete(**kwargs)
      if @notification_target.respond_to?(:notify_elicitation_complete)
        @notification_target.notify_elicitation_complete(**kwargs)
      elsif @context.respond_to?(:notify_elicitation_complete)
        @context.notify_elicitation_complete(**kwargs)
      else
        raise NoMethodError, "undefined method 'notify_elicitation_complete' for #{self}"
      end
    end

    # Forward arguments explicitly with `*args, **kwargs, &block` rather than the `...` forwarding syntax.
    # The gem supports Ruby 2.7.0 (see `required_ruby_version`), but RuboCop's Parser backend only runs on Ruby 2.7.8,
    # so leading-argument forwarding like `def method_missing(name, ...)` is allowed by the linter even though it
    # raises a `SyntaxError` on Ruby 2.7.0 through 2.7.2 (it was added in Ruby 2.7.3). Explicit forwarding keeps
    # this method loadable on Ruby 2.7.0.
    def method_missing(name, *args, **kwargs, &block)
      if @context.respond_to?(name)
        @context.public_send(name, *args, **kwargs, &block)
      else
        super
      end
    end

    def respond_to_missing?(name, include_private = false)
      @context.respond_to?(name) || super
    end

    private

    # An omitted `timeout:` is not forwarded at all, so the delegated call keeps the shape it had
    # before per-request timeouts existed. A notification target that predates the keyword
    # (a custom object standing in for a session) keeps working until a caller actually asks for a timeout,
    # and the transport applies its own default in that case.
    def timeout_kwarg(timeout)
      timeout.nil? ? {} : { timeout: timeout }
    end
  end
end
