///|
/// Current client/server protocol version. Both sides reject packets carrying a
/// different version.
fn protocol_version_impl() -> Int32 {
  6
}

///|
/// Structured error categories used on the wire.
pub(all) enum ProtocolErrorCode {
  ProtocolInvalidJson
  ProtocolUnsupportedVersion
  ProtocolUnknownPacket
  ProtocolInvalidPacket
  ProtocolInvalidUri
  ProtocolProviderError
} derive(Eq, Debug)

///|
/// Debug label that keeps the local enum prefix visible.
pub fn ProtocolErrorCode::label(self : ProtocolErrorCode) -> String {
  match self {
    ProtocolInvalidJson => "ProtocolInvalidJson"
    ProtocolUnsupportedVersion => "ProtocolUnsupportedVersion"
    ProtocolUnknownPacket => "ProtocolUnknownPacket"
    ProtocolInvalidPacket => "ProtocolInvalidPacket"
    ProtocolInvalidUri => "ProtocolInvalidUri"
    ProtocolProviderError => "ProtocolProviderError"
  }
}

///|
/// Remote protocol error packet payload. `request_id` correlates failures to a
/// request; `provider_code` preserves lower-level filesystem/provider
/// categories.
pub struct ProtocolError {
  code : ProtocolErrorCode
  message : String
  request_id : String
  provider_code : String
} derive(Debug)

///|
/// Request addressed only by document URI.
pub(all) struct DocumentRequest {
  id : String
  uri : @base_common.Uri
} derive(Debug)

///|
/// Request addressed by URI, expected document revision, and UTF-16 offset.
/// Servers use the revision to drop stale feature requests.
pub(all) struct DocumentPositionRequest {
  id : String
  uri : @base_common.Uri
  document_revision : String
  offset : Int32
} derive(Debug)

///|
/// Request addressed by URI and expected document revision, for whole-document
/// feature refreshes.
pub(all) struct DocumentRevisionRequest {
  id : String
  uri : @base_common.Uri
  document_revision : String
} derive(Debug)

///|
/// Browser-to-server request packet variants.
pub(all) enum ClientPacket {
  ResolveDirectory(DocumentRequest)
  OpenDocument(DocumentRequest)
  WatchDocument(DocumentRequest)
  CloseDocument(DocumentRequest)
  Hover(DocumentPositionRequest)
  Definition(DocumentPositionRequest)
  References(DocumentPositionRequest)
  DocumentSymbols(DocumentRevisionRequest)
  ListHostDirectory(HostPathRequest)
  SwitchWorkspace(HostPathRequest)
} derive(Debug)

///|
/// Loaded or changed document payload from the remote server.
pub(all) struct DocumentPayload {
  id : String
  document : @workspace.DocumentSnapshot
} derive(Debug)

///|
/// One stat for the requested directory URI with exactly one level of
/// `children` resolved; nested directories arrive with `children: None`.
pub(all) struct DirectoryResolvedPayload {
  id : String
  stat : @workspace.WorkspaceStat
} derive(Debug)

///|
/// Server-initiated diagnostics push for a URI at a document revision. The
/// backend publishes these from `moon check` runs, so the packet carries no
/// request id.
pub(all) struct DiagnosticsPayload {
  uri : @base_common.Uri
  revision : String
  diagnostics : Array[@language.Diagnostic]
} derive(Debug)

///|
/// Hover response for a specific URI, document revision, and request id.
pub(all) struct HoverResultPayload {
  id : String
  uri : @base_common.Uri
  revision : String
  hover : @language.Hover?
} derive(Debug)

///|
/// Definition response for a specific URI, document revision, and request id.
pub(all) struct DefinitionResultPayload {
  id : String
  uri : @base_common.Uri
  revision : String
  locations : Array[@language.Location]
} derive(Debug)

///|
/// A reference location enriched by the server with one-based line/column
/// and the text of the containing line, so clients can list cross-file
/// references without fetching file contents.
pub(all) struct ReferenceItem {
  uri : @base_common.Uri
  range : @base_common.Range
  line : Int32
  column : Int32
  line_text : String
  is_test : Bool?
} derive(Debug)

///|
/// References response for a specific URI and document revision.
pub(all) struct ReferencesResultPayload {
  id : String
  uri : @base_common.Uri
  revision : String
  references : Array[ReferenceItem]
} derive(Debug)

///|
/// Document symbols response for a specific URI and document revision.
pub(all) struct DocumentSymbolsResultPayload {
  id : String
  uri : @base_common.Uri
  revision : String
  symbols : Array[@language.DocumentSymbol]
} derive(Debug)

///|
/// Server-to-browser response, event, and error packet variants.
pub(all) enum ServerPacket {
  DirectoryResolved(DirectoryResolvedPayload)
  DocumentLoaded(DocumentPayload)
  DocumentChanged(DocumentPayload)
  Diagnostics(DiagnosticsPayload)
  HoverResult(HoverResultPayload)
  DefinitionResult(DefinitionResultPayload)
  ReferencesResult(ReferencesResultPayload)
  DocumentSymbolsResult(DocumentSymbolsResultPayload)
  HostDirectoryListed(HostDirectoryListedPayload)
  WorkspaceSwitched(WorkspaceSwitchedPayload)
  RemoteError(ProtocolError)
} derive(Debug)

///|
/// Client-packet decode result that preserves structured protocol errors.
pub enum ClientPacketDecodeResult {
  DecodedClientPacket(ClientPacket)
  ClientPacketDecodeError(ProtocolError)
} derive(Debug)

///|
/// Server-packet decode result that preserves structured protocol errors.
pub enum ServerPacketDecodeResult {
  DecodedServerPacket(ServerPacket)
  ServerPacketDecodeError(ProtocolError)
} derive(Debug)

///|
/// Version-negotiation result used before opening a long-lived connection.
pub enum ProtocolNegotiation {
  ProtocolAccepted(Int32)
  ProtocolRejected(ProtocolError)
} derive(Debug)

///|
/// Builds a protocol error, defaulting an empty message to the wire code.
pub fn ProtocolError::ProtocolError(
  code : ProtocolErrorCode,
  message : String,
  request_id~ : String = "",
  provider_code~ : String = "",
) -> ProtocolError {
  let message = if message == "" { code.to_string() } else { message }
  { code, message, request_id, provider_code, }
}

///|
/// Converts a filesystem-provider failure into a protocol error without losing
/// the provider error code.
pub fn ProtocolError::from_file_system_error(
  error : @workspace.FileSystemProviderError,
  request_id~ : String = "",
) -> ProtocolError {
  ProtocolError(
    ProtocolProviderError,
    error.message,
    request_id~,
    provider_code=error.code.to_string(),
  )
}

///|
/// Stable compact code used in JSON payloads.
pub fn ProtocolErrorCode::to_string(self : ProtocolErrorCode) -> String {
  match self {
    ProtocolInvalidJson => "InvalidJson"
    ProtocolUnsupportedVersion => "UnsupportedVersion"
    ProtocolUnknownPacket => "UnknownPacket"
    ProtocolInvalidPacket => "InvalidPacket"
    ProtocolInvalidUri => "InvalidUri"
    ProtocolProviderError => "ProviderError"
  }
}

///|
/// Decodes a wire error code, failing closed to `InvalidPacket` for unknown
/// values.
pub fn ProtocolErrorCode::from_string(code : String) -> ProtocolErrorCode {
  match code {
    "InvalidJson" => ProtocolInvalidJson
    "UnsupportedVersion" => ProtocolUnsupportedVersion
    "UnknownPacket" => ProtocolUnknownPacket
    "InvalidPacket" => ProtocolInvalidPacket
    "InvalidUri" => ProtocolInvalidUri
    "ProviderError" => ProtocolProviderError
    _ => ProtocolInvalidPacket
  }
}

///|
/// Accepts only the exact current protocol version.
fn negotiate_protocol_version_impl(version : Int32) -> ProtocolNegotiation {
  if version == protocol_version() {
    ProtocolAccepted(version)
  } else {
    ProtocolRejected(
      ProtocolError(
        ProtocolUnsupportedVersion,
        "Unsupported remote protocol version \{version.to_string()}",
      ),
    )
  }
}

///|
/// Serializes client requests to the MoonBit-owned remote protocol JSON shape.
pub impl ToJson for ClientPacket with fn to_json(self) {
  match self {
    ResolveDirectory(request) =>
      document_request_json("ResolveDirectory", request)
    OpenDocument(request) => document_request_json("OpenDocument", request)
    WatchDocument(request) => document_request_json("WatchDocument", request)
    CloseDocument(request) => document_request_json("CloseDocument", request)
    Hover(request) => position_request_json("Hover", request)
    Definition(request) => position_request_json("Definition", request)
    References(request) => position_request_json("References", request)
    DocumentSymbols(request) =>
      revision_request_json("DocumentSymbols", request)
    ListHostDirectory(request) =>
      host_path_request_json("ListHostDirectory", request)
    SwitchWorkspace(request) =>
      host_path_request_json("SwitchWorkspace", request)
  }
}

///|
/// Serializes server responses and events to the remote protocol JSON shape.
pub impl ToJson for ServerPacket with fn to_json(self) {
  match self {
    DirectoryResolved(payload) => directory_resolved_json(payload)
    DocumentLoaded(payload) => document_payload_json("DocumentLoaded", payload)
    DocumentChanged(payload) =>
      document_payload_json("DocumentChanged", payload)
    Diagnostics(payload) => diagnostics_payload_json(payload)
    HoverResult(payload) => hover_result_json(payload)
    DefinitionResult(payload) => definition_result_json(payload)
    ReferencesResult(payload) => references_result_json(payload)
    DocumentSymbolsResult(payload) => document_symbols_result_json(payload)
    HostDirectoryListed(payload) => host_directory_listed_json(payload)
    WorkspaceSwitched(payload) => workspace_switched_json(payload)
    RemoteError(error) => error_packet_json(error)
  }
}

///|
/// Decodes and validates a client packet, returning structured errors instead
/// of raising on malformed JSON or packet shape.
pub fn decode_client_packet(raw : String) -> ClientPacketDecodeResult {
  match parse_packet_object(raw) {
    Some(packet) =>
      match validate_packet_version(packet) {
        Some(error) => ClientPacketDecodeError(error)
        None =>
          match json_string_field(packet, "type") {
            Some("ResolveDirectory") =>
              decode_document_client(packet, request_id(packet), request => {
                ResolveDirectory(request)
              })
            Some("OpenDocument") =>
              decode_document_client(packet, request_id(packet), request => {
                OpenDocument(request)
              })
            Some("WatchDocument") =>
              decode_document_client(packet, request_id(packet), request => {
                WatchDocument(request)
              })
            Some("CloseDocument") =>
              decode_document_client(packet, request_id(packet), request => {
                CloseDocument(request)
              })
            Some("Hover") =>
              decode_position_client(packet, request_id(packet), request => {
                Hover(request)
              })
            Some("Definition") =>
              decode_position_client(packet, request_id(packet), request => {
                Definition(request)
              })
            Some("References") =>
              decode_position_client(packet, request_id(packet), request => {
                References(request)
              })
            Some("DocumentSymbols") =>
              decode_version_client(packet, request_id(packet), request => {
                DocumentSymbols(request)
              })
            Some("ListHostDirectory") =>
              decode_host_path_client(packet, request_id(packet), request => {
                ListHostDirectory(request)
              })
            Some("SwitchWorkspace") =>
              decode_host_path_client(packet, request_id(packet), request => {
                SwitchWorkspace(request)
              })
            Some(kind) =>
              ClientPacketDecodeError(
                ProtocolError(
                  ProtocolUnknownPacket,
                  "Unknown client packet type \{kind}",
                  request_id=request_id(packet),
                ),
              )
            None =>
              ClientPacketDecodeError(
                ProtocolError(
                  ProtocolInvalidPacket,
                  "Remote protocol packet is missing a type.",
                  request_id=request_id(packet),
                ),
              )
          }
      }
    None =>
      ClientPacketDecodeError(
        ProtocolError(ProtocolInvalidJson, "Invalid remote protocol JSON."),
      )
  }
}

///|
/// Decodes and validates a server packet, returning structured errors instead
/// of raising on malformed JSON or packet shape.
pub fn decode_server_packet(raw : String) -> ServerPacketDecodeResult {
  match parse_packet_object(raw) {
    Some(packet) =>
      match validate_packet_version(packet) {
        Some(error) => ServerPacketDecodeError(error)
        None =>
          match json_string_field(packet, "type") {
            Some("DirectoryResolved") =>
              decode_directory_resolved_payload(packet, request_id(packet))
            Some("DocumentLoaded") =>
              decode_document_server(packet, request_id(packet), payload => {
                DocumentLoaded(payload)
              })
            Some("DocumentChanged") =>
              decode_document_server(packet, request_id(packet), payload => {
                DocumentChanged(payload)
              })
            Some("Diagnostics") => decode_diagnostics_payload(packet)
            Some("HoverResult") =>
              decode_hover_result_payload(packet, request_id(packet))
            Some("DefinitionResult") =>
              decode_definition_result_payload(packet, request_id(packet))
            Some("ReferencesResult") =>
              decode_references_result_payload(packet, request_id(packet))
            Some("DocumentSymbolsResult") =>
              decode_document_symbols_result_payload(packet, request_id(packet))
            Some("HostDirectoryListed") =>
              decode_host_directory_listed_payload(packet, request_id(packet))
            Some("WorkspaceSwitched") =>
              decode_workspace_switched_payload(packet, request_id(packet))
            Some("Error") => decode_error_payload(packet, request_id(packet))
            Some(kind) =>
              ServerPacketDecodeError(
                ProtocolError(
                  ProtocolUnknownPacket,
                  "Unknown server packet type \{kind}",
                  request_id=request_id(packet),
                ),
              )
            None =>
              ServerPacketDecodeError(
                ProtocolError(
                  ProtocolInvalidPacket,
                  "Remote protocol packet is missing a type.",
                  request_id=request_id(packet),
                ),
              )
          }
      }
    None =>
      ServerPacketDecodeError(
        ProtocolError(ProtocolInvalidJson, "Invalid remote protocol JSON."),
      )
  }
}

///|
fn decode_document_client(
  packet : Json,
  id : String,
  build : (DocumentRequest) -> ClientPacket,
) -> ClientPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) => DecodedClientPacket(build({ id, uri, }))
    ProtocolUriError(error) => ClientPacketDecodeError(error)
  }
}

///|
fn decode_position_client(
  packet : Json,
  id : String,
  build : (DocumentPositionRequest) -> ClientPacket,
) -> ClientPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) =>
      DecodedClientPacket(
        build({
          id,
          uri,
          document_revision: json_string_field(packet, "documentRevision").unwrap_or(
            "",
          ),
          offset: json_int_field(packet, "offset").unwrap_or(0),
        }),
      )
    ProtocolUriError(error) => ClientPacketDecodeError(error)
  }
}

///|
fn decode_version_client(
  packet : Json,
  id : String,
  build : (DocumentRevisionRequest) -> ClientPacket,
) -> ClientPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) =>
      DecodedClientPacket(
        build({
          id,
          uri,
          document_revision: json_string_field(packet, "documentRevision").unwrap_or(
            "",
          ),
        }),
      )
    ProtocolUriError(error) => ClientPacketDecodeError(error)
  }
}

///|
fn decode_document_server(
  packet : Json,
  id : String,
  build : (DocumentPayload) -> ServerPacket,
) -> ServerPacketDecodeResult {
  match json_field(packet, "document") {
    Some(document_json) =>
      match parse_source_document(document_json, id) {
        ParsedProtocolDocument(document) =>
          DecodedServerPacket(build({ id, document, }))
        ProtocolDocumentError(error) => ServerPacketDecodeError(error)
      }
    None =>
      ServerPacketDecodeError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Document packet is missing a document.",
          request_id=id,
        ),
      )
  }
}

///|
fn decode_directory_resolved_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match json_field(packet, "stat") {
    Some(stat_json) =>
      match parse_workspace_stat(stat_json) {
        Some(stat) => DecodedServerPacket(DirectoryResolved({ id, stat, }))
        None =>
          ServerPacketDecodeError(
            ProtocolError(
              ProtocolInvalidPacket,
              "Directory resolved packet carries an invalid stat.",
              request_id=id,
            ),
          )
      }
    None =>
      ServerPacketDecodeError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Directory resolved packet is missing a stat.",
          request_id=id,
        ),
      )
  }
}

///|
fn decode_diagnostics_payload(packet : Json) -> ServerPacketDecodeResult {
  match parse_uri_field(packet, "uri", "") {
    ParsedProtocolUri(uri) => {
      let diagnostics = []
      match json_field(packet, "diagnostics") {
        Some(Array(items)) =>
          for item in items {
            if parse_diagnostic(item) is Some(diagnostic) {
              diagnostics.push(diagnostic)
            }
          }
        _ => ()
      }
      DecodedServerPacket(
        Diagnostics({
          uri,
          revision: json_string_field(packet, "documentRevision").unwrap_or(""),
          diagnostics,
        }),
      )
    }
    ProtocolUriError(error) => ServerPacketDecodeError(error)
  }
}

///|
fn decode_hover_result_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) => {
      let hover = match json_field(packet, "hover") {
        Some(value) => parse_hover(value)
        None => None
      }
      DecodedServerPacket(
        HoverResult({
          id,
          uri,
          revision: json_string_field(packet, "documentRevision").unwrap_or(""),
          hover,
        }),
      )
    }
    ProtocolUriError(error) => ServerPacketDecodeError(error)
  }
}

///|
fn decode_definition_result_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) => {
      let locations = []
      match json_field(packet, "locations") {
        Some(Array(items)) =>
          for item in items {
            if parse_location(item) is Some(location) {
              locations.push(location)
            }
          }
        _ => ()
      }
      DecodedServerPacket(
        DefinitionResult({
          id,
          uri,
          revision: json_string_field(packet, "documentRevision").unwrap_or(""),
          locations,
        }),
      )
    }
    ProtocolUriError(error) => ServerPacketDecodeError(error)
  }
}

///|
fn decode_references_result_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) => {
      let references = []
      match json_field(packet, "references") {
        Some(Array(items)) =>
          for item in items {
            if parse_reference_item(item) is Some(reference) {
              references.push(reference)
            }
          }
        _ => ()
      }
      DecodedServerPacket(
        ReferencesResult({
          id,
          uri,
          revision: json_string_field(packet, "documentRevision").unwrap_or(""),
          references,
        }),
      )
    }
    ProtocolUriError(error) => ServerPacketDecodeError(error)
  }
}

///|
fn decode_document_symbols_result_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match parse_uri_field(packet, "uri", id) {
    ParsedProtocolUri(uri) => {
      let symbols = []
      match json_field(packet, "symbols") {
        Some(Array(items)) =>
          for item in items {
            if parse_document_symbol(item) is Some(symbol) {
              symbols.push(symbol)
            }
          }
        _ => ()
      }
      DecodedServerPacket(
        DocumentSymbolsResult({
          id,
          uri,
          revision: json_string_field(packet, "documentRevision").unwrap_or(""),
          symbols,
        }),
      )
    }
    ProtocolUriError(error) => ServerPacketDecodeError(error)
  }
}

///|
fn decode_error_payload(packet : Json, id : String) -> ServerPacketDecodeResult {
  match json_field(packet, "error") {
    Some(error) =>
      DecodedServerPacket(
        RemoteError(
          ProtocolError(
            ProtocolErrorCode::from_string(
              json_string_field(error, "code").unwrap_or("InvalidPacket"),
            ),
            json_string_field(error, "message").unwrap_or("Remote error."),
            request_id=json_string_field(error, "requestId").unwrap_or(id),
            provider_code=json_string_field(error, "providerCode").unwrap_or(""),
          ),
        ),
      )
    None =>
      ServerPacketDecodeError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Error packet is missing an error object.",
          request_id=id,
        ),
      )
  }
}

///|
fn parse_packet_object(raw : String) -> Json? {
  match parse_json(raw) {
    Some(Object(_)) as parsed => parsed
    _ => None
  }
}

///|
fn validate_packet_version(packet : Json) -> ProtocolError? {
  let version = json_int_field(packet, "version").unwrap_or(0)
  if version == protocol_version() {
    None
  } else {
    Some(
      ProtocolError(
        ProtocolUnsupportedVersion,
        "Unsupported remote protocol version \{version.to_string()}",
        request_id=request_id(packet),
      ),
    )
  }
}

///|
fn request_id(packet : Json) -> String {
  json_string_field(packet, "id").unwrap_or("")
}

///|
priv enum ProtocolUriResult {
  ParsedProtocolUri(@base_common.Uri)
  ProtocolUriError(ProtocolError)
}

///|
/// `Uri::parse` raises `UriError` (D6); the protocol keeps its own error
/// mapping, so decode failures collapse to `None` here. Strict mode keeps
/// the old contract of rejecting scheme-less strings — the protocol only
/// ever transports `to_string()` output, which always carries a scheme.
fn try_parse_uri(raw : String) -> @base_common.Uri? {
  Some(@base_common.Uri::parse(raw, strict=true)) catch {
    _ => None
  }
}

///|
fn parse_uri_field(
  packet : Json,
  key : String,
  id : String,
) -> ProtocolUriResult {
  match json_string_field(packet, key) {
    Some(raw) =>
      match try_parse_uri(raw) {
        Some(uri) => ParsedProtocolUri(uri)
        None =>
          ProtocolUriError(
            ProtocolError(
              ProtocolInvalidUri,
              "Invalid document URI \{raw}",
              request_id=id,
            ),
          )
      }
    None =>
      ProtocolUriError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Remote protocol packet is missing \{key}.",
          request_id=id,
        ),
      )
  }
}

///|
priv enum ProtocolDocumentResult {
  ParsedProtocolDocument(@workspace.DocumentSnapshot)
  ProtocolDocumentError(ProtocolError)
}

///|
fn parse_source_document(value : Json, id : String) -> ProtocolDocumentResult {
  match value {
    Object(_) =>
      match json_string_field(value, "uri") {
        Some(raw_uri) =>
          match try_parse_uri(raw_uri) {
            Some(uri) =>
              ParsedProtocolDocument(
                DocumentSnapshot(
                  uri,
                  json_string_field(value, "displayName").unwrap_or(""),
                  json_string_field(value, "languageId").unwrap_or(""),
                  json_string_field(value, "revision").unwrap_or(""),
                  json_string_field(value, "text").unwrap_or(""),
                ),
              )
            None =>
              ProtocolDocumentError(
                ProtocolError(
                  ProtocolInvalidUri,
                  "Invalid document URI \{raw_uri}",
                  request_id=id,
                ),
              )
          }
        None =>
          ProtocolDocumentError(
            ProtocolError(
              ProtocolInvalidPacket,
              "Source document is missing uri.",
              request_id=id,
            ),
          )
      }
    _ =>
      ProtocolDocumentError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Document payload must be an object.",
          request_id=id,
        ),
      )
  }
}

///|
fn document_request_json(kind : String, request : DocumentRequest) -> Json {
  let fields = packet_fields(kind, request.id)
  fields["uri"] = Json(request.uri.to_string())
  Json(fields)
}

///|
fn position_request_json(
  kind : String,
  request : DocumentPositionRequest,
) -> Json {
  let fields = packet_fields(kind, request.id)
  fields["uri"] = Json(request.uri.to_string())
  fields["documentRevision"] = Json(request.document_revision)
  fields["offset"] = Json(request.offset)
  Json(fields)
}

///|
fn revision_request_json(
  kind : String,
  request : DocumentRevisionRequest,
) -> Json {
  let fields = packet_fields(kind, request.id)
  fields["uri"] = Json(request.uri.to_string())
  fields["documentRevision"] = Json(request.document_revision)
  Json(fields)
}

///|
fn document_payload_json(kind : String, payload : DocumentPayload) -> Json {
  let fields = packet_fields(kind, payload.id)
  fields["document"] = payload.document.to_json()
  Json(fields)
}

///|
fn directory_resolved_json(payload : DirectoryResolvedPayload) -> Json {
  let fields = packet_fields("DirectoryResolved", payload.id)
  fields["stat"] = workspace_stat_json(payload.stat)
  Json(fields)
}

///|
/// Kind crosses the wire as a string; unresolved `children` is omitted so
/// the decoder can tell "not resolved" (`None`) from "empty" (`[]`).
fn workspace_stat_json(stat : @workspace.WorkspaceStat) -> Json {
  let fields : Map[String, Json] = {
    "uri": Json(stat.uri.to_string()),
    "name": Json(stat.name),
    "kind": Json(stat.kind.to_string()),
  }
  if stat.children is Some(children) {
    fields["children"] = Json(children.map(workspace_stat_json))
  }
  Json(fields)
}

///|
fn parse_workspace_stat(value : Json) -> @workspace.WorkspaceStat? {
  guard value is Object(_) else { return None }
  guard json_string_field(value, "uri") is Some(raw_uri) else { return None }
  guard try_parse_uri(raw_uri) is Some(uri) else { return None }
  guard json_string_field(value, "kind") is Some(raw_kind) else { return None }
  guard @workspace.WorkspaceEntryKind::from_string(raw_kind) is Some(kind) else {
    return None
  }
  let children = match json_field(value, "children") {
    Some(Array(items)) => {
      let resolved = []
      for item in items {
        if parse_workspace_stat(item) is Some(stat) {
          resolved.push(stat)
        }
      }
      Some(resolved)
    }
    _ => None
  }
  Some({
    uri,
    name: json_string_field(value, "name").unwrap_or(
      @base_common.basename_or_authority(uri),
    ),
    kind,
    children,
  })
}

///|
fn diagnostics_payload_json(payload : DiagnosticsPayload) -> Json {
  let fields = packet_fields("Diagnostics", "")
  fields["uri"] = Json(payload.uri.to_string())
  fields["documentRevision"] = Json(payload.revision)
  fields["diagnostics"] = Json(payload.diagnostics.map(diagnostic_json))
  Json(fields)
}

///|
fn hover_result_json(payload : HoverResultPayload) -> Json {
  let fields = packet_fields("HoverResult", payload.id)
  fields["uri"] = Json(payload.uri.to_string())
  fields["documentRevision"] = Json(payload.revision)
  fields["hover"] = match payload.hover {
    Some(hover) => hover_json(hover)
    None => Json::null()
  }
  Json(fields)
}

///|
fn definition_result_json(payload : DefinitionResultPayload) -> Json {
  let fields = packet_fields("DefinitionResult", payload.id)
  fields["uri"] = Json(payload.uri.to_string())
  fields["documentRevision"] = Json(payload.revision)
  fields["locations"] = Json(payload.locations.map(location_json))
  Json(fields)
}

///|
fn references_result_json(payload : ReferencesResultPayload) -> Json {
  let fields = packet_fields("ReferencesResult", payload.id)
  fields["uri"] = Json(payload.uri.to_string())
  fields["documentRevision"] = Json(payload.revision)
  fields["references"] = Json(payload.references.map(reference_item_json))
  Json(fields)
}

///|
fn reference_item_json(item : ReferenceItem) -> Json {
  let fields : Map[String, Json] = {
    "uri": Json(item.uri.to_string()),
    "range": range_json(item.range),
    "line": Json(item.line),
    "column": Json(item.column),
    "lineText": Json(item.line_text),
  }
  if item.is_test is Some(value) {
    fields["is_test"] = Json(value)
  }
  Json(fields)
}

///|
fn document_symbols_result_json(payload : DocumentSymbolsResultPayload) -> Json {
  let fields = packet_fields("DocumentSymbolsResult", payload.id)
  fields["uri"] = Json(payload.uri.to_string())
  fields["documentRevision"] = Json(payload.revision)
  fields["symbols"] = Json(payload.symbols.map(document_symbol_json))
  Json(fields)
}

///|
fn error_packet_json(error : ProtocolError) -> Json {
  let fields = packet_fields("Error", error.request_id)
  fields["error"] = ToJson::to_json(error)
  Json(fields)
}

///|
/// JSON error payload used inside `RemoteError` server packets.
pub impl ToJson for ProtocolError with fn to_json(self) {
  let fields : Map[String, Json] = {
    "code": Json(self.code.to_string()),
    "message": Json(self.message),
    "requestId": Json(self.request_id),
  }
  if self.provider_code != "" {
    fields["providerCode"] = Json(self.provider_code)
  }
  Json(fields)
}

///|
fn packet_fields(kind : String, id : String) -> Map[String, Json] {
  let fields : Map[String, Json] = {
    "version": Json(protocol_version()),
    "type": Json(kind),
  }
  if id != "" {
    fields["id"] = Json(id)
  }
  fields
}

///|
fn diagnostic_json(diagnostic : @language.Diagnostic) -> Json {
  let fields : Map[String, Json] = {
    "range": range_json(diagnostic.range),
    "severity": Json(diagnostic.severity.label()),
    "message": Json(diagnostic.message),
  }
  if diagnostic.tags is Some(tags) {
    fields["tags"] = Json(tags.map(tag => Json(tag.label())))
  }
  Json(fields)
}

///|
fn document_symbol_json(symbol : @language.DocumentSymbol) -> Json {
  {
    "name": Json(symbol.name),
    "kind": Json(symbol.kind),
    "range": range_json(symbol.range),
  }
}

///|
fn hover_json(hover : @language.Hover) -> Json {
  {
    "range": range_json(hover.range),
    "contents": Json(hover.contents.map(hover_content_json)),
  }
}

///|
fn hover_content_json(content : @language.HoverContent) -> Json {
  match content {
    PlainText(text) => { "kind": Json("plaintext"), "value": Json(text) }
    Markdown(text) => { "kind": Json("markdown"), "value": Json(text) }
  }
}

///|
fn location_json(location : @language.Location) -> Json {
  let fields : Map[String, Json] = {
    "uri": Json(location.uri.to_string()),
    "range": range_json(location.range),
  }
  if location.is_test is Some(value) {
    fields["is_test"] = Json(value)
  }
  Json(fields)
}

///|
/// Serializes a line/column `Range`, used by the types that carry a `Range`
/// (Location, DocumentSymbol, ReferenceItem).
fn range_json(range : @base_common.Range) -> Json {
  {
    "startLineNumber": Json(range.start_line_number),
    "startColumn": Json(range.start_column),
    "endLineNumber": Json(range.end_line_number),
    "endColumn": Json(range.end_column),
  }
}

///|
fn parse_range(value : Json) -> @base_common.Range? {
  match
    (
      json_int_field(value, "startLineNumber"),
      json_int_field(value, "startColumn"),
      json_int_field(value, "endLineNumber"),
      json_int_field(value, "endColumn"),
    ) {
    (Some(start_line), Some(start_column), Some(end_line), Some(end_column)) =>
      Some(Range(start_line, start_column, end_line, end_column))
    _ => None
  }
}

///|
fn parse_diagnostic(value : Json) -> @language.Diagnostic? {
  match value {
    Object(_) =>
      match json_field(value, "range") {
        Some(range_json) =>
          match parse_range(range_json) {
            Some(range) =>
              Some({
                range,
                severity: diagnostic_severity(
                  json_string_field(value, "severity").unwrap_or("Info"),
                ),
                message: json_string_field(value, "message").unwrap_or(""),
                tags: parse_diagnostic_tags(value),
              })
            None => None
          }
        None => None
      }
    _ => None
  }
}

///|
fn parse_diagnostic_tags(value : Json) -> Array[@language.DiagnosticTag]? {
  match json_field(value, "tags") {
    Some(Array(items)) => {
      let tags : Array[@language.DiagnosticTag] = []
      for item in items {
        match item {
          String("Unnecessary") => tags.push(Unnecessary)
          String("Deprecated") => tags.push(Deprecated)
          _ => ()
        }
      }
      if tags.is_empty() {
        None
      } else {
        Some(tags)
      }
    }
    _ => None
  }
}

///|
fn parse_hover(value : Json) -> @language.Hover? {
  match value {
    Null => None
    Object(_) =>
      match (json_field(value, "range"), json_field(value, "contents")) {
        (Some(range_json), Some(contents_json)) =>
          match parse_range(range_json) {
            Some(range) => {
              let contents = parse_hover_contents(contents_json)
              if contents.length() == 0 {
                None
              } else {
                Some({ range, contents, })
              }
            }
            None => None
          }
        _ => None
      }
    _ => None
  }
}

///|
fn parse_hover_contents(value : Json) -> Array[@language.HoverContent] {
  match value {
    String(text) => parse_hover_plaintext(text)
    Object(_) =>
      match parse_hover_content(value) {
        Some(content) => [content]
        None => []
      }
    Array(items) => {
      let out : Array[@language.HoverContent] = []
      for item in items {
        for content in parse_hover_contents(item) {
          out.push(content)
        }
      }
      out
    }
    _ => []
  }
}

///|
fn parse_hover_content(value : Json) -> @language.HoverContent? {
  match value {
    String(text) =>
      match parse_hover_plaintext(text) {
        [content] => Some(content)
        _ => None
      }
    Object(_) => {
      let text = match json_string_field(value, "value") {
        Some(text) => text
        None => return None
      }
      if text == "" {
        return None
      }
      match json_string_field(value, "kind") {
        Some("markdown") => Some(Markdown(text))
        _ => Some(PlainText(text))
      }
    }
    _ => None
  }
}

///|
fn parse_hover_plaintext(text : String) -> Array[@language.HoverContent] {
  if text == "" {
    []
  } else {
    [PlainText(text)]
  }
}

///|
fn parse_location(value : Json) -> @language.Location? {
  match value {
    Null => None
    Object(_) =>
      match (json_string_field(value, "uri"), json_field(value, "range")) {
        (Some(raw_uri), Some(range_json)) =>
          match (try_parse_uri(raw_uri), parse_range(range_json)) {
            (Some(uri), Some(range)) => {
              let is_test = match json_field(value, "is_test") {
                None => None
                Some(True) => Some(true)
                Some(False) => Some(false)
                _ => return None
              }
              Some({ is_test, uri, range, })
            }
            _ => None
          }
        _ => None
      }
    _ => None
  }
}

///|
fn parse_reference_item(value : Json) -> ReferenceItem? {
  parse_location(value).map(location => {
    uri: location.uri,
    range: location.range,
    is_test: location.is_test,
    line: json_int_field(value, "line").unwrap_or(0),
    column: json_int_field(value, "column").unwrap_or(0),
    line_text: json_string_field(value, "lineText").unwrap_or(""),
  })
}

///|
fn parse_document_symbol(value : Json) -> @language.DocumentSymbol? {
  match value {
    Object(_) =>
      match json_field(value, "range") {
        Some(range_json) =>
          match parse_range(range_json) {
            Some(range) =>
              Some({
                name: json_string_field(value, "name").unwrap_or(""),
                kind: json_string_field(value, "kind").unwrap_or(""),
                range,
              })
            None => None
          }
        None => None
      }
    _ => None
  }
}

///|
fn diagnostic_severity(label : String) -> @language.DiagnosticSeverity {
  match label {
    "Error" => Error
    "Warning" => Warning
    "Hint" => Hint
    _ => Info
  }
}

///|
fn parse_json(raw : String) -> Json? {
  Some(@json.parse(raw)) catch {
    _ => None
  }
}

///|
fn json_field(value : Json, key : String) -> Json? {
  match value {
    Object(fields) => fields.get(key)
    _ => None
  }
}

///|
fn json_string_field(value : Json, key : String) -> String? {
  match json_field(value, key) {
    Some(String(text)) => Some(text)
    _ => None
  }
}

///|
fn json_int_field(value : Json, key : String) -> Int32? {
  match json_field(value, key) {
    Some(Number(number, repr=_)) => Some(number.to_int())
    _ => None
  }
}

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

///|
pub extend ClientPacket with ToJson::{to_json}

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

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

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

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

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

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

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

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

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

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

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

///|
pub extend ProtocolError with ToJson::{to_json}

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

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

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

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

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

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

///|
pub extend ServerPacket with ToJson::{to_json}

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