// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Sender-side request logic. Some things from this module are also used on the receiver side, eg
//! [`extrinsic_delay`], but most of the receiver-side request logic lives elsewhere.

use super::{config::SubstrateConfig, error::Error};
use blake2::{
	digest::{consts::U16, Mac},
	Blake2bMac,
};
use codec::{Decode, DecodeAll};
use futures::channel::oneshot;
use log::debug;
use mixnet::core::{Delay, MessageId, PostErr, Scattered};
use sp_core::Bytes;
use std::time::Duration;

const LOG_TARGET: &str = "mixnet";

fn send_err<T>(reply_sender: oneshot::Sender<Result<T, Error>>, err: Error) {
	if let Err(Err(err)) = reply_sender.send(Err(err)) {
		debug!(target: LOG_TARGET, "Failed to inform requester of error: {err}");
	}
}

fn send_reply<T: Decode>(reply_sender: oneshot::Sender<Result<T, Error>>, mut data: &[u8]) {
	let res = match Result::decode_all(&mut data) {
		Ok(res) => res.map_err(Error::Remote),
		Err(_) => Err(Error::BadReply),
	};
	match reply_sender.send(res) {
		Ok(_) => (),
		Err(Ok(_)) => debug!(target: LOG_TARGET, "Failed to send reply to requester"),
		Err(Err(err)) => debug!(target: LOG_TARGET, "Failed to inform requester of error: {err}"),
	}
}

/// First byte of a submit extrinsic request, identifying it as such.
pub const SUBMIT_EXTRINSIC: u8 = 1;

const EXTRINSIC_DELAY_PERSONA: &[u8; 16] = b"submit-extrn-dly";

/// Returns the artificial delay that should be inserted between receipt of a submit extrinsic
/// request with the given message ID and import of the extrinsic into the local transaction pool.
pub fn extrinsic_delay(message_id: &MessageId, config: &SubstrateConfig) -> Duration {
	let h = Blake2bMac::<U16>::new_with_salt_and_personal(message_id, b"", EXTRINSIC_DELAY_PERSONA)
		.expect("Key, salt, and persona sizes are fixed and small enough");
	let delay = Delay::exp(h.finalize().into_bytes().as_ref());
	delay.to_duration(config.mean_extrinsic_delay)
}

/// Request parameters and local reply channel. Stored by the
/// [`RequestManager`](mixnet::request_manager::RequestManager).
pub enum Request {
	SubmitExtrinsic { extrinsic: Bytes, reply_sender: oneshot::Sender<Result<(), Error>> },
}

impl Request {
	/// Forward an error to the user of the mixnet service.
	fn send_err(self, err: Error) {
		match self {
			Request::SubmitExtrinsic { reply_sender, .. } => send_err(reply_sender, err),
		}
	}

	/// Forward a reply to the user of the mixnet service.
	pub fn send_reply(self, data: &[u8]) {
		match self {
			Request::SubmitExtrinsic { reply_sender, .. } => send_reply(reply_sender, data),
		}
	}
}

impl mixnet::request_manager::Request for Request {
	type Context = SubstrateConfig;

	fn with_data<T>(&self, f: impl FnOnce(Scattered<u8>) -> T, _context: &Self::Context) -> T {
		match self {
			Request::SubmitExtrinsic { extrinsic, .. } =>
				f([&[SUBMIT_EXTRINSIC], extrinsic.as_ref()].as_slice().into()),
		}
	}

	fn num_surbs(&self, context: &Self::Context) -> usize {
		match self {
			Request::SubmitExtrinsic { .. } => context.surb_factor,
		}
	}

	fn handling_delay(&self, message_id: &MessageId, context: &Self::Context) -> Duration {
		match self {
			Request::SubmitExtrinsic { .. } => extrinsic_delay(message_id, context),
		}
	}

	fn handle_post_err(self, err: PostErr, _context: &Self::Context) {
		self.send_err(err.into());
	}

	fn handle_retry_limit_reached(self, _context: &Self::Context) {
		self.send_err(Error::NoReply);
	}
}
