/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2025 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::time::UNIX_EPOCH;

use darkfi::{
    event_graph::Event,
    zk::{
        halo2::{Field, Value},
        Proof, ProvingKey, Witness, ZkCircuit,
    },
    zkas::ZkBinary,
    Result,
};
use darkfi_sdk::{
    bridgetree::Position,
    crypto::{pasta_prelude::FromUniformBytes, poseidon_hash, MerkleTree},
    pasta::pallas,
};
use log::info;
use rand::{rngs::OsRng, CryptoRng, RngCore};

pub const RLN_APP_IDENTIFIER: pallas::Base = pallas::Base::from_raw([4242, 0, 0, 0]);
pub const RLN_TRAPDOOR_DERIVATION_PATH: pallas::Base = pallas::Base::from_raw([4211, 0, 0, 0]);
pub const RLN_NULLIFIER_DERIVATION_PATH: pallas::Base = pallas::Base::from_raw([4212, 0, 0, 0]);

/// RLN epoch genesis
pub const RLN_GENESIS: u64 = 1738688400;
/// RLN epoch length in seconds
pub const RLN_EPOCH_LEN: u64 = 600; // 10 min

pub const RLN2_SIGNAL_ZKBIN: &[u8] = include_bytes!("../../proof/rlnv2-diff-signal.zk.bin");
pub const RLN2_SLASH_ZKBIN: &[u8] = include_bytes!("../../proof/rlnv2-diff-slash.zk.bin");

/// Find closest epoch to given timestamp
pub fn closest_epoch(timestamp: u64) -> u64 {
    let time_diff = timestamp - RLN_GENESIS;
    let epoch_idx = time_diff as f64 / RLN_EPOCH_LEN as f64;
    let rounded = epoch_idx.round() as i64;
    RLN_GENESIS + (rounded * RLN_EPOCH_LEN as i64) as u64
}

/// Hash message/event modulo `Fp`
pub fn hash_event(event: &Event) -> pallas::Base {
    let mut buf = [0u8; 64];
    buf[..blake3::OUT_LEN].copy_from_slice(event.id().as_bytes());
    pallas::Base::from_uniform_bytes(&buf)
}

#[derive(Copy, Clone)]
pub struct RlnIdentity {
    pub nullifier: pallas::Base,
    pub trapdoor: pallas::Base,
    pub user_message_limit: u64,
    /// This should increment during a single epoch and reset on new epochs
    pub message_id: u64,
    /// Last known epoch
    pub last_epoch: u64,
}

impl RlnIdentity {
    pub fn new(mut rng: (impl CryptoRng + RngCore)) -> Self {
        Self {
            nullifier: poseidon_hash([
                RLN_NULLIFIER_DERIVATION_PATH,
                pallas::Base::random(&mut rng),
            ]),
            trapdoor: poseidon_hash([RLN_TRAPDOOR_DERIVATION_PATH, pallas::Base::random(&mut rng)]),
            user_message_limit: 100,
            message_id: 1,
            last_epoch: closest_epoch(UNIX_EPOCH.elapsed().unwrap().as_secs()),
        }
    }

    pub fn commitment(&self) -> pallas::Base {
        poseidon_hash([
            poseidon_hash([self.nullifier, self.trapdoor]),
            pallas::Base::from(self.user_message_limit),
        ])
    }

    pub fn create_signal_proof(
        &self,
        event: &Event,
        identity_tree: &MerkleTree,
        identity_pos: Position,
        proving_key: &ProvingKey,
    ) -> Result<(Proof, Vec<pallas::Base>)> {
        // 1. Construct share
        let epoch = pallas::Base::from(closest_epoch(event.timestamp));
        let message_id = pallas::Base::from(self.message_id);
        let external_nullifier = poseidon_hash([epoch, RLN_APP_IDENTIFIER]);
        let a_0 = poseidon_hash([self.nullifier, self.trapdoor]);
        let a_1 = poseidon_hash([a_0, external_nullifier, message_id]);
        let x = hash_event(event);
        let y = a_0 + x * a_1;

        let internal_nullifier = poseidon_hash([a_1]);

        // 2. Create Merkle proof
        let identity_root = identity_tree.root(0).unwrap();
        let identity_path = identity_tree.witness(identity_pos, 0).unwrap();

        // 3. Create ZK proof
        let witnesses = vec![
            Witness::Base(Value::known(self.nullifier)),
            Witness::Base(Value::known(self.trapdoor)),
            Witness::MerklePath(Value::known(identity_path.clone().try_into().unwrap())),
            Witness::Uint32(Value::known(u64::from(identity_pos).try_into().unwrap())),
            Witness::Base(Value::known(x)),
            Witness::Base(Value::known(external_nullifier)),
            Witness::Base(Value::known(message_id)),
            Witness::Base(Value::known(pallas::Base::from(self.user_message_limit))),
            Witness::Base(Value::known(epoch)),
        ];

        let public_inputs =
            vec![epoch, external_nullifier, x, y, internal_nullifier, identity_root.inner()];

        info!(target: "crypto::rln::create_proof", "[RLN] Creating proof for event {}", event.id());
        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
        let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);

        let proof = Proof::create(proving_key, &[signal_circuit], &public_inputs, &mut OsRng)?;
        Ok((proof, vec![y, internal_nullifier]))
    }
}

/// Recover a secret from given secret shares
#[allow(dead_code)]
pub fn sss_recover(shares: &[(pallas::Base, pallas::Base)]) -> pallas::Base {
    let mut secret = pallas::Base::zero();
    for (j, share_j) in shares.iter().enumerate() {
        let mut prod = pallas::Base::one();
        for (i, share_i) in shares.iter().enumerate() {
            if i != j {
                prod *= share_i.0 * (share_i.0 - share_j.0).invert().unwrap();
            }
        }

        prod *= share_j.1;
        secret += prod;
    }

    secret
}
