// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0

use nym_credentials::ecash::utils::ecash_today;
use nym_credentials_interface::AvailableBandwidth;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::RwLock;

const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_secs(5 * 60); // 5 minutes
const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 5 * 1024 * 1024; // 5MB

#[derive(Debug, Clone, Copy)]
pub struct BandwidthFlushingBehaviourConfig {
    /// Defines maximum delay between client bandwidth information being flushed to the persistent storage.
    pub client_bandwidth_max_flushing_rate: Duration,

    /// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage.
    pub client_bandwidth_max_delta_flushing_amount: i64,
}

impl Default for BandwidthFlushingBehaviourConfig {
    fn default() -> Self {
        Self {
            client_bandwidth_max_flushing_rate: DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE,
            client_bandwidth_max_delta_flushing_amount:
                DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT,
        }
    }
}

#[derive(Debug, Clone)]
pub struct ClientBandwidth {
    inner: Arc<RwLock<ClientBandwidthInner>>,
}

#[derive(Debug)]
struct ClientBandwidthInner {
    pub(crate) bandwidth: AvailableBandwidth,
    pub(crate) last_synced: OffsetDateTime,

    /// the number of bytes the client had during the last sync.
    /// it is used to determine whether the current value should be synced with the storage
    /// by checking the delta with the known amount
    pub(crate) bytes_at_last_sync: i64,
    pub(crate) bytes_delta_since_sync: i64,
}

impl ClientBandwidth {
    pub fn new(bandwidth: AvailableBandwidth) -> ClientBandwidth {
        ClientBandwidth {
            inner: Arc::new(RwLock::new(ClientBandwidthInner {
                bandwidth,
                last_synced: OffsetDateTime::now_utc(),
                bytes_at_last_sync: bandwidth.bytes,
                bytes_delta_since_sync: 0,
            })),
        }
    }

    pub(crate) async fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool {
        let guard = self.inner.read().await;

        if guard.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount {
            return true;
        }

        if guard.last_synced + cfg.client_bandwidth_max_flushing_rate < OffsetDateTime::now_utc() {
            return true;
        }

        false
    }

    pub async fn available(&self) -> i64 {
        self.inner.read().await.bandwidth.bytes
    }

    pub(crate) async fn delta_since_sync(&self) -> i64 {
        self.inner.read().await.bytes_delta_since_sync
    }
    pub(crate) async fn expiration(&self) -> OffsetDateTime {
        self.inner.read().await.bandwidth.expiration
    }

    pub(crate) async fn expired(&self) -> bool {
        self.expiration().await < ecash_today()
    }

    pub(crate) async fn decrease_bandwidth(&self, decrease: i64) {
        let mut guard = self.inner.write().await;

        guard.bandwidth.bytes -= decrease;
        guard.bytes_delta_since_sync -= decrease;
    }

    pub(crate) async fn increase_bandwidth(&self, increase: i64, new_expiration: OffsetDateTime) {
        let mut guard = self.inner.write().await;

        guard.bandwidth.bytes += increase;
        guard.bandwidth.expiration = new_expiration;
        guard.bytes_delta_since_sync += increase;
    }

    pub(crate) async fn expire_bandwidth(&self) {
        let mut guard = self.inner.write().await;

        guard.bandwidth = AvailableBandwidth::default();
        guard.last_synced = OffsetDateTime::now_utc();
        guard.bytes_at_last_sync = 0;
        guard.bytes_delta_since_sync = 0;
    }

    pub(crate) async fn resync_bandwidth_with_storage(&self, stored: i64) {
        let mut guard = self.inner.write().await;

        guard.bandwidth.bytes = stored;
        guard.bytes_at_last_sync = stored;
        guard.bytes_delta_since_sync = 0;
        guard.last_synced = OffsetDateTime::now_utc();
    }
}
