//! RPC support for client tor-proto objects.

use std::{collections::HashMap, net::SocketAddr, sync::Arc};

use derive_deftly::Deftly;
use tor_linkspec::{HasAddrs, HasRelayIds};
use tor_llcrypto::pk;
use tor_rpcbase::{self as rpc, SingleIdResponse};

use crate::{
    ClientTunnel,
    client::stream::{ClientDataStreamCtrl, ClientStreamCtrl},
};

/// RPC method that returns the tunnel for a given object.
///
/// This is currently implemented for Data streams,
/// but could in the future be implemented for other types.
///
/// # In the Arti RPC System
///
/// Return a tunnel associated with a given object.
///
/// (A tunnel is a collection of one or more circuits
/// used to transmit data.)
///
/// Gives an error if the object is not associated with a tunnel,
/// or if the tunnel was closed.
///
/// The returned ObjectId is a handle to a `ClientTunnel`.
/// The caller should drop this ObjectId when they are done with the ClientTunnel.
#[derive(Debug, serde::Deserialize, Deftly)]
#[derive_deftly(rpc::DynMethod)]
#[deftly(rpc(method_name = "arti:get_tunnel"))]
#[non_exhaustive]
pub struct GetTunnel {}

impl rpc::RpcMethod for GetTunnel {
    type Output = rpc::SingleIdResponse;
    type Update = rpc::NoUpdates;
}

/// RPC method to describe the path for an object.
///
/// This is currently implemented for [`ClientTunnel`],
/// but could in the future be implemented for other types.
///
/// # In the Arti RPC System
///
/// Describe the path(s) of a tunnel through the Tor network.
///
/// (Because of [Conflux], a tunnel can have multiple paths.
/// This method describes the members of each one.)
///
/// If `include_deprecated_ids` is true,
/// the output includes deprecated node identity types,
/// including the RSA identity.
/// Otherwise, only non-deprecated identities are included.
///
/// [Conflux]: https://spec.torproject.org/proposals/329-traffic-splitting.html
#[derive(Debug, serde::Deserialize, Deftly)]
#[derive_deftly(rpc::DynMethod)]
#[deftly(rpc(method_name = "arti:describe_path"))]
#[non_exhaustive]
pub struct DescribePath {
    /// If true, the output will include deprecated node identity types.
    #[serde(default)]
    include_deprecated_ids: bool,
}

impl rpc::RpcMethod for DescribePath {
    type Output = PathDescription;
    type Update = rpc::NoUpdates;
}

/// A RPC-level description for a single entry in a tunnel or circuit path.
#[derive(serde::Serialize, Clone, Debug)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum PathEntry {
    /// A hop not corresponding to a known relay.
    ///
    /// Typically, this is the join point for a rendezvous circuit for
    /// communicating with or as an onion service.
    VirtualHop {},

    /// Return
    KnownRelay {
        /// A set of IDs for this Tor relay.
        ///
        /// Each ID represents a long-term public key used to identify the relay.
        /// Deprecated ID types are not included, unless they were specifically requested.
        ids: RelayIds,

        /// A list of the relay's addresses.
        addrs: Vec<SocketAddr>,
    },
}

/// Serializable container of relay identities.
///
/// Differs from [`tor_linkspec::RelayIds`] in is serialize behavior;
/// See <https://gitlab.torproject.org/tpo/core/arti/-/issues/2477>.
///
/// Every relay will have at least one ID.  On the current Tor network
/// as of April 2026, every relay has an Ed25519 ID.
/// (This may not always be the case in the future;
/// for example, if we migrate to ML-DSA keys,
/// we may eventually retire Ed25519 IDs.)
#[derive(Clone, Debug, serde::Serialize)]
pub struct RelayIds {
    /// Copy of the ed25519 id from the underlying ChanTarget.
    #[serde(rename = "ed25519", skip_serializing_if = "Option::is_none")]
    ed_identity: Option<pk::ed25519::Ed25519Identity>,

    /// Copy of the rsa id from the underlying ChanTarget.
    ///
    /// This is a deprecated ID type.
    #[serde(rename = "rsa", skip_serializing_if = "Option::is_none")]
    rsa_identity: Option<pk::rsa::RsaIdentity>,
}

/// A description of a tunnel's path.
///
/// Note that tunnels are potentially made of multiple circuits, even though
/// Arti (as of April 2026) does not yet build Conflux tunnels.
/// Therefore, users should make sure to handle multi-path tunnels here.
#[derive(serde::Serialize, Clone, Debug)]
pub struct PathDescription {
    /// The entries in a given tunnel's path(s).
    ///
    /// Within each path, entries are ordered from first (closest to the client)
    /// to last (farthest from the client).
    ///
    /// Since tunnels can have multiple paths, each path is identified with a string.
    /// The actual content of these strings is not documented,
    /// but the string identifying each tunnel is stable.
    ///
    /// (That is, if you query a tunnel's path description twice,
    /// each path will have the same identifying string both times.
    /// If you get a new identifying string,
    /// it represents a path that was previously not part of the tunnel.)
    path: HashMap<String, Vec<PathEntry>>,
}

impl PathEntry {
    /// Construct a PathEntry from a PathEntry returned by a circuit.
    fn from_client_entry(
        detail: &crate::client::circuit::PathEntry,
        command: &DescribePath,
    ) -> Self {
        let Some(owned_chan_target) = detail.as_chan_target() else {
            return PathEntry::VirtualHop {};
        };

        let ids = tor_linkspec::RelayIds::from_relay_ids(owned_chan_target);
        let ids = RelayIds {
            ed_identity: ids.ed_identity().cloned(),
            rsa_identity: if command.include_deprecated_ids {
                ids.rsa_identity().cloned()
            } else {
                None
            },
        };
        let addrs = owned_chan_target.addrs().collect();
        PathEntry::KnownRelay { ids, addrs }
    }
}

/// Helper: Return the [`ClientTunnel`] for a [`ClientDataStreamCtrl`],
/// or an RPC error if the stream isn't attached to a tunnel.
fn client_stream_tunnel(stream: &ClientDataStreamCtrl) -> Result<Arc<ClientTunnel>, rpc::RpcError> {
    stream.tunnel().ok_or_else(|| {
        rpc::RpcError::new(
            "Stream was not attached to a tunnel".to_string(),
            rpc::RpcErrorKind::RequestError,
        )
    })
}

/// Helper: Return a [`PathDescription`] for a [`ClientTunnel`]
fn tunnel_path(tunnel: &ClientTunnel, method: &DescribePath) -> PathDescription {
    let path = tunnel
        .tagged_paths()
        .into_iter()
        .map(|(id, path)| {
            let id = id.display_chan_circ().to_string();
            let path = path
                .iter()
                .map(|hop| PathEntry::from_client_entry(hop, method))
                .collect();
            (id, path)
        })
        .collect();
    PathDescription { path }
}

/// Implementation function: implements GetTunnel on ClientDataStreamCtrl.
async fn client_data_stream_ctrl_get_tunnel(
    target: Arc<ClientDataStreamCtrl>,
    _method: Box<GetTunnel>,
    ctx: Arc<dyn rpc::Context>,
) -> Result<rpc::SingleIdResponse, rpc::RpcError> {
    let tunnel: Arc<dyn rpc::Object> = client_stream_tunnel(&target)? as _;
    let id = ctx.register_owned(tunnel);
    Ok(SingleIdResponse::from(id))
}

/// Implementation function: implements DescribePath on a ClientTunnel.
async fn client_tunnel_describe_path(
    target: Arc<ClientTunnel>,
    method: Box<DescribePath>,
    _ctx: Arc<dyn rpc::Context>,
) -> Result<PathDescription, rpc::RpcError> {
    Ok(tunnel_path(&target, &method))
}

/// Implementation function: implements DescribePath on a ClientDataStreamCtrl.
async fn client_data_stream_ctrl_describe_path(
    target: Arc<ClientDataStreamCtrl>,
    method: Box<DescribePath>,
    _ctx: Arc<dyn rpc::Context>,
) -> Result<PathDescription, rpc::RpcError> {
    let tunnel = client_stream_tunnel(&target)?;
    Ok(tunnel_path(&tunnel, &method))
}

rpc::static_rpc_invoke_fn! {
    client_data_stream_ctrl_get_tunnel;
    client_tunnel_describe_path;
    client_data_stream_ctrl_describe_path;
}
