// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Transaction validity extraction and checking utilities.
//!
//! This module provides functions to extract validity timestamps from serialized
//! consensus transactions and check their validity status.
//!
//! # Validity Semantics by Transaction Type
//!
//! | Type | valid_from | valid_until |
//! |------|------------|-------------|
//! | VersionedTransaction | `valid_from` field | `valid_from + TRANSACTION_VALIDITY_WINDOW_MS` |
//! | RexUpdateResult | None (immediately valid) | `target_timestamp` (deadline) |
//! | AdminTransaction | None | None (never expires) |
//!
//! # Handling Unparseable Payloads
//!
//! When a payload cannot be deserialized, `extract_valid_from` and `extract_valid_until`
//! return `None`. This means unparseable payloads are treated as having no validity
//! constraints (always valid). **This is intentional:**
//!
//! - The mempool is not the correctness layer; execution validates transactions
//! - Rejecting unparseable payloads here would couple mempool to consensus message format
//! - Invalid payloads will be rejected during execution (wasting block space, but not
//!   compromising correctness)
//!
//! To prevent DoS via garbage payloads, the mempool has capacity limits and the execution
//! layer charges fees for invalid transactions.

use crate::{ConsensusMessage, TRANSACTION_VALIDITY_WINDOW_MS};

/// Maximum time in the future a transaction's valid_from can be before we reject it.
/// Transactions with valid_from more than 60 seconds in the future are considered invalid.
pub const MAX_FUTURE_VALID_FROM_MS: u64 = 60_000;

/// Extract both `valid_from` and `valid_until` timestamps from a serialized consensus transaction.
///
/// This is more efficient than calling `extract_valid_from` and `extract_valid_until` separately
/// as it only deserializes the payload once.
///
/// Returns `(valid_from, valid_until)` where:
/// - VersionedTransaction: `(Some(valid_from), Some(valid_from + TRANSACTION_VALIDITY_WINDOW_MS))`
/// - RexUpdateResult: `(None, Some(target_timestamp))`
/// - AdminTransaction: `(None, None)`
/// - Unparseable: `(None, None)`
pub fn extract_validity(payload: &[u8]) -> (Option<u64>, Option<u64>) {
    let Ok(msg) = bincode::deserialize::<ConsensusMessage>(payload) else {
        return (None, None);
    };

    match msg {
        ConsensusMessage::VersionedTransaction(vtx) => {
            let valid_from_i64 = vtx.message.valid_from();
            // Convert i64 to u64, returning (None, None) for negative values
            let Ok(valid_from) = u64::try_from(valid_from_i64) else {
                return (None, None);
            };
            // Use checked_add to handle overflow (treats overflow as unparseable).
            // Semantic "too far in future" validation happens at get_batch() time
            // via is_too_far_in_future(), which uses MAX_FUTURE_VALID_FROM_MS (60s).
            let Some(valid_until) = valid_from.checked_add(TRANSACTION_VALIDITY_WINDOW_MS) else {
                return (None, None);
            };
            (Some(valid_from), Some(valid_until))
        }
        ConsensusMessage::RexUpdateResult(otx) => {
            // RexUpdateResult: immediately valid, expires at target_timestamp
            (None, Some(otx.target_timestamp))
        }
        ConsensusMessage::AdminTransaction(_) => {
            // AdminTransaction: immediately valid, never expires
            (None, None)
        }
    }
}

/// Extract the `valid_from` timestamp from a serialized consensus transaction.
///
/// Returns `Some(timestamp)` for VersionedTransaction, `None` for other types.
///
/// Returns `None` if:
/// - The payload cannot be parsed (unparseable = no validity = always valid)
/// - `valid_from` is negative
/// - `valid_from + TRANSACTION_VALIDITY_WINDOW_MS` would overflow
///
/// Note: If you need both `valid_from` and `valid_until`, use `extract_validity()` instead
/// to avoid deserializing twice.
pub fn extract_valid_from(payload: &[u8]) -> Option<u64> {
    extract_validity(payload).0
}

/// Extract the `valid_until` timestamp from a serialized consensus transaction.
///
/// - VersionedTransaction: `valid_from + TRANSACTION_VALIDITY_WINDOW_MS`
/// - RexUpdateResult: `target_timestamp`
/// - AdminTransaction: `None` (never expires)
///
/// Returns `None` if:
/// - The payload cannot be parsed (unparseable = no validity = never expires)
/// - `valid_from` is negative or would cause overflow
/// - The transaction type has no expiry (AdminTransaction)
///
/// Note: If you need both `valid_from` and `valid_until`, use `extract_validity()` instead
/// to avoid deserializing twice.
pub fn extract_valid_until(payload: &[u8]) -> Option<u64> {
    extract_validity(payload).1
}

/// Check if a transaction has expired.
///
/// A transaction is expired if `now > valid_until`.
#[inline]
pub fn is_expired(valid_until: u64, now: u64) -> bool {
    now > valid_until
}

/// Check if a transaction is not yet valid.
///
/// A transaction is not yet valid if `now < valid_from`.
#[inline]
pub fn is_not_yet_valid(valid_from: u64, now: u64) -> bool {
    now < valid_from
}

/// Check if a transaction's valid_from is too far in the future.
///
/// A transaction is too far in the future if `valid_from > now + MAX_FUTURE_VALID_FROM_MS`.
#[inline]
pub fn is_too_far_in_future(valid_from: u64, now: u64) -> bool {
    valid_from > now.saturating_add(MAX_FUTURE_VALID_FROM_MS)
}

// ============================================================================
// Test Helpers (available with testing feature)
// ============================================================================

/// Create a serialized VersionedTransaction with a specific valid_from timestamp.
///
/// This helper is useful for testing validity extraction and filtering logic.
#[cfg(any(test, feature = "testing"))]
pub fn create_versioned_transaction_with_timestamp(valid_from: i64) -> Vec<u8> {
    use rialo_s_message::{legacy::Message as LegacyMessage, ConfigHashPrefix, VersionedMessage};
    use rialo_s_transaction::versioned::VersionedTransaction;

    let message = LegacyMessage {
        header: rialo_s_message::MessageHeader {
            num_required_signatures: 1,
            num_readonly_signed_accounts: 0,
            num_readonly_unsigned_accounts: 0,
        },
        account_keys: vec![rialo_s_pubkey::Pubkey::new_unique()],
        valid_from,
        config_hash_prefix: ConfigHashPrefix::new(0),
        occ: false,
        instructions: vec![],
    };

    let vtx = VersionedTransaction {
        signatures: vec![],
        message: VersionedMessage::Legacy(message),
    };

    let consensus_msg = ConsensusMessage::VersionedTransaction(vtx);
    bincode::serialize(&consensus_msg).expect("serialization should succeed")
}

/// Create a serialized RexUpdateResult with a specific target_timestamp.
///
/// This helper is useful for testing validity extraction and filtering logic.
#[cfg(any(test, feature = "testing"))]
pub fn create_rex_update_with_target_timestamp(target_timestamp: u64) -> Vec<u8> {
    use crate::RexUpdateResult;

    let rex_result = RexUpdateResult {
        target_timestamp,
        ..Default::default()
    };

    let consensus_msg = ConsensusMessage::RexUpdateResult(Box::new(rex_result));
    bincode::serialize(&consensus_msg).expect("serialization should succeed")
}

/// Create a serialized AdminTransaction (epoch change).
///
/// This helper is useful for testing validity extraction and filtering logic.
/// Admin transactions have no validity window (never expire).
#[cfg(any(test, feature = "testing"))]
pub fn create_admin_transaction() -> Vec<u8> {
    use crate::{AdminTransaction, EpochChangeConfig, EpochIdentifier};

    let config = EpochChangeConfig {
        current_epoch: EpochIdentifier::new(0),
        new_epoch: EpochIdentifier::new(1),
        validators: vec![],
        consensus_config: None,
    };

    let admin_tx = AdminTransaction::EpochChange(Box::new(config));
    let consensus_msg = ConsensusMessage::AdminTransaction(Box::new(admin_tx));
    bincode::serialize(&consensus_msg).expect("serialization should succeed")
}

// ============================================================================
// Tests
// ============================================================================

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

    // ------------------------------------------------------------------------
    // extract_validity tests (combined extraction)
    // ------------------------------------------------------------------------

    #[test]
    fn test_extract_validity_versioned_transaction() {
        let valid_from: i64 = 1_000_000_000;
        let payload = create_versioned_transaction_with_timestamp(valid_from);

        let (from, until) = extract_validity(&payload);

        assert_eq!(from, Some(valid_from as u64));
        assert_eq!(
            until,
            Some(valid_from as u64 + TRANSACTION_VALIDITY_WINDOW_MS)
        );
    }

    #[test]
    fn test_extract_validity_rex_update() {
        let target_timestamp: u64 = 1_000_000;
        let payload = create_rex_update_with_target_timestamp(target_timestamp);

        let (from, until) = extract_validity(&payload);

        // RexUpdateResult: immediately valid (None), expires at target_timestamp
        assert_eq!(from, None);
        assert_eq!(until, Some(target_timestamp));
    }

    #[test]
    fn test_extract_validity_admin_transaction() {
        let payload = create_admin_transaction();

        let (from, until) = extract_validity(&payload);

        // AdminTransaction: immediately valid, never expires
        assert_eq!(from, None);
        assert_eq!(until, None);
    }

    #[test]
    fn test_extract_validity_invalid_payload() {
        let payload = b"invalid payload";

        let (from, until) = extract_validity(payload);

        // Unparseable: treated as immediately valid, never expires
        assert_eq!(from, None);
        assert_eq!(until, None);
    }

    // ------------------------------------------------------------------------
    // extract_valid_from tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_extract_valid_from_versioned_transaction() {
        let valid_from: i64 = 1_000_000_000;
        let payload = create_versioned_transaction_with_timestamp(valid_from);

        let result = extract_valid_from(&payload);

        assert_eq!(result, Some(valid_from as u64));
    }

    #[test]
    fn test_extract_valid_from_versioned_transaction_negative() {
        // Negative valid_from should be handled (converted or rejected)
        let valid_from: i64 = -1000;
        let payload = create_versioned_transaction_with_timestamp(valid_from);

        let result = extract_valid_from(&payload);

        // Negative values should return None (invalid)
        assert_eq!(result, None);
    }

    #[test]
    fn test_extract_valid_from_rex_update() {
        let payload = create_rex_update_with_target_timestamp(1_000_000);

        let result = extract_valid_from(&payload);

        // RexUpdateResult has no valid_from (immediately valid)
        assert_eq!(result, None);
    }

    #[test]
    fn test_extract_valid_from_admin_transaction() {
        let payload = create_admin_transaction();

        let result = extract_valid_from(&payload);

        // AdminTransaction has no valid_from
        assert_eq!(result, None);
    }

    #[test]
    fn test_extract_valid_from_invalid_payload() {
        let payload = b"invalid payload";

        let result = extract_valid_from(payload);

        assert_eq!(result, None);
    }

    #[test]
    fn test_extract_valid_from_accepts_far_future_timestamp() {
        // Large timestamps are accepted at extraction time.
        // Semantic "too far in future" validation happens at get_batch() time.
        let year_2200: i64 = 7_258_118_400_000; // ~230 years from epoch
        let payload = create_versioned_transaction_with_timestamp(year_2200);

        let result = extract_valid_from(&payload);

        // Should succeed - extraction accepts any valid timestamp
        // (is_too_far_in_future() check happens later at get_batch time)
        assert_eq!(result, Some(year_2200 as u64));
    }

    // ------------------------------------------------------------------------
    // extract_valid_until tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_extract_valid_until_versioned_transaction() {
        let valid_from: i64 = 1_000_000_000;
        let payload = create_versioned_transaction_with_timestamp(valid_from);

        let result = extract_valid_until(&payload);

        // valid_until = valid_from + TRANSACTION_VALIDITY_WINDOW_MS
        let expected = (valid_from as u64) + TRANSACTION_VALIDITY_WINDOW_MS;
        assert_eq!(result, Some(expected));
    }

    #[test]
    fn test_extract_valid_until_rex_update() {
        let target_timestamp: u64 = 1_000_000;
        let payload = create_rex_update_with_target_timestamp(target_timestamp);

        let result = extract_valid_until(&payload);

        // RexUpdateResult: valid_until = target_timestamp
        assert_eq!(result, Some(target_timestamp));
    }

    #[test]
    fn test_extract_valid_until_admin_transaction() {
        let payload = create_admin_transaction();

        let result = extract_valid_until(&payload);

        // AdminTransaction: no expiry
        assert_eq!(result, None);
    }

    #[test]
    fn test_extract_valid_until_invalid_payload() {
        let payload = b"invalid payload";

        let result = extract_valid_until(payload);

        assert_eq!(result, None);
    }

    // ------------------------------------------------------------------------
    // is_expired tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_is_expired_past_deadline() {
        let valid_until = 1000;
        let now = 1001;

        assert!(is_expired(valid_until, now));
    }

    #[test]
    fn test_is_expired_at_deadline() {
        let valid_until = 1000;
        let now = 1000;

        // Exactly at deadline is NOT expired (now <= valid_until)
        assert!(!is_expired(valid_until, now));
    }

    #[test]
    fn test_is_expired_before_deadline() {
        let valid_until = 1000;
        let now = 999;

        assert!(!is_expired(valid_until, now));
    }

    // ------------------------------------------------------------------------
    // is_not_yet_valid tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_is_not_yet_valid_before_valid_from() {
        let valid_from = 1000;
        let now = 999;

        assert!(is_not_yet_valid(valid_from, now));
    }

    #[test]
    fn test_is_not_yet_valid_at_valid_from() {
        let valid_from = 1000;
        let now = 1000;

        // Exactly at valid_from is valid (now >= valid_from)
        assert!(!is_not_yet_valid(valid_from, now));
    }

    #[test]
    fn test_is_not_yet_valid_after_valid_from() {
        let valid_from = 1000;
        let now = 1001;

        assert!(!is_not_yet_valid(valid_from, now));
    }

    // ------------------------------------------------------------------------
    // is_too_far_in_future tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_is_too_far_in_future_within_limit() {
        let now = 1_000_000;
        let valid_from = now + MAX_FUTURE_VALID_FROM_MS; // exactly at limit

        assert!(!is_too_far_in_future(valid_from, now));
    }

    #[test]
    fn test_is_too_far_in_future_past_limit() {
        let now = 1_000_000;
        let valid_from = now + MAX_FUTURE_VALID_FROM_MS + 1; // 1ms past limit

        assert!(is_too_far_in_future(valid_from, now));
    }

    #[test]
    fn test_is_too_far_in_future_well_within_limit() {
        let now = 1_000_000;
        let valid_from = now + 1000; // 1 second in future

        assert!(!is_too_far_in_future(valid_from, now));
    }

    #[test]
    fn test_is_too_far_in_future_in_past() {
        let now = 1_000_000;
        let valid_from = now - 1000; // in the past

        assert!(!is_too_far_in_future(valid_from, now));
    }
}
