///|
/// Builder for a JSON-RPC endpoint method table.
pub struct EndpointBuilder {
  priv handlers : Map[String, Handler]
  priv errors : Array[EndpointError]
}

///|
/// Create an empty endpoint builder.
pub fn EndpointBuilder::EndpointBuilder() -> EndpointBuilder {
  { handlers: {}, errors: [] }
}

///|
/// Convert a typed async MoonBit function into a low-level JSON-RPC handler.
///
/// The generated handler decodes `params` as `P`, calls `run`, and converts
/// the result to JSON. Parameter decode failures are reported as
/// `RpcErrorWithData` with JSON-RPC `INVALID_PARAMS_CODE`.
pub fn[P : @json.FromJson, R : ToJson] typed(
  run : async (P) -> R raise RpcError,
) -> Handler {
  async fn(params) -> Json raise RpcError {
    let decoded : P = decode_params(params)
    run(decoded).to_json()
  }
}

///|
fn[P : @json.FromJson] decode_params(params : Json?) -> P raise RpcError {
  let value = params.unwrap_or(null)
  @json.from_json(value) catch {
    JsonDecodeError((path, message)) => {
      let data : Map[String, Json] = {
        "path": path.to_json(),
        "message": Json::string(message),
      }
      raise RpcErrorWithData(
        code=INVALID_PARAMS_CODE,
        message="Invalid params",
        data=Json::object(data),
      )
    }
  }
}

///|
fn validate_method_name(name : String) -> Unit raise EndpointError {
  if name == "" {
    raise InvalidMethod(name)
  } else if name.has_prefix("rpc.") {
    raise ReservedMethod(name)
  }
}

///|
/// Register a method handler on the builder.
///
/// Validation errors are accumulated and reported by `build`, so calls can be
/// chained while preserving all registration attempts.
pub fn EndpointBuilder::handle(
  self : EndpointBuilder,
  name : String,
  handler : Handler,
) -> EndpointBuilder {
  let handlers = self.handlers.copy()
  let errors = self.errors.copy()
  try validate_method_name(name) catch {
    error => errors.push(error)
  } noraise {
    _ if self.handlers.get(name) is Some(_) =>
      errors.push(DuplicateMethod(name))
    _ => handlers[name] = handler
  }
  { handlers, errors }
}

///|
/// Build the runtime endpoint.
pub fn EndpointBuilder::build(
  self : EndpointBuilder,
) -> Endpoint raise EndpointError {
  match self.errors {
    [error, ..] => raise error
    [] => { router: self.handlers, next_id: 1, pending: {} }
  }
}

///|
struct Pending {
  name : String
} derive(Eq, Debug)

///|
/// One side of a JSON-RPC connection.
pub struct Endpoint {
  priv router : Map[String, Handler]
  priv mut next_id : Int
  priv pending : Map[Id, Pending]
}

///|
fn make_request_message(rpc_method : String, params? : Json, id? : Id) -> Json {
  let obj : Map[String, Json] = {
    "jsonrpc": "2.0",
    "method": Json::string(rpc_method),
  }
  if params is Some(value) {
    obj["params"] = value
  }
  if id is Some(value) {
    obj["id"] = value.to_json()
  }
  Json::object(obj)
}

///|
/// Create a JSON-RPC request and register its id as pending.
///
/// If `id` is omitted, the endpoint allocates an integer id. The returned JSON
/// value is the request document to send through a transport.
pub fn Endpoint::request(
  self : Endpoint,
  rpc_method : String,
  params? : Json,
  id? : Id,
) -> Json raise EndpointError {
  validate_method_name(rpc_method)
  let request_id = match id {
    Some(value) => value
    None => self.allocate_id()
  }
  guard self.pending.get(request_id) is None else {
    raise DuplicatePendingId(request_id)
  }
  self.pending[request_id] = { name: rpc_method }
  make_request_message(rpc_method, params?, id=request_id)
}

///|
fn Endpoint::allocate_id(self : Endpoint) -> Id {
  let mut value = Number(self.next_id)
  while self.pending.get(value) is Some(_) {
    self.next_id = self.next_id + 1
    value = Number(self.next_id)
  }
  self.next_id = self.next_id + 1
  value
}

///|
/// Create a JSON-RPC notification document.
///
/// Notifications do not allocate an id and cannot produce a completed local
/// request.
pub fn Endpoint::notify(
  _self : Endpoint,
  rpc_method : String,
  params? : Json,
) -> Json raise EndpointError {
  validate_method_name(rpc_method)
  make_request_message(rpc_method, params?)
}

///|
/// Handle a raw JSON document.
pub async fn Endpoint::receive_text(
  self : Endpoint,
  input : String,
) -> ReceiveResult {
  try @json.parse(input) catch {
    _ =>
      {
        outgoing: [
          unknown_id_error(
            RpcError(code=PARSE_ERROR_CODE, message="Parse error"),
          ),
        ],
        completed: [],
        unexpected: [],
      }
  } noraise {
    value => self.receive_json(value)
  }
}

///|
/// Handle an already parsed JSON value.
pub async fn Endpoint::receive_json(
  self : Endpoint,
  value : Json,
) -> ReceiveResult {
  let result = ReceiveResult()
  match value {
    Array(items) => self.receive_batch(items, result)
    item => {
      self.receive_one(item, result)
      result
    }
  }
}

///|
async fn Endpoint::receive_batch(
  self : Endpoint,
  items : Array[Json],
  result : ReceiveResult,
) -> ReceiveResult {
  if items.is_empty() {
    result.outgoing.push(
      unknown_id_error(
        RpcError(code=INVALID_REQUEST_CODE, message="Invalid Request"),
      ),
    )
  } else {
    for item in items {
      self.receive_one(item, result)
    }
  }
  result
}

///|
async fn Endpoint::receive_one(
  self : Endpoint,
  value : Json,
  result : ReceiveResult,
) -> Unit {
  match classify_message(value) {
    RequestLike => self.receive_request_like(value, result)
    ResponseLike => self.receive_response_like(value, result)
    InvalidLike => self.receive_request_like(value, result)
  }
}

///|
async fn Endpoint::receive_request_like(
  self : Endpoint,
  value : Json,
  result : ReceiveResult,
) -> Unit {
  try parse_request(value) catch {
    RequestError(error~, ..) as request_error =>
      result.outgoing.push(error_response(request_error.response_id(), error))
  } noraise {
    request => self.receive_request(request, result)
  }
}

///|
async fn Endpoint::receive_request(
  self : Endpoint,
  request : Request,
  result : ReceiveResult,
) -> Unit {
  let response_id = request.response_id()
  guard self.router.get(request.rpc_method) is Some(handler) else {
    if !request.notification {
      result.outgoing.push(
        error_response(
          response_id,
          RpcError(code=METHOD_NOT_FOUND_CODE, message="Method not found"),
        ),
      )
    }
  }
  try handler(request.params) catch {
    error =>
      if !request.notification {
        result.outgoing.push(error_response(response_id, error))
      }
  } noraise {
    json_result =>
      if !request.notification {
        result.outgoing.push(success_response(response_id, json_result))
      }
  }
}

///|
fn Endpoint::receive_response_like(
  self : Endpoint,
  value : Json,
  result : ReceiveResult,
) -> Unit {
  try @json.from_json(value) catch {
    _ => result.unexpected.push(value)
  } noraise {
    response => self.complete_response(response, value, result)
  }
}

///|
fn Endpoint::complete_response(
  self : Endpoint,
  response : Response,
  raw : Json,
  result : ReceiveResult,
) -> Unit {
  try @json.from_json(response.id) catch {
    _ => result.unexpected.push(raw)
  } noraise {
    id => {
      guard self.pending.get(id) is Some(pending) else {
        result.unexpected.push(raw)
      }
      self.pending.remove(id)
      result.completed.push({
        id,
        method_name: pending.name,
        result: response.result,
      })
    }
  }
}