///|
/// Who may keep a cached list result: any intermediary, or only caches inside
/// the same authorisation context (the `cacheScope` of a cacheable result).
pub(all) enum Scope {
  Public
  Private
} derive(Eq, Debug)

///|
/// The same scope as the wire names it.
pub fn Scope::to_cache_scope(self : Scope) -> @types.CacheScope {
  match self {
    Public => @types.Public
    Private => @types.Private
  }
}

///|
pub extend Scope with Eq::{equal, not_equal}

///|
pub extend Scope with Debug::{to_repr}

///|
/// An MCP server (← FastMCP's `FastMCP`): the tools, resources, resource
/// templates and prompts it offers, and who it says it is.
///
/// It speaks both eras of the protocol. A request carrying its protocol version
/// in `_meta` is served statelessly under the 2026-07-28 revision; an
/// `initialize` request opens the handshake of the revisions before it. The
/// same server is reached over Streamable HTTP with `to_asgi`, over stdio with
/// `run_stdio`, or by any transport of your own through `handle`.
///
/// `ttl_ms` and `scope` are the caching hints every list result carries under
/// the 2026-07-28 revision. The presets say "stale at once" and "this caller
/// only", the cautious end of both, since a server that changes what it lists
/// per caller would otherwise leak one caller's list to the next.
pub struct Server {
  info : @types.Implementation
  instructions : String?
  ttl_ms : Int32
  scope : Scope
  priv tools : Map[String, @tools.Tool]
  priv checks : Map[String, @moonschema.Schema]
  priv resources : Map[String, @resources.Resource]
  priv templates : Array[@resources.ResourceTemplate]
  priv prompts : Map[String, @prompts.Prompt]
  priv watchers : Map[String, Watcher]
  priv mut logger : @moonlog.Logger?
}

///|
pub fn Server::new(
  name : String,
  version~ : String = "0.1.0",
  title? : String,
  instructions? : String,
  ttl_ms~ : Int32 = 0,
  scope~ : Scope = Private,
) -> Server {
  {
    info: { name, title, version, },
    instructions,
    ttl_ms,
    scope,
    tools: Map([]),
    checks: Map([]),
    resources: Map([]),
    templates: [],
    prompts: Map([]),
    watchers: Map([]),
    logger: None,
  }
}

///|
/// Write the server's own log through `logger` (← a structured logger passed
/// in, never reached for globally): a request served, a request refused, a
/// subscription opened. `Context::log` writes here too, beside the
/// notification it sends, so a client that went away does not take the record
/// with it.
///
/// Without one the server keeps no log of its own; the `notifications/message`
/// its handlers ask for still go to the client.
pub fn Server::log_to(self : Server, logger~ : @moonlog.Logger) -> Unit {
  self.logger = Some(logger)
}

///|
/// Offer `tool`. Its input schema is compiled here, so a call is checked
/// against it before the tool runs. A schema that does not compile is a
/// mistake in the program, not in a request, so it aborts now rather than
/// failing the first call. A second tool under the same name replaces the
/// first.
pub fn Server::add_tool(self : Server, tool : @tools.Tool) -> Unit {
  let name = tool.component.name
  let check = @moonschema.Schema::new(tool.parameters) catch {
    e => abort("tool '\{name}': input schema does not compile: \{e}")
  }
  self.tools[name] = tool
  self.checks[name] = check
}

///|
/// Offer a tool whose handler returns any JSON value (← `@mcp.tool`). An object
/// becomes structured content as well as text; anything else becomes text. An
/// error the handler raises becomes a result marked `isError`, which the model
/// reads and can correct itself from. A name the protocol does not allow aborts
/// at registration.
pub fn Server::tool(
  self : Server,
  name : String,
  run : async (Map[String, Json]) -> Json,
  input_schema~ : Json = { "type": "object" },
  title? : String,
  description? : String,
  output_schema? : Json,
  annotations? : @types.ToolAnnotations,
) -> Unit {
  let shape = @tools.Tool::new(
    name,
    parameters=input_schema,
    run_fn=_ => @tools.ToolResult::new(),
    title?,
    description?,
    output_schema?,
    annotations?,
  ) catch {
    e => abort("tool '\{name}': \{text_of(e)}")
  }
  self.add_tool({
    ..shape,
    run_fn: args => {
      let value = run(args) catch {
        @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
        e => raise @exceptions.Tool(text_of(e))
      }
      shape.convert_result(value)
    },
  })
}

///|
/// Offer `resource` at its URI. A second resource at the same URI replaces the
/// first.
pub fn Server::add_resource(
  self : Server,
  resource : @resources.Resource,
) -> Unit {
  self.resources[resource.uri] = resource
}

///|
/// Offer a resource read as text (← `@mcp.resource(uri)`).
pub fn Server::resource(
  self : Server,
  uri : String,
  read : async () -> String,
  name? : String,
  title? : String,
  description? : String,
  mime_type~ : String = "text/plain",
) -> Unit {
  self.add_resource(
    @resources.Resource::from_function(
      uri,
      read_fn=() => {
        let text = read() catch {
          @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
          @exceptions.NotFound(m) => raise @exceptions.NotFound(m)
          e => raise @exceptions.Resource(text_of(e))
        }
        Text(text)
      },
      name?,
      title?,
      description?,
      mime_type~,
    ),
  )
}

///|
/// Offer `template`; a URI no fixed resource answers is matched against the
/// templates in the order they were added.
pub fn Server::add_template(
  self : Server,
  template : @resources.ResourceTemplate,
) -> Unit {
  self.templates.push(template)
}

///|
/// Offer the resources an RFC 6570 template names, read as text with the
/// parameters the URI matched (← `@mcp.resource("users://{id}")`). A template
/// that does not parse aborts at registration.
pub fn Server::template(
  self : Server,
  uri_template : String,
  read : async (Map[String, Json]) -> String,
  name? : String,
  title? : String,
  description? : String,
  mime_type~ : String = "text/plain",
) -> Unit {
  let properties : Map[String, Json] = Map([])
  for param in @resources.parse_template_params(uri_template) {
    properties[param] = { "type": "string" }
  }
  let template = @resources.ResourceTemplate::new(
    uri_template,
    parameters={ "type": "object", "properties": Json(properties) },
    read_fn=params => {
      let text = read(params) catch {
        @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
        @exceptions.NotFound(m) => raise @exceptions.NotFound(m)
        e => raise @exceptions.Resource(text_of(e))
      }
      Text(text)
    },
    name?,
    title?,
    description?,
    mime_type~,
  ) catch {
    e => abort("resource template '\{uri_template}': \{text_of(e)}")
  }
  self.add_template(template)
}

///|
/// Offer `prompt`. A second prompt under the same name replaces the first.
pub fn Server::add_prompt(self : Server, prompt : @prompts.Prompt) -> Unit {
  self.prompts[prompt.component.name] = prompt
}

///|
/// Offer a prompt rendered as one user message (← `@mcp.prompt`).
pub fn Server::prompt(
  self : Server,
  name : String,
  render : async (Map[String, Json]) -> String,
  title? : String,
  description? : String,
  arguments~ : Array[@types.PromptArgument] = [],
) -> Unit {
  self.add_prompt(
    @prompts.Prompt::new(
      name,
      render_fn=args => {
        let text = render(args) catch {
          @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
          @exceptions.Validation(m) => raise @exceptions.Validation(m)
          e => raise @exceptions.Prompt(text_of(e))
        }
        Text(text)
      },
      title?,
      description?,
      arguments~,
    ),
  )
}

///|
/// Offer a tool that also sees the request it serves (← FastMCP's `Context`
/// parameter): its identity in `_meta`, the headers that carried it, the
/// progress token it offered, and whatever the client answered on a retry.
///
/// The handler answers with a `ToolResult`. An ordinary one carries content;
/// `ToolResult::input_required` carries an ask instead, which the client
/// satisfies and then sends the same call again with `inputResponses` and
/// `requestState`, or with an answer if it cannot.
pub fn Server::tool_context(
  self : Server,
  name : String,
  run : async (Map[String, Json], @context.Context) -> @tools.ToolResult,
  input_schema~ : Json = { "type": "object" },
  title? : String,
  description? : String,
  output_schema? : Json,
  annotations? : @types.ToolAnnotations,
) -> Unit {
  let shape = @tools.Tool::new(
    name,
    parameters=input_schema,
    run_fn=_ => @tools.ToolResult::new(),
    title?,
    description?,
    output_schema?,
    annotations?,
  ) catch {
    e => abort("tool '\{name}': \{text_of(e)}")
  }
  self.add_tool({
    ..shape,
    context_run_fn: Some(async fn(
      args,
      context,
    ) raise @exceptions.FastMCPError {
      run(args, context) catch {
        @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
        e => raise @exceptions.Tool(text_of(e))
      }
    }),
  })
}

///|
/// Offer a resource whose reader sees the request it serves. A read that needs
/// something from the client answers with `ResourceResult::input_required`
/// instead of contents.
pub fn Server::resource_context(
  self : Server,
  uri : String,
  read : async (@context.Context) -> @resources.RawResourceValue,
  name? : String,
  title? : String,
  description? : String,
  mime_type~ : String = "text/plain",
) -> Unit {
  self.add_resource(
    @resources.Resource::from_function(
      uri,
      read_fn=() => Text(""),
      name?,
      title?,
      description?,
      mime_type~,
      context_read_fn=async fn(context) raise @exceptions.FastMCPError {
        read(context) catch {
          @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
          @exceptions.NotFound(m) => raise @exceptions.NotFound(m)
          e => raise @exceptions.Resource(text_of(e))
        }
      },
    ),
  )
}

///|
/// Offer the resources an RFC 6570 template names, read in the request that
/// matched them.
pub fn Server::template_context(
  self : Server,
  uri_template : String,
  read : async (Map[String, Json], @context.Context) -> @resources.RawResourceValue,
  name? : String,
  title? : String,
  description? : String,
  mime_type~ : String = "text/plain",
) -> Unit {
  let properties : Map[String, Json] = Map([])
  for param in @resources.parse_template_params(uri_template) {
    properties[param] = { "type": "string" }
  }
  let template = @resources.ResourceTemplate::new(
    uri_template,
    parameters={ "type": "object", "properties": Json(properties) },
    read_fn=_ => Text(""),
    name?,
    title?,
    description?,
    mime_type~,
    context_read_fn=async fn(params, context) raise @exceptions.FastMCPError {
      read(params, context) catch {
        @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
        @exceptions.NotFound(m) => raise @exceptions.NotFound(m)
        e => raise @exceptions.Resource(text_of(e))
      }
    },
  ) catch {
    e => abort("resource template '\{uri_template}': \{text_of(e)}")
  }
  self.add_template(template)
}

///|
/// Offer a prompt whose renderer sees the request it serves.
pub fn Server::prompt_context(
  self : Server,
  name : String,
  render : async (Map[String, Json], @context.Context) -> @prompts.RenderedPrompt,
  title? : String,
  description? : String,
  arguments~ : Array[@types.PromptArgument] = [],
) -> Unit {
  self.add_prompt(
    @prompts.Prompt::new(
      name,
      render_fn=_ => @prompts.RenderedPrompt::Text(""),
      title?,
      description?,
      arguments~,
      context_render_fn=async fn(args, context) raise @exceptions.FastMCPError {
        render(args, context) catch {
          @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
          @exceptions.Validation(m) => raise @exceptions.Validation(m)
          e => raise @exceptions.Prompt(text_of(e))
        }
      },
    ),
  )
}