// Host filesystem browsing packets: the repository picker lists absolute
// directories on the server host and switches the served workspace root.
// These packets are host-level concerns — a `RemoteServer` policy layer
// without a native host rejects them.

///|
/// Request addressed by an absolute host filesystem path. An empty path asks
/// the server to start from the current workspace root.
pub(all) struct HostPathRequest {
  id : String
  path : String
} derive(Debug)

///|
/// One child directory in a host filesystem listing. `is_workspace` marks
/// directories that look like a repository root (a `moon.mod`,
/// `moon.mod.json`, or `.git` entry).
pub(all) struct HostDirectoryEntry {
  name : String
  path : String
  is_workspace : Bool
} derive(Debug, Eq)

///|
/// One resolved host directory level: the absolute listed path, its parent
/// (`None` at the filesystem root), and its child directories.
pub(all) struct HostDirectoryListing {
  path : String
  parent : String?
  entries : Array[HostDirectoryEntry]
} derive(Debug, Eq)

///|
/// Response payload for `ListHostDirectory`.
pub(all) struct HostDirectoryListedPayload {
  id : String
  listing : HostDirectoryListing
} derive(Debug)

///|
/// The served workspace root changed. Carries the requester's id on the
/// direct response and an empty id on the broadcast to every other session.
pub(all) struct WorkspaceSwitchedPayload {
  id : String
  root : String
  name : String
} derive(Debug)

///|
fn host_path_request_json(kind : String, request : HostPathRequest) -> Json {
  let fields = packet_fields(kind, request.id)
  fields["path"] = Json(request.path)
  Json(fields)
}

///|
fn decode_host_path_client(
  packet : Json,
  id : String,
  build : (HostPathRequest) -> ClientPacket,
) -> ClientPacketDecodeResult {
  match json_string_field(packet, "path") {
    Some(path) => DecodedClientPacket(build({ id, path, }))
    None =>
      ClientPacketDecodeError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Remote protocol packet is missing path.",
          request_id=id,
        ),
      )
  }
}

///|
/// `parent` is omitted at the filesystem root so the decoder can tell "no
/// parent" from an empty string.
fn host_directory_listed_json(payload : HostDirectoryListedPayload) -> Json {
  let fields = packet_fields("HostDirectoryListed", payload.id)
  fields["path"] = Json(payload.listing.path)
  if payload.listing.parent is Some(parent) {
    fields["parent"] = Json(parent)
  }
  fields["entries"] = Json(
    payload.listing.entries.map(host_directory_entry_json),
  )
  Json(fields)
}

///|
fn host_directory_entry_json(entry : HostDirectoryEntry) -> Json {
  {
    "name": Json(entry.name),
    "path": Json(entry.path),
    "isWorkspace": Json(entry.is_workspace),
  }
}

///|
fn decode_host_directory_listed_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match json_string_field(packet, "path") {
    Some(path) => {
      let entries : Array[HostDirectoryEntry] = []
      match json_field(packet, "entries") {
        Some(Array(items)) =>
          for item in items {
            if parse_host_directory_entry(item) is Some(entry) {
              entries.push(entry)
            }
          }
        _ => ()
      }
      DecodedServerPacket(
        HostDirectoryListed({
          id,
          listing: {
            path,
            parent: json_string_field(packet, "parent"),
            entries,
          },
        }),
      )
    }
    None =>
      ServerPacketDecodeError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Host directory packet is missing path.",
          request_id=id,
        ),
      )
  }
}

///|
fn parse_host_directory_entry(value : Json) -> HostDirectoryEntry? {
  guard value is Object(_) else { return None }
  guard json_string_field(value, "name") is Some(name) else { return None }
  guard json_string_field(value, "path") is Some(path) else { return None }
  let is_workspace = json_field(value, "isWorkspace") is Some(True)
  Some({ name, path, is_workspace, })
}

///|
fn workspace_switched_json(payload : WorkspaceSwitchedPayload) -> Json {
  let fields = packet_fields("WorkspaceSwitched", payload.id)
  fields["root"] = Json(payload.root)
  fields["name"] = Json(payload.name)
  Json(fields)
}

///|
fn decode_workspace_switched_payload(
  packet : Json,
  id : String,
) -> ServerPacketDecodeResult {
  match json_string_field(packet, "root") {
    Some(root) =>
      DecodedServerPacket(
        WorkspaceSwitched({
          id,
          root,
          name: json_string_field(packet, "name").unwrap_or(""),
        }),
      )
    None =>
      ServerPacketDecodeError(
        ProtocolError(
          ProtocolInvalidPacket,
          "Workspace switched packet is missing root.",
          request_id=id,
        ),
      )
  }
}

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

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

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

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

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

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

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