//! Rust-shaped wrappers around the wire `MxValue`, `MxArray`, and
//! `MxStatusProxy` types.
//!
//! [`MxValue`] keeps both the original protobuf message and a friendly
//! [`MxValueProjection`] enum, so callers can pass values through the wire
//! without losing information while still being able to pattern-match on
//! the typed variant. The same split applies to [`MxArrayValue`] and
//! [`MxArrayProjection`]. [`MxStatus`] wraps the MXAccess `MxStatusProxy`
//! status envelope.

use crate::generated::mxaccess_gateway::v1::mx_array::Values;
use crate::generated::mxaccess_gateway::v1::mx_value::Kind;
use crate::generated::mxaccess_gateway::v1::{
    BoolArray, DoubleArray, FloatArray, Int32Array, Int64Array, MxArray, MxDataType,
    MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue as ProtoMxValue, RawArray,
    StringArray, TimestampArray,
};

/// Owned `MxValue` carrying both the raw protobuf message and a typed
/// [`MxValueProjection`] view.
///
/// The constructors set both `data_type` and `variant_type` to the values
/// the worker expects so values built locally round-trip through MXAccess
/// without surprises.
#[derive(Clone, Debug, PartialEq)]
pub struct MxValue {
    raw: ProtoMxValue,
    projection: MxValueProjection,
}

impl MxValue {
    /// Wrap a protobuf [`ProtoMxValue`] and compute its
    /// [`MxValueProjection`].
    pub fn from_proto(raw: ProtoMxValue) -> Self {
        let projection = MxValueProjection::from_proto(&raw);
        Self { raw, projection }
    }

    /// Build a boolean `MxValue` (`MxDataType::Boolean`, `VT_BOOL`).
    pub fn bool(value: bool) -> Self {
        Self::from_proto(ProtoMxValue {
            data_type: MxDataType::Boolean as i32,
            variant_type: "VT_BOOL".to_owned(),
            kind: Some(Kind::BoolValue(value)),
            ..ProtoMxValue::default()
        })
    }

    /// Build a 32-bit integer `MxValue` (`MxDataType::Integer`, `VT_I4`).
    pub fn int32(value: i32) -> Self {
        Self::from_proto(ProtoMxValue {
            data_type: MxDataType::Integer as i32,
            variant_type: "VT_I4".to_owned(),
            kind: Some(Kind::Int32Value(value)),
            ..ProtoMxValue::default()
        })
    }

    /// Build a 64-bit integer `MxValue` (`MxDataType::Integer`, `VT_I8`).
    pub fn int64(value: i64) -> Self {
        Self::from_proto(ProtoMxValue {
            data_type: MxDataType::Integer as i32,
            variant_type: "VT_I8".to_owned(),
            kind: Some(Kind::Int64Value(value)),
            ..ProtoMxValue::default()
        })
    }

    /// Build a 32-bit float `MxValue` (`MxDataType::Float`, `VT_R4`).
    pub fn float(value: f32) -> Self {
        Self::from_proto(ProtoMxValue {
            data_type: MxDataType::Float as i32,
            variant_type: "VT_R4".to_owned(),
            kind: Some(Kind::FloatValue(value)),
            ..ProtoMxValue::default()
        })
    }

    /// Build a 64-bit float `MxValue` (`MxDataType::Double`, `VT_R8`).
    pub fn double(value: f64) -> Self {
        Self::from_proto(ProtoMxValue {
            data_type: MxDataType::Double as i32,
            variant_type: "VT_R8".to_owned(),
            kind: Some(Kind::DoubleValue(value)),
            ..ProtoMxValue::default()
        })
    }

    /// Build a string `MxValue` (`MxDataType::String`, `VT_BSTR`).
    pub fn string(value: impl Into<String>) -> Self {
        Self::from_proto(ProtoMxValue {
            data_type: MxDataType::String as i32,
            variant_type: "VT_BSTR".to_owned(),
            kind: Some(Kind::StringValue(value.into())),
            ..ProtoMxValue::default()
        })
    }

    /// Borrow the underlying protobuf message exactly as it will travel
    /// over the wire.
    pub fn raw(&self) -> &ProtoMxValue {
        &self.raw
    }

    /// Borrow the typed projection.
    pub fn projection(&self) -> &MxValueProjection {
        &self.projection
    }

    /// Consume the wrapper and return the underlying protobuf message.
    pub fn into_proto(self) -> ProtoMxValue {
        self.raw
    }
}

impl From<MxValue> for ProtoMxValue {
    fn from(value: MxValue) -> Self {
        value.into_proto()
    }
}

impl From<ProtoMxValue> for MxValue {
    fn from(value: ProtoMxValue) -> Self {
        Self::from_proto(value)
    }
}

/// Typed view over an [`MxValue`].
///
/// Mirrors the `MxValue::Kind` oneof on the wire, plus a [`MxValueProjection::Null`]
/// variant for `is_null=true` and a [`MxValueProjection::Unset`] variant for
/// values that arrive without a `kind` set.
#[derive(Clone, Debug, PartialEq)]
pub enum MxValueProjection {
    /// No `kind` was present on the wire.
    Unset,
    /// `is_null = true` on the wire.
    Null,
    /// Boolean value.
    Bool(bool),
    /// 32-bit signed integer value.
    Int32(i32),
    /// 64-bit signed integer value.
    Int64(i64),
    /// 32-bit float value.
    Float(f32),
    /// 64-bit float value.
    Double(f64),
    /// UTF-8 string value.
    String(String),
    /// Wall-clock timestamp.
    Timestamp(prost_types::Timestamp),
    /// Array value carrying a homogeneous element type.
    Array(MxArrayValue),
    /// Opaque variant payload that the gateway could not project to a typed
    /// scalar.
    Raw(Vec<u8>),
}

impl MxValueProjection {
    fn from_proto(value: &ProtoMxValue) -> Self {
        if value.is_null {
            return Self::Null;
        }

        match value.kind.as_ref() {
            Some(Kind::BoolValue(value)) => Self::Bool(*value),
            Some(Kind::Int32Value(value)) => Self::Int32(*value),
            Some(Kind::Int64Value(value)) => Self::Int64(*value),
            Some(Kind::FloatValue(value)) => Self::Float(*value),
            Some(Kind::DoubleValue(value)) => Self::Double(*value),
            Some(Kind::StringValue(value)) => Self::String(value.clone()),
            Some(Kind::TimestampValue(value)) => Self::Timestamp(*value),
            Some(Kind::ArrayValue(value)) => Self::Array(MxArrayValue::from_proto(value.clone())),
            Some(Kind::RawValue(value)) => Self::Raw(value.clone()),
            // SparseArrayValue is write-only: the gateway expands it before forwarding
            // to the worker and never emits it in events or read replies. Map it to
            // Unset so any read-side code that encounters a stale or mis-routed
            // sparse value degrades gracefully rather than panicking.
            Some(Kind::SparseArrayValue(_)) | None => Self::Unset,
        }
    }
}

/// Owned `MxArray` carrying both the raw protobuf message and a typed
/// [`MxArrayProjection`] view of its elements.
#[derive(Clone, Debug, PartialEq)]
pub struct MxArrayValue {
    raw: MxArray,
    projection: MxArrayProjection,
}

impl MxArrayValue {
    /// Wrap a protobuf [`MxArray`] and compute its
    /// [`MxArrayProjection`].
    pub fn from_proto(raw: MxArray) -> Self {
        let projection = MxArrayProjection::from_proto(&raw);
        Self { raw, projection }
    }

    /// Build a one-dimensional string array (`VT_ARRAY|VT_BSTR`).
    pub fn string(values: Vec<String>) -> Self {
        Self::from_proto(MxArray {
            element_data_type: MxDataType::String as i32,
            variant_type: "VT_ARRAY|VT_BSTR".to_owned(),
            dimensions: vec![values.len() as u32],
            values: Some(Values::StringValues(StringArray { values })),
            ..MxArray::default()
        })
    }

    /// Borrow the underlying protobuf array message.
    pub fn raw(&self) -> &MxArray {
        &self.raw
    }

    /// Borrow the typed projection of the array's elements.
    pub fn projection(&self) -> &MxArrayProjection {
        &self.projection
    }
}

/// Typed view over an [`MxArrayValue`].
///
/// Each variant matches the corresponding `MxArray::Values` oneof arm on
/// the wire.
#[derive(Clone, Debug, PartialEq)]
pub enum MxArrayProjection {
    /// No `values` oneof was present on the wire.
    Unset,
    /// Boolean elements.
    Bool(Vec<bool>),
    /// 32-bit signed integer elements.
    Int32(Vec<i32>),
    /// 64-bit signed integer elements.
    Int64(Vec<i64>),
    /// 32-bit float elements.
    Float(Vec<f32>),
    /// 64-bit float elements.
    Double(Vec<f64>),
    /// UTF-8 string elements.
    String(Vec<String>),
    /// Timestamp elements.
    Timestamp(Vec<prost_types::Timestamp>),
    /// Opaque variant payloads, one per element.
    Raw(Vec<Vec<u8>>),
}

impl MxArrayProjection {
    fn from_proto(array: &MxArray) -> Self {
        match array.values.as_ref() {
            Some(Values::BoolValues(BoolArray { values })) => Self::Bool(values.clone()),
            Some(Values::Int32Values(Int32Array { values })) => Self::Int32(values.clone()),
            Some(Values::Int64Values(Int64Array { values })) => Self::Int64(values.clone()),
            Some(Values::FloatValues(FloatArray { values })) => Self::Float(values.clone()),
            Some(Values::DoubleValues(DoubleArray { values })) => Self::Double(values.clone()),
            Some(Values::StringValues(StringArray { values })) => Self::String(values.clone()),
            Some(Values::TimestampValues(TimestampArray { values })) => {
                Self::Timestamp(values.clone())
            }
            Some(Values::RawValues(RawArray { values })) => Self::Raw(values.clone()),
            None => Self::Unset,
        }
    }
}

/// Typed wrapper around the MXAccess `MxStatusProxy` envelope, the
/// per-value status that accompanies every `OnDataChange`/`Read` reply.
#[derive(Clone, Debug, PartialEq)]
pub struct MxStatus {
    raw: MxStatusProxy,
}

impl MxStatus {
    /// Wrap a protobuf [`MxStatusProxy`].
    pub fn from_proto(raw: MxStatusProxy) -> Self {
        Self { raw }
    }

    /// Borrow the underlying protobuf message.
    pub fn raw(&self) -> &MxStatusProxy {
        &self.raw
    }

    /// Raw `MXSTATUS_PROXY.Success` member, carried verbatim from COM.
    ///
    /// This is a diagnostic value, not a verdict: the wire contract makes
    /// [`Self::category`] authoritative, and `ensure_mxaccess_success` branches
    /// on the category alone.
    pub fn success(&self) -> i32 {
        self.raw.success
    }

    /// Decode the status category, or `None` if the wire value is not a
    /// known [`MxStatusCategory`].
    pub fn category(&self) -> Option<MxStatusCategory> {
        MxStatusCategory::try_from(self.raw.category).ok()
    }

    /// Decode the source (provider, gateway, worker) that flagged the
    /// status, or `None` for unknown values.
    pub fn detected_by(&self) -> Option<MxStatusSource> {
        MxStatusSource::try_from(self.raw.detected_by).ok()
    }

    /// `MXSTATUS_PROXY.Detail` numeric reason code.
    pub fn detail(&self) -> i32 {
        self.raw.detail
    }

    /// Raw, undecoded category integer (useful for forward compatibility).
    pub fn raw_category(&self) -> i32 {
        self.raw.raw_category
    }

    /// Raw, undecoded detected-by integer.
    pub fn raw_detected_by(&self) -> i32 {
        self.raw.raw_detected_by
    }

    /// Optional human-readable diagnostic from MXAccess.
    pub fn diagnostic_text(&self) -> &str {
        &self.raw.diagnostic_text
    }
}
