//! Typed domain models for the sailbox API: lifecycle, listing, ingress
//! listeners, and volumes.
//!
//! The core owns the wire schema: it deserializes API responses into these
//! structs (applying the wire-schema field defaults) and serializes them back
//! out for bindings. A binding maps a struct onto its own public type by field
//! name without re-parsing the wire shape.

use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

/// Lifecycle status of a sailbox. An unrecognized server value is preserved in
/// `Other` so a status a newer backend introduces never fails parsing. The
/// default is `Other("")`: the status of a handle no server response has
/// filled in yet.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SailboxStatus {
    /// Running and serving.
    Running,
    /// Paused in memory.
    Paused,
    /// Sleeping (checkpointed to disk).
    Sleeping,
    /// In a failed state.
    Failed,
    /// Terminated.
    Terminated,
    /// A status this SDK version does not recognize, kept verbatim.
    Other(String),
}

impl Default for SailboxStatus {
    fn default() -> Self {
        SailboxStatus::Other(String::new())
    }
}

impl SailboxStatus {
    /// The wire string for this status.
    pub fn as_str(&self) -> &str {
        match self {
            SailboxStatus::Running => "running",
            SailboxStatus::Paused => "paused",
            SailboxStatus::Sleeping => "sleeping",
            SailboxStatus::Failed => "failed",
            SailboxStatus::Terminated => "terminated",
            SailboxStatus::Other(s) => s,
        }
    }
}

impl From<&str> for SailboxStatus {
    fn from(s: &str) -> SailboxStatus {
        match s {
            "running" => SailboxStatus::Running,
            "paused" => SailboxStatus::Paused,
            "sleeping" => SailboxStatus::Sleeping,
            "failed" => SailboxStatus::Failed,
            "terminated" => SailboxStatus::Terminated,
            other => SailboxStatus::Other(other.to_string()),
        }
    }
}

impl std::fmt::Display for SailboxStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl Serialize for SailboxStatus {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for SailboxStatus {
    fn deserialize<D: serde::Deserializer<'de>>(
        deserializer: D,
    ) -> Result<SailboxStatus, D::Error> {
        Ok(SailboxStatus::from(
            String::deserialize(deserializer)?.as_str(),
        ))
    }
}

/// A lifecycle status a client can filter by in [`ListSailboxesQuery`]. A closed set: a
/// client only filters by statuses the SDK knows. Server responses use the open
/// [`SailboxStatus`], which tolerates a status a newer backend introduces.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SailboxStatusFilter {
    /// Running and serving.
    Running,
    /// Paused in memory.
    Paused,
    /// Sleeping (checkpointed to disk).
    Sleeping,
    /// In a failed state.
    Failed,
    /// Terminated.
    Terminated,
}

/// Server-side ordering for list results.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SailboxListOrder {
    /// Activity recency: active/restorable boxes first, then updated_at.
    #[default]
    Activity,
    /// Newest-created boxes first.
    CreatedAtDesc,
}

impl SailboxListOrder {
    /// The wire string for this order.
    pub fn as_str(&self) -> &'static str {
        match self {
            SailboxListOrder::Activity => "activity",
            SailboxListOrder::CreatedAtDesc => "created_at_desc",
        }
    }

    /// Parse a wire string.
    pub fn parse(raw: &str) -> Option<SailboxListOrder> {
        match raw.trim() {
            "activity" => Some(SailboxListOrder::Activity),
            "created_at_desc" => Some(SailboxListOrder::CreatedAtDesc),
            _ => None,
        }
    }
}

impl SailboxStatusFilter {
    /// The wire string for this status.
    pub fn as_str(&self) -> &'static str {
        match self {
            SailboxStatusFilter::Running => "running",
            SailboxStatusFilter::Paused => "paused",
            SailboxStatusFilter::Sleeping => "sleeping",
            SailboxStatusFilter::Failed => "failed",
            SailboxStatusFilter::Terminated => "terminated",
        }
    }

    /// Parse a wire string, returning `None` for a status the SDK does not know.
    pub fn parse(s: &str) -> Option<SailboxStatusFilter> {
        match s {
            "running" => Some(SailboxStatusFilter::Running),
            "paused" => Some(SailboxStatusFilter::Paused),
            "sleeping" => Some(SailboxStatusFilter::Sleeping),
            "failed" => Some(SailboxStatusFilter::Failed),
            "terminated" => Some(SailboxStatusFilter::Terminated),
            _ => None,
        }
    }
}

/// Wire protocol for an ingress port or listener. An unrecognized value is
/// preserved in `Other` so a protocol a newer backend introduces never fails.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListenerProtocol {
    /// Raw TCP.
    Tcp,
    /// HTTP.
    Http,
    /// A protocol this SDK version does not recognize, kept verbatim.
    Other(String),
}

impl ListenerProtocol {
    /// The wire string for this protocol.
    pub fn as_str(&self) -> &str {
        match self {
            ListenerProtocol::Tcp => "tcp",
            ListenerProtocol::Http => "http",
            ListenerProtocol::Other(s) => s,
        }
    }
}

impl From<&str> for ListenerProtocol {
    fn from(s: &str) -> ListenerProtocol {
        match s {
            "tcp" => ListenerProtocol::Tcp,
            "http" => ListenerProtocol::Http,
            other => ListenerProtocol::Other(other.to_string()),
        }
    }
}

impl std::fmt::Display for ListenerProtocol {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl Serialize for ListenerProtocol {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for ListenerProtocol {
    fn deserialize<D: serde::Deserializer<'de>>(
        deserializer: D,
    ) -> Result<ListenerProtocol, D::Error> {
        Ok(ListenerProtocol::from(
            String::deserialize(deserializer)?.as_str(),
        ))
    }
}

/// Status of a listener's ingress route. An unrecognized value is preserved in
/// `Other` so a status a newer backend introduces never fails to parse.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListenerRouteStatus {
    /// The route state is not yet known.
    Unspecified,
    /// The route is being set up.
    Pending,
    /// The route is active and ready to carry traffic.
    Active,
    /// The route is being restored after a checkpoint or migration.
    Restoring,
    /// The route is temporarily unavailable.
    Unavailable,
    /// A status this SDK version does not recognize, kept verbatim.
    Other(String),
}

impl ListenerRouteStatus {
    /// The user-facing string for this status. The backend's proto spelling
    /// (`LISTENER_ROUTE_STATUS_*`) is normalized here, once, so no wrapper
    /// ever sees it.
    pub fn as_str(&self) -> &str {
        match self {
            ListenerRouteStatus::Unspecified => "unspecified",
            ListenerRouteStatus::Pending => "pending",
            ListenerRouteStatus::Active => "active",
            ListenerRouteStatus::Restoring => "restoring",
            ListenerRouteStatus::Unavailable => "unavailable",
            ListenerRouteStatus::Other(s) => s,
        }
    }
}

impl From<&str> for ListenerRouteStatus {
    /// Parse either the backend's proto spelling or the friendly form (so a
    /// serialized [`Listener`](crate::Listener) round-trips).
    fn from(s: &str) -> ListenerRouteStatus {
        match s {
            "LISTENER_ROUTE_STATUS_UNSPECIFIED" | "unspecified" => ListenerRouteStatus::Unspecified,
            "LISTENER_ROUTE_STATUS_PENDING" | "pending" => ListenerRouteStatus::Pending,
            "LISTENER_ROUTE_STATUS_ACTIVE" | "active" => ListenerRouteStatus::Active,
            "LISTENER_ROUTE_STATUS_RESTORING" | "restoring" => ListenerRouteStatus::Restoring,
            "LISTENER_ROUTE_STATUS_UNAVAILABLE" | "unavailable" => ListenerRouteStatus::Unavailable,
            other => ListenerRouteStatus::Other(other.to_string()),
        }
    }
}

impl std::fmt::Display for ListenerRouteStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl Serialize for ListenerRouteStatus {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for ListenerRouteStatus {
    fn deserialize<D: serde::Deserializer<'de>>(
        deserializer: D,
    ) -> Result<ListenerRouteStatus, D::Error> {
        Ok(ListenerRouteStatus::from(
            String::deserialize(deserializer)?.as_str(),
        ))
    }
}

/// Transport protocol a client requests when exposing an ingress port. A closed
/// set: a client can only request a protocol the SDK supports. Server responses
/// use the open [`ListenerProtocol`], which tolerates a protocol a newer backend adds.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IngressProtocol {
    /// Raw TCP.
    Tcp,
    /// HTTP.
    Http,
}

impl IngressProtocol {
    /// The wire string for this protocol.
    pub fn as_str(&self) -> &'static str {
        match self {
            IngressProtocol::Tcp => "tcp",
            IngressProtocol::Http => "http",
        }
    }

    /// Parse a wire string, returning `None` for a protocol the SDK does not
    /// support.
    pub fn parse(s: &str) -> Option<IngressProtocol> {
        match s {
            "tcp" => Some(IngressProtocol::Tcp),
            "http" => Some(IngressProtocol::Http),
            _ => None,
        }
    }
}

impl std::fmt::Display for IngressProtocol {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl Serialize for IngressProtocol {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

/// Options for [`Sailbox::checkpoint`](crate::Sailbox::checkpoint).
#[derive(Debug, Clone, Default)]
pub struct CheckpointOptions {
    /// Human-readable label for the checkpoint.
    pub name: Option<String>,
    /// Retention override; must be positive when given.
    pub ttl: Option<std::time::Duration>,
}

/// Backends report kernel spellings (`x86_64`, `aarch64`); the SDK speaks the
/// create-side vocabulary (`amd64`, `arm64`).
fn normalized_architecture<'de, D: serde::Deserializer<'de>>(
    deserializer: D,
) -> Result<String, D::Error> {
    let raw = String::deserialize(deserializer)?;
    Ok(match raw.as_str() {
        "x86_64" | "amd64" => "amd64".to_string(),
        "aarch64" | "arm64" => "arm64".to_string(),
        _ => raw,
    })
}

/// Named sailbox resource size. Sailboxes are offered in a small discrete CPU
/// menu rather than free-form vCPU/memory/disk. Each size sets the vCPU count
/// plus a default memory and disk ceiling. Billing is by observed usage, so a
/// larger size is a higher ceiling, not a higher bill.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SailboxSize {
    /// Small (1 vCPU): fastest cold starts, forks, and resumes, with
    /// lower ceilings that cap what a runaway workload can consume.
    Small,
    /// Medium (4 vCPU, default).
    Medium,
}

impl SailboxSize {
    /// The wire string for this size.
    pub fn as_str(&self) -> &'static str {
        match self {
            SailboxSize::Small => "s",
            SailboxSize::Medium => "m",
        }
    }

    /// Parse a wire string, returning `None` for an unsupported size.
    pub fn parse(s: &str) -> Option<SailboxSize> {
        match s {
            "s" => Some(SailboxSize::Small),
            "m" => Some(SailboxSize::Medium),
            _ => None,
        }
    }

    /// The allowed size labels in ascending order, for error messages and help.
    pub fn allowed() -> &'static [&'static str] {
        &["s", "m"]
    }
}

impl std::fmt::Display for SailboxSize {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl Serialize for SailboxSize {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

/// A running-sailbox handle: the fields needed to exec/file/listener against a
/// live box. Returned by create/resume/from_checkpoint.
#[derive(Debug, Clone, Default, Serialize)]
pub struct SailboxHandle {
    /// The sailbox's stable identifier.
    pub sailbox_id: String,
    /// The caller-supplied sailbox name.
    pub name: String,
    /// The sailbox's lifecycle status (for example `running`).
    pub status: SailboxStatus,
    /// Network address of the worker hosting the sailbox.
    pub worker_address: String,
    /// Endpoint used to open exec/file/listener streams to the box.
    pub exec_endpoint: String,
}

/// Read-only snapshot from get/list. Deliberately omits worker_address /
/// exec_endpoint (the list/get endpoints do not return them).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SailboxInfo {
    /// The sailbox's stable identifier.
    pub sailbox_id: String,
    /// Identifier of the owning app.
    pub app_id: String,
    /// Name of the owning app.
    pub app_name: String,
    /// Identifier of the image the sailbox was created from.
    pub image_id: String,
    /// The caller-supplied sailbox name.
    pub name: String,
    /// The sailbox's lifecycle status (for example `running`).
    pub status: SailboxStatus,
    /// Configured memory size, in mebibytes.
    pub memory_mib: i64,
    /// Configured number of virtual CPUs.
    pub vcpu_count: i64,
    /// Configured state-disk size, in gibibytes.
    pub state_disk_size_gib: i64,
    /// Requested CPU allocation, in vCPUs.
    pub cpu_requested_vcpu: i64,
    /// Current CPU usage, in vCPUs.
    pub cpu_used_vcpu: f64,
    /// Requested memory allocation, in bytes.
    pub memory_requested_bytes: i64,
    /// Current memory usage, in bytes.
    pub memory_used_bytes: i64,
    /// Requested disk allocation, in bytes.
    pub disk_requested_bytes: i64,
    /// Current disk usage, in bytes.
    pub disk_used_bytes: i64,
    /// CPU architecture of the sailbox, in the create-side vocabulary
    /// (`amd64` or `arm64`).
    #[serde(deserialize_with = "normalized_architecture", default)]
    pub architecture: String,
    /// Guest schema version the box is running, when reported by the backend.
    #[serde(default)]
    pub guest_schema_version: Option<i64>,
    /// Human-readable error detail, present when the box is in an error state.
    #[serde(default)]
    pub error_message: Option<String>,
    /// Monotonic checkpoint generation counter for the sailbox.
    pub checkpoint_generation: i64,
    /// When the box last started, if it has started.
    #[serde(with = "time::serde::rfc3339::option", default)]
    pub started_at: Option<OffsetDateTime>,
    /// When the most recent checkpoint was taken, if any.
    #[serde(with = "time::serde::rfc3339::option", default)]
    pub last_checkpointed_at: Option<OffsetDateTime>,
    /// When the sailbox was created.
    #[serde(with = "time::serde::rfc3339")]
    pub created_at: OffsetDateTime,
    /// When the sailbox was last updated.
    #[serde(with = "time::serde::rfc3339")]
    pub updated_at: OffsetDateTime,
}

/// One page of list results plus the pagination envelope.
#[derive(Debug, Clone, Serialize)]
pub struct SailboxPage {
    /// The sailboxes in this page.
    pub items: Vec<SailboxInfo>,
    /// Maximum number of items requested for this page.
    pub limit: i64,
    /// Zero-based offset of the first item in this page.
    pub offset: i64,
    /// Total number of sailboxes matching the query across all pages.
    pub total: i64,
    /// True when further pages exist beyond this one.
    pub has_more: bool,
}

/// Query parameters for [`Client::sailbox_spend`](crate::Client::sailbox_spend).
#[derive(Debug, Clone, Default)]
pub struct SailboxSpendQuery {
    /// Restrict spend to sailboxes owned by this app id.
    pub app_id: Option<String>,
    /// Restrict spend to one sailbox id.
    pub sailbox_id: Option<String>,
    /// Inclusive lower bound for the spend window. The API defaults to the
    /// current UTC month when omitted.
    pub from: Option<OffsetDateTime>,
    /// Exclusive upper bound for the spend window. The API defaults to now when
    /// omitted.
    pub to: Option<OffsetDateTime>,
}

/// Estimated sailbox spend over a requested time window.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SailboxSpendResponse {
    /// Start of the spend window.
    #[serde(with = "time::serde::rfc3339")]
    pub start_at: OffsetDateTime,
    /// End of the spend window.
    #[serde(with = "time::serde::rfc3339")]
    pub end_at: OffsetDateTime,
    /// Cost from closed billing segments, in USD nanos.
    pub finalized_cost_usd_nanos: i64,
    /// Estimated cost from still-open billing segments, in USD nanos.
    pub estimated_active_cost_usd_nanos: i64,
    /// Total estimated cost, in USD nanos.
    pub estimated_total_cost_usd_nanos: i64,
    /// Total observed duration, in seconds.
    pub duration_seconds: i64,
    /// Total vCPU-seconds observed.
    pub vcpu_seconds: f64,
    /// Total GiB-seconds of memory observed.
    pub memory_gib_seconds: f64,
    /// Total GiB-seconds of state disk observed.
    pub state_disk_gib_seconds: f64,
    /// Whether non-zero pricing rates were configured by the backend.
    pub pricing_configured: bool,
    /// Pricing rates used for the local estimate.
    pub rates: SailboxSpendRates,
    /// Per-sailbox spend summaries.
    pub sailboxes: Vec<SailboxSpendItem>,
}

/// Rate metadata returned with a sailbox spend estimate.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SailboxSpendRates {
    /// Nanodollars per vCPU-second.
    pub vcpu_second_usd_nanos: i64,
    /// Nanodollars per memory GiB-second.
    pub memory_gib_second_usd_nanos: i64,
    /// Nanodollars per arm64 state-disk GiB-second.
    pub state_disk_gib_second_usd_nanos: i64,
    /// Nanodollars per sailbox creation.
    pub creation_usd_nanos: i64,
    /// Nanodollars per amd64 state-disk GiB-second.
    #[serde(default)]
    pub amd64_state_disk_gib_second_usd_nanos: i64,
}

/// Estimated spend for one sailbox over a requested time window.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SailboxSpendItem {
    /// Sailbox id.
    pub sailbox_id: String,
    /// Owning app id.
    pub app_id: String,
    /// Cost from closed billing segments, in USD nanos.
    pub finalized_cost_usd_nanos: i64,
    /// Estimated cost from still-open billing segments, in USD nanos.
    pub estimated_active_cost_usd_nanos: i64,
    /// Total estimated cost, in USD nanos.
    pub estimated_total_cost_usd_nanos: i64,
    /// Observed duration, in seconds.
    pub duration_seconds: i64,
    /// Observed vCPU-seconds.
    pub vcpu_seconds: f64,
    /// Observed GiB-seconds of memory.
    pub memory_gib_seconds: f64,
    /// Observed GiB-seconds of state disk.
    pub state_disk_gib_seconds: f64,
    /// Whether this row includes an active billing segment.
    pub active: bool,
}

/// Query parameters for [`Client::sailbox_metrics`](crate::Client::sailbox_metrics).
#[derive(Debug, Clone)]
pub struct SailboxMetricsQuery {
    /// Window name accepted by the API, for example `1h`, `6h`, `24h`, or `7d`.
    pub range: String,
}

impl Default for SailboxMetricsQuery {
    fn default() -> SailboxMetricsQuery {
        SailboxMetricsQuery {
            range: "24h".to_string(),
        }
    }
}

/// Resource-usage time series for one sailbox.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SailboxMetricsResponse {
    /// Echoed range selected by the API.
    pub range: String,
    /// Time-ordered metric points.
    pub data: Vec<SailboxMetricPoint>,
}

/// One bucket in a sailbox resource-usage time series.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SailboxMetricPoint {
    /// Bucket timestamp.
    #[serde(with = "time::serde::rfc3339")]
    pub timestamp: OffsetDateTime,
    /// Current CPU usage, in vCPUs.
    pub cpu_used_vcpu: f64,
    /// Requested CPU allocation, in vCPUs.
    pub cpu_requested_vcpu: i64,
    /// Current memory usage, in bytes.
    pub memory_used_bytes: i64,
    /// Requested memory allocation, in bytes.
    pub memory_requested_bytes: i64,
    /// Current disk usage, in bytes.
    pub disk_used_bytes: i64,
    /// Requested disk allocation, in bytes.
    pub disk_requested_bytes: i64,
}

/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
/// status after checkpointing (a running box is snapshotted; a paused/sleeping
/// one returns its existing checkpoint), so the binding can sync its handle.
#[derive(Debug, Clone, Serialize)]
pub struct SailboxCheckpoint {
    /// Stable identifier of the checkpoint.
    pub checkpoint_id: String,
    /// Identifier of the sailbox the checkpoint was taken from.
    pub sailbox_id: String,
    /// Checkpoint generation counter captured by this checkpoint.
    pub checkpoint_generation: i64,
    /// When the handle expires and becomes eligible for garbage collection;
    /// `None` for a handle that carries no fresh retention bound (a paused or
    /// sleeping source).
    #[serde(with = "time::serde::rfc3339::option")]
    pub expires_at: Option<OffsetDateTime>,
    /// Source sailbox's lifecycle status after checkpointing.
    pub status: SailboxStatus,
}

/// Filters for list/list_page.
#[derive(Debug, Clone)]
pub struct ListSailboxesQuery {
    /// Restrict results to sailboxes owned by the app with this id.
    pub app_id: Option<String>,
    /// Restrict results to sailboxes in this lifecycle status.
    pub status: Option<SailboxStatusFilter>,
    /// Free-text search filter applied by the backend.
    pub search: Option<String>,
    /// Exclude sailboxes whose guest schema version exceeds this value.
    pub max_guest_schema_version: Option<i64>,
    /// Ordering to apply before pagination.
    pub order: SailboxListOrder,
    /// Maximum number of items to return.
    pub limit: i64,
    /// Zero-based offset of the first item to return.
    pub offset: i64,
}

/// Default page size for listing, matching the sailbox API's own default. The
/// API rejects `limit=0`, so the derived all-zero default cannot be used.
#[doc(hidden)]
pub const DEFAULT_LIST_LIMIT: i64 = 50;

impl Default for ListSailboxesQuery {
    fn default() -> ListSailboxesQuery {
        ListSailboxesQuery {
            app_id: None,
            status: None,
            search: None,
            max_guest_schema_version: None,
            order: SailboxListOrder::Activity,
            limit: DEFAULT_LIST_LIMIT,
            offset: 0,
        }
    }
}

/// One guest ingress port to reserve at create time.
#[derive(Debug, Clone, Serialize)]
pub struct IngressPort {
    /// Port inside the guest to expose for ingress.
    pub guest_port: u32,
    /// Transport protocol for the port (for example `tcp`).
    pub protocol: IngressProtocol,
    /// Source addresses allowed to reach the port; empty means all sources are
    /// allowed. Always sent (even when empty) so the request states it
    /// explicitly rather than relying on an omitted field.
    pub allowlist: Vec<String>,
}

/// One NFS volume mount.
#[derive(Debug, Clone, Serialize)]
pub struct VolumeMount {
    /// Identifier of the NFS volume to mount.
    pub volume_id: String,
    /// Absolute path inside the guest where the volume is mounted.
    pub mount_path: String,
}

/// A managed NFS volume as returned by the sailbox-volume API. (The local
/// mount path is a binding-side concern and not part of this wire type.)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumeInfo {
    /// Stable identifier of the volume.
    pub volume_id: String,
    /// Caller-supplied volume name.
    pub name: String,
    /// Storage backend serving the volume.
    pub backend: String,
    /// Lifecycle status of the volume.
    pub status: String,
    /// When the volume was created, if reported.
    #[serde(with = "time::serde::rfc3339::option", default)]
    pub created_at: Option<OffsetDateTime>,
    /// When the volume was last updated, if reported.
    #[serde(with = "time::serde::rfc3339::option", default)]
    pub updated_at: Option<OffsetDateTime>,
}

/// The wire shape of the add-listener response: the scheduler resolves the
/// public endpoint in the response (so a caller renders it without a second
/// lookup) but reports nothing about routing, so it converts to a [`Listener`]
/// with an unspecified route status.
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct AddListenerWire {
    pub(crate) guest_port: u32,
    pub(crate) protocol: ListenerProtocol,
    #[serde(default)]
    pub(crate) public_url: String,
    #[serde(default, rename = "tcp_public_host")]
    pub(crate) public_host: String,
    #[serde(default, rename = "tcp_public_port")]
    pub(crate) public_port: u32,
}

impl From<AddListenerWire> for crate::worker::Listener {
    fn from(wire: AddListenerWire) -> crate::worker::Listener {
        crate::worker::Listener {
            guest_port: wire.guest_port,
            protocol: wire.protocol,
            // The expose response says nothing about reachability; confirm
            // with wait_for_listener or re-fetch via get/list.
            route_status: ListenerRouteStatus::Unspecified,
            public_url: wire.public_url,
            public_host: wire.public_host,
            public_port: wire.public_port,
        }
    }
}

/// A CA-signed SSH user certificate plus the key id that identifies the signing
/// org (`org=<id>;fp=...;iat=...`).
#[derive(Debug, Clone)]
pub struct IssuedUserCert {
    /// The OpenSSH certificate (`<type>-cert-v01@openssh.com AAAA...`).
    pub certificate: String,
    /// The certificate key id.
    pub key_id: String,
}

/// How to reach an exposed listener: a routable HTTPS URL for `http`
/// listeners, or a host/port any TCP client can dial for `tcp` listeners.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListenerEndpoint {
    /// The HTTPS URL to reach the guest service.
    Http {
        /// Routable URL.
        url: String,
    },
    /// The address to dial for a raw-TCP listener.
    Tcp {
        /// Hostname to dial.
        host: String,
        /// Port to dial.
        port: u32,
    },
}

impl std::fmt::Display for ListenerEndpoint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ListenerEndpoint::Http { url } => f.write_str(url),
            ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
        }
    }
}

/// Inputs to create a sailbox. The image is a typed [`ImageSpec`](crate::image::ImageSpec);
/// build one fluently with [`ImageDefinition`](crate::imagebuild::ImageDefinition).
#[derive(Debug, Clone)]
pub struct CreateSailboxRequest {
    /// Identifier of the app that will own the sailbox.
    pub app_id: String,
    /// Caller-supplied name for the sailbox.
    pub name: String,
    /// Guest ingress ports to reserve at create time.
    pub ingress_ports: Vec<IngressPort>,
    /// NFS volumes to mount into the guest.
    pub volume_mounts: Vec<VolumeMount>,
    /// The image to create the sailbox from. Serialized to canonical proto-JSON
    /// and sent as the `image` field.
    pub image: crate::image::ImageSpec,
    /// Requested resource size; defaults server-side when absent.
    pub size: Option<SailboxSize>,
    /// Optional memory limit in whole GiB, within the size's range; the
    /// size's default when absent.
    pub memory_gib: Option<u32>,
    /// Optional disk size in whole GiB, within the size's range; the size's
    /// default when absent.
    pub disk_gib: Option<u32>,
    /// Optional idle timeout in minutes after which Sail may sleep the
    /// sailbox.
    pub autosleep_timeout_minutes: Option<i64>,
    /// Enable SSH on the new sailbox after create: trust the org SSH CA, start
    /// `sshd`, and expose guest port 22 as TCP once the CA-only daemon owns it.
    /// An explicit port-22 ingress entry contributes just its allowlist.
    pub ssh: bool,
}

impl Default for CreateSailboxRequest {
    /// An empty request for a plain Debian-base sailbox: fill in `app_id` and
    /// `name`, override the rest as needed.
    fn default() -> CreateSailboxRequest {
        CreateSailboxRequest {
            app_id: String::new(),
            name: String::new(),
            ingress_ports: Vec::new(),
            volume_mounts: Vec::new(),
            image: crate::image::ImageSpec {
                base: Some(crate::image::BaseImage::Debian),
                ..crate::image::ImageSpec::default()
            },
            size: None,
            memory_gib: None,
            disk_gib: None,
            autosleep_timeout_minutes: None,
            ssh: false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn list_query_default_uses_a_valid_nonzero_limit() {
        // The sailbox API rejects limit=0, so the default must be the API's own
        // page size, not the derived zero.
        let query = ListSailboxesQuery::default();
        assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
        assert!(query.limit > 0);
        assert_eq!(query.offset, 0);
    }
}
