//! Concrete Web3 client for ICP, COSE, HTTPS, and signing operations.
//!
//! The client implements the capability traits consumed by `anda_engine`
//! contexts, including canister calls, signed HTTP calls, and deterministic
//! key derivation.

use anda_core::{BoxError, BoxPinFut, HttpFeatures, RPCRequestRef, cbor_rpc};
use anda_engine::context::Web3ClientFeatures;
use candid::{
    CandidType, Decode, Principal,
    utils::{ArgumentEncoder, encode_args},
};
use cbor2::{from_slice, to_canonical_vec};
use ic_agent::identity::{AnonymousIdentity, BasicIdentity, Secp256k1Identity};
use ic_auth_types::ByteBufB64;
use ic_auth_verifier::envelope::SignedEnvelope;
use ic_cose::client::CoseSDK;
use ic_cose_types::{
    CanisterCaller,
    cose::{
        ed25519::ed25519_verify,
        k256::{secp256k1_verify_bip340, secp256k1_verify_ecdsa},
        sha3_256,
    },
};
use ic_tee_gateway_sdk::crypto;
use serde::{Serialize, de::DeserializeOwned};
use std::{sync::Arc, time::Duration};

pub use ic_agent::{Agent, Identity};

use anda_engine::APP_USER_AGENT;

/// Client for interacting with outside services (includes ICP and other blockchains)
///
/// Provides cryptographic operations, canister communication, and HTTP features.
/// Manages both internal and external HTTP clients
/// with different configurations for secure communication.
#[derive(Clone)]
pub struct Client {
    outer_http: reqwest::Client,
    root_secret: [u8; 48],
    identity: Arc<dyn Identity>,
    principal: Principal,
    agent: Agent,
    cose_canister: Principal,
    allow_http: bool,
}

/// Builder for creating a new Client with custom configuration
#[non_exhaustive]
pub struct ClientBuilder {
    ic_host: String,
    root_secret: [u8; 48],
    identity: Option<Arc<dyn Identity>>,
    agent: Option<Agent>,
    cose_canister: Principal,
    outer_http: Option<reqwest::Client>,
    allow_http: bool,
}

/// Returns a new Ed25519 identity from a 32-byte secret
pub fn identity_from_secret(id_secret: [u8; 32]) -> Box<dyn Identity> {
    Box::new(BasicIdentity::from_raw_key(&id_secret))
}

/// Loads an ICP identity from a PEM file (generated by dfx or other tools, supports both Ed25519 and Secp256k1)
pub fn identity_from_pem(path: &str) -> Result<Box<dyn Identity>, BoxError> {
    let content = std::fs::read_to_string(path)?;
    match Secp256k1Identity::from_pem(content.as_bytes()) {
        Ok(identity) => Ok(Box::new(identity)),
        Err(_) => match BasicIdentity::from_pem(content.as_bytes()) {
            Ok(identity) => Ok(Box::new(identity)),
            Err(err) => Err(err.into()),
        },
    }
}

/// Loads an identity from a 32-byte hex-encoded secret or PEM file
pub fn load_identity(id_secret_or_path: &str) -> Result<Box<dyn Identity>, BoxError> {
    if id_secret_or_path == "Anonymous" {
        return Ok(Box::new(AnonymousIdentity));
    }

    match identity_from_pem(id_secret_or_path) {
        Ok(identity) => Ok(identity),
        Err(_) => {
            // Never echo the input back: it may be a (mistyped) secret.
            let id_secret = hex::decode(id_secret_or_path).map_err(
                |_| "invalid id_secret: expected a PEM file path or a 32-byte hex secret",
            )?;
            let id_secret: [u8; 32] = id_secret.try_into().map_err(|secret: Vec<u8>| {
                format!(
                    "invalid id_secret length: expected 32 bytes, got {}",
                    secret.len()
                )
            })?;
            Ok(identity_from_secret(id_secret))
        }
    }
}

impl Default for ClientBuilder {
    fn default() -> Self {
        Self {
            ic_host: "https://icp-api.io".to_string(),
            root_secret: [0; 48],
            identity: None,
            agent: None,
            cose_canister: Principal::anonymous(),
            outer_http: None,
            allow_http: false,
        }
    }
}

impl ClientBuilder {
    /// Sets the base URL of the ICP network, default is `https://icp-api.io`
    pub fn with_ic_host(mut self, ic_host: &str) -> Self {
        self.ic_host = ic_host.to_string();
        self
    }

    /// Sets the 48-byte root secret for key derivation, default is all zeros
    pub fn with_root_secret(mut self, root_secret: [u8; 48]) -> Self {
        self.root_secret = root_secret;
        self
    }

    /// Sets the principal of the COSE canister, default is anonymous, which disables COSE operations
    pub fn with_cose_canister(mut self, cose_canister: Principal) -> Self {
        self.cose_canister = cose_canister;
        self
    }

    /// Sets the identity for cryptographic operations, default is anonymous
    pub fn with_identity(mut self, identity: Arc<dyn Identity>) -> Self {
        self.identity = Some(identity);
        self
    }

    /// Sets the agent for canister communication
    pub fn with_agent(mut self, agent: Agent) -> Self {
        self.agent = Some(agent);
        self
    }

    /// Sets the external HTTP client for making requests, default is a secure client
    pub fn with_http_client(mut self, http_client: reqwest::Client) -> Self {
        self.outer_http = Some(http_client);
        self
    }

    /// Allow HTTP connections (default is false)
    pub fn with_allow_http(mut self, allow_http: bool) -> Self {
        self.allow_http = allow_http;
        self
    }

    /// Builds a [`Client`] with the configured identity, agent, and HTTP client.
    pub async fn build(self) -> Result<Client, BoxError> {
        let identity = match self.identity {
            Some(identity) => identity,
            None => Arc::from(identity_from_secret(sha3_256(&self.root_secret))),
        };
        let principal = identity
            .sender()
            .map_err(|err| format!("failed to get the principal from identity: {err}"))?;

        let agent = match self.agent {
            Some(agent) => agent,
            None => {
                let agent = Agent::builder()
                    .with_url(self.ic_host.clone())
                    .with_arc_identity(identity.clone())
                    .with_verify_query_signatures(false)
                    .build()?;

                if self.ic_host.starts_with("http://") {
                    // local replica: ignore the error so the client can still be
                    // constructed when the replica is not running yet
                    let _ = agent.fetch_root_key().await;
                }
                agent
            }
        };

        let outer_http = match self.outer_http {
            Some(http_client) => http_client,
            None => reqwest::Client::builder()
                .use_rustls_tls()
                .https_only(!self.allow_http)
                .http2_keep_alive_interval(Some(Duration::from_secs(25)))
                .http2_keep_alive_timeout(Duration::from_secs(15))
                .http2_keep_alive_while_idle(true)
                .connect_timeout(Duration::from_secs(10))
                .timeout(Duration::from_secs(120))
                .gzip(true)
                .user_agent(APP_USER_AGENT)
                .build()?,
        };

        Ok(Client {
            outer_http,
            root_secret: self.root_secret,
            identity,
            principal,
            agent,
            cose_canister: self.cose_canister,
            allow_http: self.allow_http,
        })
    }
}

impl Client {
    /// Creates a client builder with secure defaults.
    pub fn builder() -> ClientBuilder {
        ClientBuilder::default()
    }

    /// Returns the principal of the client identity, resolved at build time.
    pub fn get_principal(&self) -> Principal {
        self.principal
    }

    /// Signs a message digest with the client identity.
    pub async fn sign_envelope(
        &self,
        message_digest: [u8; 32],
    ) -> Result<SignedEnvelope, BoxError> {
        let se = SignedEnvelope::sign_digest(&self.identity, message_digest.into())?;
        Ok(se)
    }

    /// Ensures the URL uses HTTPS, unless `allow_http` was enabled for local tests.
    fn check_url(&self, url: &str) -> Result<(), BoxError> {
        if !self.allow_http && !url.starts_with("https://") {
            return Err(format!("Invalid url {url:?}, must start with https://").into());
        }
        Ok(())
    }

    /// Signs `message_digest` with the client identity and merges the
    /// authorization headers into `headers`.
    fn sign_headers(
        &self,
        message_digest: [u8; 32],
        headers: Option<http::HeaderMap>,
    ) -> Result<http::HeaderMap, BoxError> {
        let se = SignedEnvelope::sign_digest(&self.identity, message_digest.into())?;
        let mut headers = headers.unwrap_or_default();
        se.to_authorization(&mut headers)?;
        Ok(headers)
    }

    /// Builds the signed authorization headers and CBOR body for a signed RPC
    /// request with CBOR-encoded `params`.
    fn signed_rpc_request(
        &self,
        method: &str,
        params: Vec<u8>,
    ) -> Result<(http::HeaderMap, Vec<u8>), BoxError> {
        let params: ByteBufB64 = params.into();
        let req = RPCRequestRef {
            method,
            params: &params,
        };
        let body = to_canonical_vec(&req)?;
        let digest: [u8; 32] = sha3_256(&body);
        let headers = self.sign_headers(digest, None)?;
        Ok((headers, body))
    }
}

/// Sends an HTTP request with optional headers and body using the given client.
async fn send_request(
    http: reqwest::Client,
    url: String,
    method: http::Method,
    headers: Option<http::HeaderMap>,
    body: Option<Vec<u8>>,
) -> Result<reqwest::Response, BoxError> {
    let mut req = http.request(method, url);
    if let Some(headers) = headers {
        req = req.headers(headers);
    }
    if let Some(body) = body {
        req = req.body(body);
    }

    req.send().await.map_err(|e| e.into())
}

impl Web3ClientFeatures for Client {
    fn get_principal(&self) -> Principal {
        self.principal
    }

    fn sign_envelope(
        &self,
        message_digest: [u8; 32],
    ) -> BoxPinFut<Result<SignedEnvelope, BoxError>> {
        let identity = self.identity.clone();
        Box::pin(async move {
            let se = SignedEnvelope::sign_digest(&identity, message_digest.into())?;
            Ok(se)
        })
    }

    /// Derives a 256-bit AES-GCM key from the given derivation path
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    ///
    /// # Returns
    /// Result containing the derived 256-bit key or an error
    fn a256gcm_key(&self, derivation_path: Vec<Vec<u8>>) -> BoxPinFut<Result<[u8; 32], BoxError>> {
        let res = crypto::a256gcm_key(&self.root_secret, derivation_path);
        Box::pin(futures::future::ready(Ok(res)))
    }

    /// Signs a message using Ed25519 signature scheme
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    /// * `message` - Message to be signed
    ///
    /// # Returns
    /// Result containing the 64-byte signature or an error
    fn ed25519_sign_message(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message: &[u8],
    ) -> BoxPinFut<Result<[u8; 64], BoxError>> {
        let res = crypto::ed25519_sign_message(&self.root_secret, derivation_path, message);
        Box::pin(futures::future::ready(Ok(res)))
    }

    /// Verifies an Ed25519 signature
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    /// * `message` - Original message that was signed
    /// * `signature` - Signature to verify
    ///
    /// # Returns
    /// Result indicating success or failure of verification
    fn ed25519_verify(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message: &[u8],
        signature: &[u8],
    ) -> BoxPinFut<Result<(), BoxError>> {
        let res = crypto::ed25519_public_key(&self.root_secret, derivation_path);
        Box::pin(futures::future::ready(
            ed25519_verify(&res.0, message, signature).map_err(|e| e.into()),
        ))
    }

    /// Gets the public key for Ed25519
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    ///
    /// # Returns
    /// Result containing the 32-byte public key or an error
    fn ed25519_public_key(
        &self,
        derivation_path: Vec<Vec<u8>>,
    ) -> BoxPinFut<Result<[u8; 32], BoxError>> {
        let res = crypto::ed25519_public_key(&self.root_secret, derivation_path);
        Box::pin(futures::future::ready(Ok(res.0)))
    }

    /// Signs a message using Secp256k1 BIP340 Schnorr signature
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    /// * `message` - Message to be signed
    ///
    /// # Returns
    /// Result containing the 64-byte signature or an error
    fn secp256k1_sign_message_bip340(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message: &[u8],
    ) -> BoxPinFut<Result<[u8; 64], BoxError>> {
        let res =
            crypto::secp256k1_sign_message_bip340(&self.root_secret, derivation_path, message);
        Box::pin(futures::future::ready(Ok(res)))
    }

    /// Verifies a Secp256k1 BIP340 Schnorr signature
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    /// * `message` - Original message that was signed
    /// * `signature` - Signature to verify
    ///
    /// # Returns
    /// Result indicating success or failure of verification
    fn secp256k1_verify_bip340(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message: &[u8],
        signature: &[u8],
    ) -> BoxPinFut<Result<(), BoxError>> {
        let res = crypto::secp256k1_public_key(&self.root_secret, derivation_path);
        Box::pin(futures::future::ready(
            secp256k1_verify_bip340(res.0.as_slice(), message, signature).map_err(|e| e.into()),
        ))
    }

    /// Signs a message using Secp256k1 ECDSA signature
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    /// * `message` - Message to be signed
    ///
    /// # Returns
    /// Result containing the 64-byte signature or an error
    fn secp256k1_sign_message_ecdsa(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message: &[u8],
    ) -> BoxPinFut<Result<[u8; 64], BoxError>> {
        let res = crypto::secp256k1_sign_message_ecdsa(&self.root_secret, derivation_path, message);
        Box::pin(futures::future::ready(Ok(res)))
    }

    fn secp256k1_sign_digest_ecdsa(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message_hash: &[u8],
    ) -> BoxPinFut<Result<[u8; 64], BoxError>> {
        let res =
            crypto::secp256k1_sign_digest_ecdsa(&self.root_secret, derivation_path, message_hash);
        Box::pin(futures::future::ready(Ok(res)))
    }

    /// Verifies a Secp256k1 ECDSA signature
    ///
    /// # Arguments
    /// * `derivation_path` - Additional path components for key derivation
    /// * `message` - Original message that was signed
    /// * `signature` - Signature to verify
    ///
    /// # Returns
    /// Result indicating success or failure of verification
    fn secp256k1_verify_ecdsa(
        &self,
        derivation_path: Vec<Vec<u8>>,
        message: &[u8],
        signature: &[u8],
    ) -> BoxPinFut<Result<(), BoxError>> {
        let res = crypto::secp256k1_public_key(&self.root_secret, derivation_path);
        Box::pin(futures::future::ready(
            secp256k1_verify_ecdsa(res.0.as_slice(), message, signature).map_err(|e| e.into()),
        ))
    }

    /// Gets the compressed SEC1-encoded public key for Secp256k1
    ///
    /// # Arguments
    /// * `path` - Base path for key derivation
    /// * `derivation_path` - Additional path components for key derivation
    ///
    /// # Returns
    /// Result containing the 33-byte public key or an error
    fn secp256k1_public_key(
        &self,
        derivation_path: Vec<Vec<u8>>,
    ) -> BoxPinFut<Result<[u8; 33], BoxError>> {
        let res = crypto::secp256k1_public_key(&self.root_secret, derivation_path);
        Box::pin(futures::future::ready(Ok(res.0)))
    }

    fn canister_query_raw(
        &self,
        canister: Principal,
        method: String,
        args: Vec<u8>,
    ) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
        let agent = self.agent.clone();
        Box::pin(async move {
            let res = agent.query(&canister, method).with_arg(args).call().await?;
            Ok(res)
        })
    }

    fn canister_update_raw(
        &self,
        canister: Principal,
        method: String,
        args: Vec<u8>,
    ) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
        let agent = self.agent.clone();
        Box::pin(async move {
            let res = agent
                .update(&canister, method)
                .with_arg(args)
                .call_and_wait()
                .await?;
            Ok(res)
        })
    }

    fn https_call(
        &self,
        url: String,
        method: http::Method,
        headers: Option<http::HeaderMap>,
        body: Option<Vec<u8>>, // default is empty
    ) -> BoxPinFut<Result<reqwest::Response, BoxError>> {
        if let Err(err) = self.check_url(&url) {
            return Box::pin(futures::future::ready(Err(err)));
        }

        Box::pin(send_request(
            self.outer_http.clone(),
            url,
            method,
            headers,
            body,
        ))
    }

    fn https_signed_call(
        &self,
        url: String,
        method: http::Method,
        message_digest: [u8; 32],
        headers: Option<http::HeaderMap>,
        body: Option<Vec<u8>>, // default is empty
    ) -> BoxPinFut<Result<reqwest::Response, BoxError>> {
        if let Err(err) = self.check_url(&url) {
            return Box::pin(futures::future::ready(Err(err)));
        }
        let headers = match self.sign_headers(message_digest, headers) {
            Ok(headers) => headers,
            Err(err) => return Box::pin(futures::future::ready(Err(err))),
        };

        Box::pin(send_request(
            self.outer_http.clone(),
            url,
            method,
            Some(headers),
            body,
        ))
    }

    fn https_signed_rpc_raw(
        &self,
        endpoint: String,
        method: String,
        args: Vec<u8>,
    ) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
        if let Err(err) = self.check_url(&endpoint) {
            return Box::pin(futures::future::ready(Err(err)));
        }
        let (headers, body) = match self.signed_rpc_request(&method, args) {
            Ok(req) => req,
            Err(err) => return Box::pin(futures::future::ready(Err(err))),
        };

        let outer_http = self.outer_http.clone();
        Box::pin(async move {
            let res = cbor_rpc(&outer_http, &endpoint, &method, Some(headers), body).await?;
            Ok(res.into_vec())
        })
    }
}

impl HttpFeatures for Client {
    /// Makes an HTTPs request
    ///
    /// # Arguments
    /// * `url` - Target URL, should start with `https://`
    /// * `method` - HTTP method (GET, POST, etc.)
    /// * `headers` - Optional HTTP headers
    /// * `body` - Optional request body (default empty)
    async fn https_call(
        &self,
        url: &str,
        method: http::Method,
        headers: Option<http::HeaderMap>,
        body: Option<Vec<u8>>, // default is empty
    ) -> Result<reqwest::Response, BoxError> {
        self.check_url(url)?;
        send_request(
            self.outer_http.clone(),
            url.to_string(),
            method,
            headers,
            body,
        )
        .await
    }

    /// Makes a signed HTTPs request with message authentication
    ///
    /// # Arguments
    /// * `url` - Target URL
    /// * `method` - HTTP method (GET, POST, etc.)
    /// * `message_digest` - 32-byte message digest for signing
    /// * `headers` - Optional HTTP headers
    /// * `body` - Optional request body (default empty)
    async fn https_signed_call(
        &self,
        url: &str,
        method: http::Method,
        message_digest: [u8; 32],
        headers: Option<http::HeaderMap>,
        body: Option<Vec<u8>>, // default is empty
    ) -> Result<reqwest::Response, BoxError> {
        self.check_url(url)?;
        let headers = self.sign_headers(message_digest, headers)?;
        send_request(
            self.outer_http.clone(),
            url.to_string(),
            method,
            Some(headers),
            body,
        )
        .await
    }

    /// Makes a signed CBOR-encoded RPC call
    ///
    /// # Arguments
    /// * `endpoint` - URL endpoint to send the request to
    /// * `method` - RPC method name to call
    /// * `args` - Arguments to serialize as CBOR and send with the request
    async fn https_signed_rpc<T>(
        &self,
        endpoint: &str,
        method: &str,
        args: impl Serialize + Send,
    ) -> Result<T, BoxError>
    where
        T: DeserializeOwned,
    {
        self.check_url(endpoint)?;
        let args = to_canonical_vec(&args)?;
        let (headers, body) = self.signed_rpc_request(method, args)?;
        let res = cbor_rpc(&self.outer_http, endpoint, method, Some(headers), body).await?;
        let res = from_slice(&res[..])?;
        Ok(res)
    }
}

/// Implements the `CoseSDK` trait for Client to enable IC-COSE canister API calls
///
/// This implementation provides the necessary interface to interact with the
/// [IC-COSE](https://github.com/ldclabs/ic-cose) canister, allowing cryptographic
/// operations through the COSE (CBOR Object Signing and Encryption) protocol.
impl CoseSDK for Client {
    fn canister(&self) -> &Principal {
        &self.cose_canister
    }
}

impl CanisterCaller for Client {
    /// Performs a query call to a canister (read-only, no state changes)
    ///
    /// # Arguments
    /// * `canister` - Target canister principal
    /// * `method` - Method name to call
    /// * `args` - Input arguments encoded in Candid format
    async fn canister_query<
        In: ArgumentEncoder + Send,
        Out: CandidType + for<'a> candid::Deserialize<'a>,
    >(
        &self,
        canister: &Principal,
        method: &str,
        args: In,
    ) -> Result<Out, BoxError> {
        let input = encode_args(args)?;
        let res = self
            .agent
            .query(canister, method)
            .with_arg(input)
            .call()
            .await?;
        let output = Decode!(res.as_slice(), Out)?;
        Ok(output)
    }

    /// Performs an update call to a canister (may modify state)
    ///
    /// # Arguments
    /// * `canister` - Target canister principal
    /// * `method` - Method name to call
    /// * `args` - Input arguments encoded in Candid format
    async fn canister_update<
        In: ArgumentEncoder + Send,
        Out: CandidType + for<'a> candid::Deserialize<'a>,
    >(
        &self,
        canister: &Principal,
        method: &str,
        args: In,
    ) -> Result<Out, BoxError> {
        let input = encode_args(args)?;
        let res = self
            .agent
            .update(canister, method)
            .with_arg(input)
            .call_and_wait()
            .await?;
        let output = Decode!(res.as_slice(), Out)?;
        Ok(output)
    }
}
