//! Public, transport-only Station connector protocol.
//!
//! Connector authors receive no account, route, Attempt, filesystem, network,
//! credential, database, OBS, or host authority through this crate.

use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

pub type JsonObject = Map<String, Value>;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectorReleaseIdentity {
    pub package_id: String,
    pub package_version: String,
    pub manifest_digest: String,
    pub contribution_id: String,
    pub executable_digest: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectorResourceGrant {
    pub kind: String,
    pub resource: String,
    pub access: Vec<String>,
}

/// An already-connected host-owned byte stream for one exact reviewed network
/// grant. Connectors must not create sockets or perform discovery themselves.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectorNetworkBroker {
    pub resource: String,
    pub file_descriptor: i32,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectorSource {
    pub source_id: String,
    pub source_type: String,
    pub display_name: String,
    #[serde(default)]
    pub metadata: JsonObject,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ObservationProtocolIdentity {
    pub r#type: String,
    pub contract_version: u32,
    pub payload_schema_digest: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectorObservation {
    pub source_event_id: String,
    pub protocol: ObservationProtocolIdentity,
    pub source_id: String,
    pub occurred_at: String,
    pub observed_at: String,
    pub payload: JsonObject,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectorRuntimeLimits {
    pub maximum_message_bytes: usize,
    pub startup_timeout_ms: u64,
    pub health_timeout_ms: u64,
    pub shutdown_grace_ms: u64,
    #[serde(rename = "memoryLimitMiB")]
    pub memory_limit_mib: u64,
    pub maximum_restarts: u32,
    pub restart_window_ms: u64,
    pub initial_backoff_ms: u64,
    pub maximum_backoff_ms: u64,
}

impl Default for ConnectorRuntimeLimits {
    fn default() -> Self {
        Self {
            maximum_message_bytes: 64 * 1_024,
            startup_timeout_ms: 5_000,
            health_timeout_ms: 15_000,
            shutdown_grace_ms: 3_000,
            memory_limit_mib: 128,
            maximum_restarts: 5,
            restart_window_ms: 60_000,
            initial_backoff_ms: 500,
            maximum_backoff_ms: 10_000,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum HostToConnectorMessage {
    #[serde(rename = "host.initialize", rename_all = "camelCase")]
    Initialize {
        session_id: String,
        protocol_version: u32,
        expected_identity: ConnectorReleaseIdentity,
        observation_protocols: Vec<ObservationProtocolIdentity>,
        configuration: JsonObject,
        granted_resources: Vec<ConnectorResourceGrant>,
        #[serde(default)]
        network_brokers: Vec<ConnectorNetworkBroker>,
        limits: ConnectorRuntimeLimits,
    },
    #[serde(rename = "host.start", rename_all = "camelCase")]
    Start { session_id: String },
    #[serde(rename = "host.stop", rename_all = "camelCase")]
    Stop { session_id: String, reason: String },
    #[serde(rename = "host.ping", rename_all = "camelCase")]
    Ping { session_id: String, nonce: String },
    #[serde(rename = "host.observation.receipt", rename_all = "camelCase")]
    ObservationReceipt {
        session_id: String,
        source_event_id: String,
        disposition: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        detail: Option<String>,
    },
}

impl HostToConnectorMessage {
    pub fn session_id(&self) -> &str {
        match self {
            Self::Initialize { session_id, .. }
            | Self::Start { session_id }
            | Self::Stop { session_id, .. }
            | Self::Ping { session_id, .. }
            | Self::ObservationReceipt { session_id, .. } => session_id,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ConnectorToHostMessage {
    #[serde(rename = "connector.hello", rename_all = "camelCase")]
    Hello {
        session_id: String,
        protocol_version: u32,
        identity: ConnectorReleaseIdentity,
    },
    #[serde(rename = "connector.status", rename_all = "camelCase")]
    Status {
        session_id: String,
        status: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        detail: Option<String>,
        occurred_at: String,
    },
    #[serde(rename = "connector.health", rename_all = "camelCase")]
    Health {
        session_id: String,
        status: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        summary: Option<String>,
        occurred_at: String,
        #[serde(default)]
        checks: Vec<Value>,
    },
    #[serde(rename = "connector.sources", rename_all = "camelCase")]
    Sources {
        session_id: String,
        revision: u64,
        observed_at: String,
        sources: Vec<ConnectorSource>,
    },
    #[serde(rename = "connector.observation", rename_all = "camelCase")]
    Observation {
        session_id: String,
        observation: ConnectorObservation,
    },
    #[serde(rename = "connector.pong", rename_all = "camelCase")]
    Pong {
        session_id: String,
        nonce: String,
        occurred_at: String,
    },
}

impl ConnectorToHostMessage {
    pub fn session_id(&self) -> &str {
        match self {
            Self::Hello { session_id, .. }
            | Self::Status { session_id, .. }
            | Self::Health { session_id, .. }
            | Self::Sources { session_id, .. }
            | Self::Observation { session_id, .. }
            | Self::Pong { session_id, .. } => session_id,
        }
    }
}

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

    #[derive(Deserialize)]
    #[serde(rename_all = "camelCase")]
    struct Fixture {
        connector_trace: Vec<Frame>,
    }

    #[derive(Deserialize)]
    struct Frame {
        direction: String,
        message: Value,
    }

    #[test]
    fn rust_protocol_decodes_every_canonical_v1_frame() {
        let fixture: Fixture =
            serde_json::from_str(include_str!("../../../protocol/station/v1-fixtures.json"))
                .expect("canonical fixture");
        for frame in fixture.connector_trace {
            match frame.direction.as_str() {
                "host_to_connector" => {
                    serde_json::from_value::<HostToConnectorMessage>(frame.message)
                        .expect("host frame must match Rust SDK");
                }
                "connector_to_host" => {
                    serde_json::from_value::<ConnectorToHostMessage>(frame.message)
                        .expect("connector frame must match Rust SDK");
                }
                direction => panic!("unknown fixture direction {direction}"),
            }
        }
    }
}
