// Copyright 2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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, version 3 of the License.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.

#[cfg(not(test))]
pub mod api;
#[cfg(test)]
pub mod mock;
#[cfg(test)]
mod tests;

#[cfg(test)]
pub use mock as api;

use api::{AccountId, IdtyIndex};

use codec::Encode;
use fnv::{FnvHashMap, FnvHashSet};
use log::{debug, error, info, warn};
use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator;
use std::io::Write;
use std::path::PathBuf;

// TODO select metadata file using features
#[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")]
pub mod runtime {}

pub enum RuntimeConfig {}
impl subxt::config::Config for RuntimeConfig {
    type AccountId = AccountId;
    type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>;
    type AssetId = ();
    type ExtrinsicParams = subxt::config::substrate::SubstrateExtrinsicParams<Self>;
    type Hash = sp_core::H256;
    type Hasher = subxt::config::substrate::BlakeTwo256;
    type Header =
        subxt::config::substrate::SubstrateHeader<u32, subxt::config::substrate::BlakeTwo256>;
    type Signature = subxt::ext::sp_runtime::MultiSignature;
}

#[derive(Copy, Clone, Debug, Default, Encode)]
pub struct Tip {
    #[codec(compact)]
    tip: u64,
}

impl Tip {
    pub fn new(amount: u64) -> Self {
        Tip { tip: amount }
    }
}

impl From<u64> for Tip {
    fn from(n: u64) -> Self {
        Self::new(n)
    }
}

pub struct Settings {
    pub evaluation_result_dir: PathBuf,
    pub rpc_url: String,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            evaluation_result_dir: PathBuf::from("/tmp/duniter/chains/gdev/distance"),
            rpc_url: String::from("ws://127.0.0.1:9944"),
        }
    }
}

pub async fn run_and_save(client: &api::Client, settings: Settings) {
    let Some((evaluation, current_pool_index, evaluation_result_path)) =
        run(client, &settings, true).await
    else {
        return;
    };

    debug!("Saving distance evaluation result to file `{evaluation_result_path:?}`");
    let mut evaluation_result_file = std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&evaluation_result_path)
        .unwrap_or_else(|e| {
            panic!(
                "Cannot open distance evaluation result file `{evaluation_result_path:?}`: {e:?}"
            )
        });
    evaluation_result_file
        .write_all(
            &sp_distance::ComputationResult {
                distances: evaluation,
            }
            .encode(),
        )
        .unwrap_or_else(|e| {
            panic!(
                "Cannot write distance evaluation result to file `{evaluation_result_path:?}`: {e:?}"
            )
        });

    // Remove old results
    let mut files_to_remove = Vec::new();
    for entry in settings
        .evaluation_result_dir
        .read_dir()
        .unwrap_or_else(|e| {
            panic!(
                "Cannot read distance evaluation result directory `{0:?}`: {e:?}",
                settings.evaluation_result_dir
            )
        })
        .flatten()
    {
        if let Ok(entry_name) = entry.file_name().into_string() {
            if let Ok(entry_pool) = entry_name.parse::<isize>() {
                if current_pool_index as isize - entry_pool > 3 {
                    files_to_remove.push(entry.path());
                }
            }
        }
    }
    files_to_remove.into_iter().for_each(|f| {
        std::fs::remove_file(&f)
            .unwrap_or_else(move |e| warn!("Cannot remove old result file `{f:?}`: {e:?}"));
    });
}

/// Returns `Option<(evaluation, current_pool_index, evaluation_result_path)>`
pub async fn run(
    client: &api::Client,
    settings: &Settings,
    handle_fs: bool,
) -> Option<(Vec<sp_runtime::Perbill>, u32, PathBuf)> {
    let parent_hash = api::parent_hash(client).await;

    let max_depth = api::max_referee_distance(client).await;

    let current_pool_index = api::current_pool_index(client, parent_hash).await;

    // Fetch the pending identities
    let Some(evaluation_pool) = api::current_pool(client, parent_hash, current_pool_index).await
    else {
        info!("Nothing to do: Pool does not exist");
        return None;
    };

    // Stop if nothing to evaluate
    if evaluation_pool.evaluations.0.is_empty() {
        info!("Nothing to do: Pool is empty");
        return None;
    }

    let evaluation_result_path = settings
        .evaluation_result_dir
        .join((current_pool_index + 1).to_string());

    if handle_fs {
        // Stop if already evaluated
        if evaluation_result_path
            .try_exists()
            .expect("Result path unavailable")
        {
            info!("Nothing to do: File already exists");
            return None;
        }

        std::fs::create_dir_all(&settings.evaluation_result_dir).unwrap_or_else(|e| {
            error!(
                "Cannot create distance evaluation result directory `{0:?}`: {e:?}",
                settings.evaluation_result_dir
            );
        });
    }

    info!("Evaluating distance for pool {}", current_pool_index);
    let evaluation_block = api::evaluation_block(client, parent_hash).await;

    // member idty -> issued certs
    let mut members = FnvHashMap::<IdtyIndex, u32>::default();

    let mut members_iter = api::member_iter(client, evaluation_block).await;
    while let Some(member_idty) = members_iter
        .next()
        .await
        .expect("Cannot fetch next members")
    {
        members.insert(member_idty, 0);
    }

    let min_certs_for_referee = (members.len() as f32).powf(1. / (max_depth as f32)).ceil() as u32;

    // idty -> received certs
    let mut received_certs = FnvHashMap::<IdtyIndex, Vec<IdtyIndex>>::default();

    let mut certs_iter = api::cert_iter(client, evaluation_block).await;
    while let Some((receiver, issuers)) = certs_iter
        .next()
        .await
        .expect("Cannot fetch next certification")
    {
        if (issuers.len() as u32) < min_certs_for_referee {
            // This member is not referee
            members.remove(&receiver);
        }
        for (issuer, _removable_on) in issuers.iter() {
            if let Some(issued_certs) = members.get_mut(issuer) {
                *issued_certs += 1;
            }
        }
        received_certs.insert(
            receiver,
            issuers
                .into_iter()
                .map(|(issuer, _removable_on)| issuer)
                .collect(),
        );
    }

    // Only retain referees
    members.retain(|_idty, issued_certs| *issued_certs >= min_certs_for_referee);
    let referees = members;

    let evaluation = evaluation_pool
        .evaluations
        .0
        .as_slice()
        .par_iter()
        .map(|(idty, _)| distance_rule(&received_certs, &referees, max_depth, *idty))
        .collect();

    Some((evaluation, current_pool_index, evaluation_result_path))
}

fn distance_rule_recursive(
    received_certs: &FnvHashMap<IdtyIndex, Vec<IdtyIndex>>,
    referees: &FnvHashMap<IdtyIndex, u32>,
    idty: IdtyIndex,
    accessible_referees: &mut FnvHashSet<IdtyIndex>,
    known_idties: &mut FnvHashMap<IdtyIndex, u32>,
    depth: u32,
) {
    // Do not re-explore identities that have already been explored at least as deeply
    match known_idties.entry(idty) {
        std::collections::hash_map::Entry::Occupied(mut entry) => {
            if *entry.get() >= depth {
                return;
            } else {
                *entry.get_mut() = depth;
            }
        }
        std::collections::hash_map::Entry::Vacant(entry) => {
            entry.insert(depth);
        }
    }

    // If referee, add it to the list
    if referees.contains_key(&idty) {
        accessible_referees.insert(idty);
    }

    // If reached the maximum distance, stop exploring
    if depth == 0 {
        return;
    }

    // Explore certifiers
    for &certifier in received_certs.get(&idty).unwrap_or(&vec![]).iter() {
        distance_rule_recursive(
            received_certs,
            referees,
            certifier,
            accessible_referees,
            known_idties,
            depth - 1,
        );
    }
}

/// Returns the fraction `nb_accessible_referees / nb_referees`
fn distance_rule(
    received_certs: &FnvHashMap<IdtyIndex, Vec<IdtyIndex>>,
    referees: &FnvHashMap<IdtyIndex, u32>,
    depth: u32,
    idty: IdtyIndex,
) -> sp_runtime::Perbill {
    debug!("Evaluating distance for idty {}", idty);
    let mut accessible_referees =
        FnvHashSet::<IdtyIndex>::with_capacity_and_hasher(referees.len(), Default::default());
    let mut known_idties =
        FnvHashMap::<IdtyIndex, u32>::with_capacity_and_hasher(referees.len(), Default::default());
    distance_rule_recursive(
        received_certs,
        referees,
        idty,
        &mut accessible_referees,
        &mut known_idties,
        depth,
    );
    let result = if referees.contains_key(&idty) {
        sp_runtime::Perbill::from_rational(
            accessible_referees.len() as u32 - 1,
            referees.len() as u32 - 1,
        )
    } else {
        sp_runtime::Perbill::from_rational(accessible_referees.len() as u32, referees.len() as u32)
    };
    info!(
        "Distance for idty {}: {}/{} = {}%",
        idty,
        accessible_referees.len(),
        referees.len(),
        result.deconstruct() as f32 / 1_000_000_000f32 * 100f32
    );
    result
}