/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2023 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::collections::{HashMap, HashSet};

use async_std::sync::{Arc, RwLock};
use chrono::Utc;
use darkfi_serial::serialize;
use futures::{select, FutureExt};
use log::{debug, error, warn};
use rand::Rng;
use smol::Executor;

use crate::{
    net,
    net::P2pPtr,
    util::async_util::sleep,
    Error::{NetworkNotConnected, UnknownKey},
    Result,
};

mod messages;
use messages::{KeyRequest, KeyResponse, LookupMapRequest, LookupMapResponse, LookupRequest};
mod protocol;
use protocol::Protocol;

// Constants configuration
const SEEN_DURATION: i64 = 120;

/// Atomic pointer to DHT state
pub type DhtPtr = Arc<RwLock<Dht>>;

// TODO: proper errors
// TODO: lookup table to be based on directly connected peers, not broadcast based
// Using string in structures because we are at an external crate
// and cant use blake3 serialization. To be replaced once merged with core src.

/// Struct representing DHT state.
pub struct Dht {
    /// Daemon id
    pub id: blake3::Hash,
    /// Daemon hasmap
    pub map: HashMap<blake3::Hash, Vec<u8>>,
    /// Network lookup map, containing nodes that holds each key
    pub lookup: HashMap<blake3::Hash, HashSet<blake3::Hash>>,
    /// P2P network pointer
    pub p2p: P2pPtr,
    /// Channel to receive responses from P2P
    p2p_recv_channel: smol::channel::Receiver<KeyResponse>,
    /// Stop signal channel to terminate background processes
    stop_signal: smol::channel::Receiver<()>,
    /// Daemon seen requests/responses ids and timestamp,
    /// to prevent rebroadcasting and loops
    pub seen: HashMap<blake3::Hash, i64>,
}

impl Dht {
    pub async fn new(
        initial: Option<HashMap<blake3::Hash, HashSet<blake3::Hash>>>,
        p2p_ptr: P2pPtr,
        stop_signal: smol::channel::Receiver<()>,
        ex: Arc<Executor<'_>>,
    ) -> Result<DhtPtr> {
        // Generate a random id
        let mut rng = rand::thread_rng();
        let n: u16 = rng.gen();
        let id = blake3::hash(&serialize(&n));
        let map = HashMap::default();
        let lookup = match initial {
            Some(l) => l,
            None => HashMap::default(),
        };
        let p2p = p2p_ptr.clone();
        let (p2p_send_channel, p2p_recv_channel) = smol::channel::unbounded::<KeyResponse>();
        let seen = HashMap::default();

        let dht = Arc::new(RwLock::new(Dht {
            id,
            map,
            lookup,
            p2p,
            p2p_recv_channel,
            stop_signal,
            seen,
        }));

        // Registering P2P protocols
        let registry = p2p_ptr.protocol_registry();
        let _dht = dht.clone();
        registry
            .register(net::SESSION_ALL, move |channel, p2p_ptr| {
                let sender = p2p_send_channel.clone();
                let dht = _dht.clone();
                async move { Protocol::init(channel, sender, dht, p2p_ptr).await.unwrap() }
            })
            .await;

        // Task to periodically clean up daemon seen messages
        ex.spawn(prune_seen_messages(dht.clone())).detach();

        Ok(dht)
    }

    /// Store provided key value pair, update lookup map and broadcast new insert to network
    pub async fn insert(
        &mut self,
        key: blake3::Hash,
        value: Vec<u8>,
    ) -> Result<Option<blake3::Hash>> {
        self.map.insert(key, value);

        if let Err(e) = self.lookup_insert(key, self.id) {
            error!(target: "dht", "Failed to insert record to lookup map: {}", e);
            return Err(e)
        };

        let request = LookupRequest::new(self.id, key, 0);
        if let Err(e) = self.p2p.broadcast(request).await {
            error!(target: "dht", "Failed broadcasting request: {}", e);
            return Err(e)
        }

        Ok(Some(key))
    }

    /// Remove provided key value pair and update lookup map
    pub async fn remove(&mut self, key: blake3::Hash) -> Result<Option<blake3::Hash>> {
        // Check if key value pair existed and act accordingly
        match self.map.remove(&key) {
            Some(_) => {
                debug!(target: "dht", "Key removed: {}", key);
                let request = LookupRequest::new(self.id, key, 1);
                if let Err(e) = self.p2p.broadcast(request).await {
                    error!(target: "dht", "Failed broadcasting request: {}", e);
                    return Err(e)
                }

                self.lookup_remove(key, self.id)
            }
            None => Ok(None),
        }
    }

    /// Store provided key node pair in lookup map and update network
    pub fn lookup_insert(
        &mut self,
        key: blake3::Hash,
        node_id: blake3::Hash,
    ) -> Result<Option<blake3::Hash>> {
        let mut lookup_set = match self.lookup.get(&key) {
            Some(s) => s.clone(),
            None => HashSet::new(),
        };

        lookup_set.insert(node_id);
        self.lookup.insert(key, lookup_set);

        Ok(Some(key))
    }

    /// Remove provided node id from keys set in local lookup map
    pub fn lookup_remove(
        &mut self,
        key: blake3::Hash,
        node_id: blake3::Hash,
    ) -> Result<Option<blake3::Hash>> {
        if let Some(s) = self.lookup.get(&key) {
            let mut lookup_set = s.clone();
            lookup_set.remove(&node_id);
            if lookup_set.is_empty() {
                self.lookup.remove(&key);
            } else {
                self.lookup.insert(key, lookup_set);
            }
        }

        Ok(Some(key))
    }

    /// Verify if provided key exists and return flag if local or in network
    pub fn contains_key(&self, key: blake3::Hash) -> Option<bool> {
        match self.lookup.contains_key(&key) {
            true => Some(self.map.contains_key(&key)),
            false => None,
        }
    }

    /// Get key from local map, acting as daemon cache
    pub fn get(&self, key: blake3::Hash) -> Option<&Vec<u8>> {
        self.map.get(&key)
    }

    /// Generate key request and broadcast it to the network
    pub async fn request_key(&self, key: blake3::Hash) -> Result<()> {
        // Verify the key exist in the lookup map.
        let peers = match self.lookup.get(&key) {
            Some(v) => v.clone(),
            None => return Err(UnknownKey),
        };

        debug!(target: "dht", "Key is in peers: {:?}", peers);

        // We retrieve p2p network connected channels, to verify if we
        // are connected to a network.
        // Using len here because is_empty() uses unstable library feature
        // called 'exact_size_is_empty'.
        if self.p2p.channels().lock().await.values().len() == 0 {
            return Err(NetworkNotConnected)
        }

        // We create a key request, and broadcast it to the network
        // We choose last known peer as request recipient
        let peer = *peers.iter().last().unwrap();
        let request = KeyRequest::new(self.id, peer, key);
        // TODO: ask connected peers directly, not broadcast
        if let Err(e) = self.p2p.broadcast(request).await {
            error!(target: "dht", "Failed broadcasting request: {}", e);
            return Err(e)
        }

        Ok(())
    }

    /// Auxilary function to sync lookup map with network
    pub async fn sync_lookup_map(&mut self) -> Result<()> {
        debug!(target: "dht", "Starting lookup map sync...");
        let channels_map = self.p2p.channels().lock().await.clone();
        let values = channels_map.values();
        // Using len here because is_empty() uses unstable library feature
        // called 'exact_size_is_empty'.
        if values.len() != 0 {
            // Node iterates the channel peers to ask for their lookup map
            for channel in values {
                // Communication setup
                let msg_subsystem = channel.get_message_subsystem();
                msg_subsystem.add_dispatch::<LookupMapResponse>().await;
                let response_sub = channel.subscribe_msg::<LookupMapResponse>().await?;

                // Node creates a `LookupMapRequest` and sends it
                let order = LookupMapRequest::new(self.id);
                channel.send(order).await?;

                // Node stores response data.
                let resp = response_sub.receive().await?;
                if resp.lookup.is_empty() {
                    warn!(target: "dht", "Retrieved empty lookup map from an unsynced node, retrying...");
                    continue
                }

                // Store retrieved records
                debug!(target: "dht", "Processing received records");
                for (k, v) in &resp.lookup {
                    for node in v {
                        self.lookup_insert(*k, *node)?;
                    }
                }

                break
            }
        } else {
            warn!(target: "dht", "Node is not connected to other nodes");
        }

        debug!(target: "dht", "Lookup map synced!");
        Ok(())
    }
}

// Auxilary function to wait for a key response from the P2P network.
pub async fn waiting_for_response(dht: DhtPtr) -> Result<Option<KeyResponse>> {
    let (p2p_recv_channel, stop_signal, timeout) = {
        let _dht = dht.read().await;
        (
            _dht.p2p_recv_channel.clone(),
            _dht.stop_signal.clone(),
            _dht.p2p.settings().connect_timeout_seconds as u64,
        )
    };
    let ex = Arc::new(Executor::new());
    let (timeout_s, timeout_r) = smol::channel::unbounded::<()>();
    ex.spawn(async move {
        sleep(timeout).await;
        timeout_s.send(()).await.unwrap_or(());
    })
    .detach();

    select! {
        msg = p2p_recv_channel.recv().fuse() => {
                let response = msg?;
                return Ok(Some(response))
        },
        _ = stop_signal.recv().fuse() => {},
        _ = timeout_r.recv().fuse() => {},
    }
    Ok(None)
}

// Auxilary function to periodically prun seen messages, based on when they were received.
// This helps us to prevent broadcasting loops.
async fn prune_seen_messages(dht: DhtPtr) {
    loop {
        sleep(SEEN_DURATION as u64).await;
        debug!(target: "dht", "Pruning seen messages");

        let now = Utc::now().timestamp();

        let mut prune = vec![];
        let map = dht.read().await.seen.clone();
        for (k, v) in map.iter() {
            if now - v > SEEN_DURATION {
                prune.push(k);
            }
        }

        let mut map = map.clone();
        for i in prune {
            map.remove(i);
        }

        dht.write().await.seen = map;
    }
}
