use crate::CommonOpts;
use anyhow::{Context, Result};
use holochain_client::{
    AdminWebsocket, AppWebsocket, AuthorizeSigningCredentialsPayload, CellInfo, ClientAgentSigner,
    ExternIO, ZomeCallTarget,
};
use serde::de::DeserializeOwned;

/// A Holochain Agent Manager
pub struct Ham {
    app_connection: AppWebsocket,
    _signer: ClientAgentSigner,
}

impl Ham {
    /// Connect to a running Holochain conductor's admin interface
    pub async fn connect(value: CommonOpts) -> Result<Self> {
        let admin_port = value.admin_port;
        let app_id = value.app_id;

        use std::net::Ipv4Addr;
        let admin = AdminWebsocket::connect((Ipv4Addr::LOCALHOST, admin_port))
            .await
            .context("Failed to connect to admin interface")?;

        // find is an app interface exists if it does then use it else create an app interface
        let app_interfaces = admin
            .list_app_interfaces()
            .await
            .context("Failed to list app interfaces")?;

        let app_interface = app_interfaces
            .iter()
            .find(|app_interface| app_interface.installed_app_id == None);

        let port = if let Some(app_interface) = app_interface {
            println!("Using existing app interface: {:?}", app_interface.port);
            app_interface.port
        } else {
            let port = admin
                .attach_app_interface(value.app_port, holochain_client::AllowedOrigins::Any, None)
                .await
                .context("Failed to attach app interface")?;
            port
        };
        let issued_token: holochain_client::AppAuthenticationTokenIssued = admin
            .issue_app_auth_token(app_id.to_string().into())
            .await
            .context("Failed to issue app auth token")?;
        let signer = ClientAgentSigner::default();
        let app_connection = AppWebsocket::connect(
            (Ipv4Addr::LOCALHOST, port),
            issued_token.token,
            signer.clone().into(),
        )
        .await
        .context("Failed to connect to app interface")?;

        let installed_app = app_connection.app_info().await.unwrap().unwrap();

        let cells = installed_app.cell_info.into_values().next().unwrap();
        let cell_id = match cells[0].clone() {
            CellInfo::Provisioned(c) => c.cell_id,
            _ => panic!("Invalid cell type"),
        };
        let credentials = admin
            .authorize_signing_credentials(AuthorizeSigningCredentialsPayload {
                cell_id: cell_id.clone(),
                functions: None,
            })
            .await
            .unwrap();
        signer.add_credentials(cell_id.clone(), credentials);

        Ok(Self {
            app_connection,
            _signer: signer,
        })
    }

    pub async fn zome_call<I, R>(
        &self,
        role_name: &str,
        zome_name: &str,
        fn_name: &str,
        payload: I,
    ) -> Result<R>
    where
        I: serde::Serialize + std::fmt::Debug,
        R: DeserializeOwned,
    {
        let response = self
            .app_connection
            .call_zome(
                ZomeCallTarget::RoleName(role_name.to_string()),
                zome_name.into(),
                fn_name.into(),
                ExternIO::encode(payload)?,
            )
            .await
            .map_err(|e| anyhow::anyhow!("Failed to call zome: {}", e))?;
        rmp_serde::from_slice(&response.0).context("Failed to deserialize response")
    }
}
