//
// Copyright (C) 2025 Automated Design Corp.. All Rights Reserved.
// Created Date: 2025-03-14 11:57:58
// -----
// Last Modified: 2025-03-16 18:01:47
// -----
// 
//



//! # CoreLink Types Module
//!
//! This module defines the core types and structures used in the CoreLink IPC protocol.
//! It includes the `CoreLinkMessage` struct, which represents a message in the protocol,
//! and the `CoreLinkMessageType` enum, which defines the types of messages that can be sent.
//!
//! The module also provides utility functions for serializing messages, calculating CRC32 checksums,
//! and validating messages.
//!
//! ## Key Features
//! - **Message Types**: Defines various message types such as `Read`, `Write`, `Subscribe`, and `Broadcast`.
//! - **Control Codes**: Supports control commands like `Shutdown`, `Initialize`, and `Run` for node management.
//! - **Payloads**: Provides structured payloads for subscriptions, topic values, and heartbeats.
//! - **Validation**: Includes methods to validate messages and compute CRC32 checksums.
//!
//! ## Usage
//! This module is used to construct, serialize, and validate messages in the CoreLink protocol.
//! It is designed to work with the `UdpBroker` and `UdpClient` for low-latency IPC communication.
//!
//! ## Example
//! ```rust
//! let message = CoreLinkMessage {
//!     transaction_id: 1,
//!     timecode: 0,
//!     node_id: "example_node".to_string(),
//!     message_type: CoreLinkMessageType::Read,
//!     crc: 0,
//!     data: serde_json::Value::Null,
//! };
//! message.update_crc32();
//! let bytes = message.to_bytes();
//! ```
//!
//! ## Dependencies
//! - `serde`: For serialization and deserialization.
//! - `num_derive`: For deriving `FromPrimitive` and `ToPrimitive` traits.
//! - `crc32fast`: For CRC32 checksum calculation.
//!
//! ## Notes
//! - Messages are serialized to JSON for flexibility but can be a performance bottleneck for high-frequency messaging.
//! - CRC32 checksums are used to verify message integrity.

use serde::{Serialize, Deserialize};
use num_derive::{FromPrimitive, ToPrimitive};
use crc32fast::Hasher;

/// Enumerates the message type of a CoreLink message.
///
/// Each variant represents a specific type of message that can be sent or received.
/// The enum is serialized as an `i16` for compatibility with the protocol.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, ToPrimitive, FromPrimitive, Eq, PartialEq)]
#[repr(i16)]
#[serde(try_from = "i16", into = "i16")]
pub enum CoreLinkMessageType {
    /// No operation. This message type is used as a placeholder and should not trigger any action.
    NoOp = 0,

    /// A request to read a value from the register specified by the `fqdn`.
    /// The response will contain the requested value or an error message.
    Read = 0x04,

    /// A request to write a value to the register specified by the `fqdn`.
    /// The response will indicate success or provide an error message.
    Write = 0x0F,

    /// A request to register with the broker. 
    /// The `fqdn` shall contain the `node_id`.
    /// The response will confirm the registration or provide an error message.
    Register = 0xFA,

    /// A request to subscribe to updates for the register specified by the `fqdn`.
    /// The response will confirm the subscription or provide an error message.
    Subscribe = 0xFD,

    /// A broadcast message, typically used for unsolicited updates of a subscription value.
    /// No response is expected for broadcast messages.
    Broadcast = 0xFF,

    /// A heartbeat message, used to indicate the sender is still active.
    /// No response is expected for heartbeat messages.
    Heartbeat = 0x1FF,

    /// A control command to the node itself, not to its data.
    /// This is usually used to initialize, configure, start, or stop processing in a node.
    Control = 0x200,
}

impl TryFrom<i16> for CoreLinkMessageType {
    type Error = anyhow::Error;

    /// Attempts to convert an `i16` value to a `CoreLinkMessageType`.
    ///
    /// ### Parameters
    /// - `value`: The `i16` value to convert.
    ///
    /// ### Returns
    /// The corresponding `CoreLinkMessageType` or an error if the value is invalid.
    fn try_from(value: i16) -> Result<Self, Self::Error> {
        num_traits::FromPrimitive::from_i16(value)
            .ok_or_else(|| anyhow::anyhow!("Invalid CoreLinkMessageType value: {}", value))
    }
}

impl Into<i16> for CoreLinkMessageType {
    /// Converts a `CoreLinkMessageType` to its corresponding `i16` value.
    ///
    /// ### Returns
    /// The `i16` representation of the `CoreLinkMessageType`.
    fn into(self) -> i16 {
        self as i16
    }
}

/// Enumerates control codes for `CoreLinkControlMessage`.
///
/// These codes are used to send control commands to a node, such as shutdown, initialization, or custom commands.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[repr(u8)]
pub enum CoreLinkControlCode {
    /// No operation.
    NoOp = 0,

    /// Shutdown the node, close all communications, and terminate the process.
    Shutdown = 2,

    /// Reset or initialize the node.
    Initialize = 20,

    /// Load configuration and prepare to operate in run mode.
    Prepare = 24,

    /// Full run mode.
    Run = 50,

    /// Halt and catch fire (used for testing).
    Hcf = 0x9D,

    /// A custom, implementation-specific code.
    /// Additional information will be included in the `data` field.
    Custom = 0xFF,
}

/// Represents a control message sent to a node.
///
/// This struct is used to send control commands like shutdown, initialization, or custom commands.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreLinkControlMessage {
    /// The control code specifying the command.
    pub code: CoreLinkControlCode,

    /// Additional data for the control command.
    pub data: serde_json::Value,
}

impl CoreLinkControlMessage {
    /// Constructs a blank `CoreLinkControlMessage` with `NoOp` code and no data.
    pub fn new() -> Self {
        Self {
            code: CoreLinkControlCode::NoOp,
            data: serde_json::Value::Null,
        }
    }

    /// Constructs a `CoreLinkControlMessage` with the specified control code but no data.
    ///
    /// ### Parameters
    /// - `code`: The control code to use.
    pub fn new_with_code(code: CoreLinkControlCode) -> Self {
        Self {
            code,
            data: serde_json::Value::Null,
        }
    }
}

/// Represents a CoreLink message.
///
/// This struct encapsulates all the data required for a message in the CoreLink protocol.
/// It includes fields for the transaction ID, timecode, node ID, message type, CRC32 checksum, and the message data.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreLinkMessage {
    /// Transaction ID. Should be non-zero. This ID is used to match a command to its
    /// arguments or status response. A transaction ID should not be re-used sequentially.
    pub transaction_id: u32,

    /// A timecode identifying the time this message was sent.
    /// The time is in microseconds and should be synchronized with the broker.
    /// This is a signed value to detect roll-over.
    pub timecode: i64,

    /// String representing the node ID of the target object.
    pub node_id: String,

    /// Identifies the type of this message.
    pub message_type: CoreLinkMessageType,

    /// Cyclical Redundancy Checksum. 32-bit. This field is used to verify the integrity
    /// of the message. It should not be set manually; use the `calculate_crc32` function instead.
    pub crc: u32,

    /// JSON value representing the data in the message. This field can be `NULL`.
    ///
    /// ### Remarks
    /// While JSON is convenient and flexible, it may become a performance bottleneck.
    /// Consider using more efficient serialization formats like `bincode`, `protobuf`, or `MessagePack`
    /// for high-frequency messaging.
    pub data: serde_json::Value,
}

impl CoreLinkMessage {
    /// Creates a new `CoreLinkMessage` with default values.
    ///
    /// The default values are:
    /// - `transaction_id`: 0
    /// - `timecode`: 0
    /// - `node_id`: Empty string
    /// - `message_type`: `CoreLinkMessageType::NoOp`
    /// - `crc`: 0
    /// - `data`: `serde_json::Value::Null`
    pub fn new() -> Self {
        Self {
            transaction_id: 0,
            timecode: 0,
            node_id: String::new(),
            message_type: CoreLinkMessageType::NoOp,
            crc: 0,
            data: serde_json::Value::Null,
        }
    }

    /// Creates a new `CoreLinkMessage` with a control code.
    ///
    /// ### Parameters
    /// - `code`: The control code to include in the message.
    pub fn new_control_msg(code: CoreLinkControlCode) -> Self {
        Self {
            transaction_id: 0,
            timecode: 0,
            node_id: String::new(),
            message_type: CoreLinkMessageType::Control,
            crc: 0,
            data: serde_json::json!(CoreLinkControlMessage::new_with_code(code)),
        }
    }

    /// Creates a success response message based on a request.
    ///
    /// ### Parameters
    /// - `req`: The request message to base the response on.
    ///
    /// ### Returns
    /// A new `CoreLinkMessage` indicating success.
    pub fn new_req_success_message(req: &CoreLinkMessage) -> Self {
        let data = serde_json::json!({
            "error": false,
            "error_message": "",
            "req": req.data.clone(),
        });

        Self {
            transaction_id: req.transaction_id,
            timecode: 0,
            node_id: req.node_id.clone(),
            message_type: req.message_type.clone(),
            crc: 0,
            data,
        }
    }

    /// Creates an error response message based on a request.
    ///
    /// ### Parameters
    /// - `req`: The request message to base the response on.
    /// - `error_message`: The error message to include in the response.
    ///
    /// ### Returns
    /// A new `CoreLinkMessage` indicating an error.
    pub fn new_req_error_message(req: &CoreLinkMessage, error_message: &str) -> Self {
        let data = serde_json::json!({
            "error": true,
            "error_message": error_message.to_string(),
            "req": req.data.clone(),
        });

        Self {
            transaction_id: req.transaction_id,
            timecode: 0,
            node_id: req.node_id.clone(),
            message_type: req.message_type.clone(),
            crc: 0,
            data,
        }
    }

    /// Creates a new message with a payload.
    ///
    /// ### Parameters
    /// - `req`: The request message to base the new message on.
    /// - `payload`: The payload to include in the new message.
    ///
    /// ### Returns
    /// A new `CoreLinkMessage` with the specified payload.
    pub fn new_with_payload(req: &CoreLinkMessage, payload: serde_json::Value) -> Self {
        Self {
            transaction_id: req.transaction_id,
            timecode: 0,
            node_id: req.node_id.clone(),
            message_type: req.message_type.clone(),
            crc: 0,
            data: payload,
        }
    }

    /// Converts this instance to a little-endian byte array.
    ///
    /// The byte array includes all fields of the message, serialized in little-endian format.
    /// The structure of the byte array is as follows:
    /// - Transaction ID (4 bytes)
    /// - Timecode (8 bytes)
    /// - Message type (2 bytes)
    /// - Node ID length (2 bytes) + Node ID (variable length)
    /// - CRC32 (4 bytes)
    /// - Data length (4 bytes) + Data (variable length)
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut ret = Vec::new();

        // Serialize transaction_id (4 bytes)
        ret.extend_from_slice(&self.transaction_id.to_le_bytes());

        // Serialize timecode (8 bytes)
        ret.extend_from_slice(&self.timecode.to_le_bytes());

        // Serialize message_type (2 bytes)
        ret.extend_from_slice(&(self.message_type as i16).to_le_bytes());

        // Serialize node_id length (2 bytes) and node_id (as bytes)
        let node_id_bytes = self.node_id.as_bytes();
        ret.extend_from_slice(&(node_id_bytes.len() as u16).to_le_bytes());
        ret.extend_from_slice(node_id_bytes);

        // Serialize crc (4 bytes)
        ret.extend_from_slice(&self.crc.to_le_bytes());

        // Serialize data length (4 bytes) and data (as JSON string bytes)
        let data_str = self.data.to_string();
        let data_bytes = data_str.as_bytes();
        ret.extend_from_slice(&(data_bytes.len() as u32).to_le_bytes());
        ret.extend_from_slice(data_bytes);

        ret
    }

    /// Calculates the CRC32 checksum for the `CoreLinkMessage` (excluding the `crc` field).
    ///
    /// This function serializes the message fields (excluding `crc`) and computes their CRC32 checksum.
    /// The result can be used to verify the integrity of the message.
    pub fn calculate_crc32(message: &CoreLinkMessage) -> u32 {
        let mut hasher = Hasher::new();

        // Serialize fields excluding `crc`
        hasher.update(&message.transaction_id.to_le_bytes());
        hasher.update(&message.timecode.to_le_bytes());
        hasher.update(&(message.message_type as i16).to_le_bytes());

        let node_id_bytes = message.node_id.as_bytes();
        hasher.update(&(node_id_bytes.len() as u16).to_le_bytes());
        hasher.update(node_id_bytes);

        let data_str = message.data.to_string();
        let data_bytes = data_str.as_bytes();
        hasher.update(&(data_bytes.len() as u32).to_le_bytes());
        hasher.update(data_bytes);

        // Finalize the CRC32 calculation
        hasher.finalize()
    }

    /// Calculates and stores the CRC32 checksum in this message.
    ///
    /// This function updates the `crc` field of the message with the computed CRC32 checksum.
    pub fn update_crc32(&mut self) {
        self.crc = CoreLinkMessage::calculate_crc32(self);
    }

    /// Returns `true` if this message is valid for processing.
    ///
    /// A message is considered valid if:
    /// - The `message_type` is not `NoOp`.
    /// - The `transaction_id` is non-zero.
    /// - The CRC32 checksum matches the computed value.
    ///
    /// ### Remarks
    /// Being invalid isn't necessarily an error. It could simply mean the message is a placeholder
    /// or does not require processing.
    pub fn is_valid(&self) -> bool {
        if self.message_type == CoreLinkMessageType::NoOp {
            log::debug!("Invalid message: message_type is NoOp");
            return false;
        }

        if self.transaction_id == 0 {
            log::debug!("Invalid message: transaction_id is 0");
            return false;
        }

        let crc_res = CoreLinkMessage::calculate_crc32(self);
        if crc_res != self.crc {
            log::debug!("CRC Check failed! calculated={} != expected={}", crc_res, self.crc);
            return false;
        } else {
            return true;
        }
    }

    /// Returns `true` if this message is an error response.
    ///
    /// ### Returns
    /// `true` if the message contains an error, otherwise `false`.
    pub fn is_error(&self) -> bool {
        if let Some(err) = self.data.get("error") {
            if let Some(b_err) = err.as_bool() {
                return b_err;
            }
        }

        false
    }

    /// Extracts an error message from an error response, if it exists.
    ///
    /// ### Returns
    /// The error message as a string slice, or a placeholder string if no error message is found.
    pub fn error_message(&self) -> &str {
        if self.is_error() {
            if let Some(err_msg) = self.data.get("error_message") {
                if let Some(sz_err_msg) = err_msg.as_str() {
                    return sz_err_msg;
                } else {
                    return "<COULD NOT CONVERT ERR MSG>";
                }
            } else {
                return "<DOES NOT CONTAIN ERROR MESSAGE>";
            }
        } else {
            return "<NO ERROR>";
        }
    }
}

/// Data payload for a subscription request.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreLinkSubscriptionPayload {
    /// Fully-qualified domain name identifying the data.
    #[serde(default)]
    pub fqdn: String,

    /// Options for the request. Available options may depend upon the data source.
    pub options: serde_json::Value,

    /// User data that will be reflected back to the client.
    pub user_data: serde_json::Value,
}

impl CoreLinkSubscriptionPayload {
    /// Constructs a new `CoreLinkSubscriptionPayload`.
    ///
    /// ### Parameters
    /// - `fqdn`: The fully-qualified domain name for the subscription.
    pub fn new(fqdn: &str) -> Self {
        Self {
            fqdn: fqdn.to_lowercase(),
            options: serde_json::Value::Null,
            user_data: serde_json::Value::Null,
        }
    }
}

/// Data payload for a topic and value combination.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreLinkTopicValuePayload {
    /// Fully-qualified domain name identifying the data.
    #[serde(default)]
    pub fqdn: String,

    /// Value to be written to the topic.
    pub value: serde_json::Value,

    /// User data that will be reflected back to the client.
    pub user_data: serde_json::Value,
}

impl CoreLinkTopicValuePayload {
    /// Constructs a new `CoreLinkTopicValuePayload`.
    ///
    /// ### Parameters
    /// - `fqdn`: The fully-qualified domain name for the topic.
    pub fn new(fqdn: &str) -> Self {
        Self {
            fqdn: fqdn.to_lowercase(),
            value: serde_json::Value::Null,
            user_data: serde_json::Value::Null,
        }
    }
}

/// Data payload for a request on a topic.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreLinkTopicRequestPayload {
    /// Fully-qualified domain name identifying the data.
    #[serde(default)]
    pub fqdn: String,

    /// Options related to the request.
    /// Available options are specific to the data source.
    pub options: serde_json::Value,

    /// User data that will be reflected back to the client.
    pub user_data: serde_json::Value,
}

impl CoreLinkTopicRequestPayload {
    /// Constructs a new `CoreLinkTopicRequestPayload`.
    ///
    /// ### Parameters
    /// - `fqdn`: The fully-qualified domain name for the request.
    pub fn new(fqdn: &str) -> Self {
        Self {
            fqdn: fqdn.to_lowercase(),
            options: serde_json::Value::Null,
            user_data: serde_json::Value::Null,
        }
    }
}

/// Data payload for a heartbeat message.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreLinkHeartbeatPayload {
    /// Timecode with microsecond resolution.
    #[serde(default)]
    pub timecode: i64,
}

impl CoreLinkHeartbeatPayload {
    /// Constructs a new `CoreLinkHeartbeatPayload`.
    ///
    /// ### Parameters
    /// - `timecode`: The timecode for the heartbeat.
    pub fn new(timecode: i64) -> Self {
        Self { timecode }
    }
}