use std::sync::Arc;

use crate::{
    types::Capability,
    util::{create_websocket_client, query},
    ClientInfo,
};
use anyhow::{anyhow, Context, Result};
use futures::StreamExt;
use graphql_client::{GraphQLQuery, Response};
use graphql_ws_client::graphql::StreamingOperation;

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct RequestCapability;

pub async fn request_capability(
    executor_url: String,
    app_name: String,
    app_desc: String,
    app_domain: String,
    app_url: Option<String>,
    app_icon_path: Option<String>,
    capabilities: Option<Vec<Capability>>,
) -> Result<String> {
    let query = RequestCapability::build_query(request_capability::Variables {
        auth_info: request_capability::AuthInfoInput {
            app_name: app_name,
            app_desc: app_desc,
            app_domain: app_domain,
            app_url: app_url,
            app_icon_path: app_icon_path,
            capabilities: capabilities.map(|val| val.into_iter().map(|val| val.into()).collect()),
        },
    });
    let response_body: Response<request_capability::ResponseData> = reqwest::Client::new()
        .post(executor_url)
        .json(&query)
        .send()
        .await?
        .json()
        .await?;

    let response_data = response_body
        .data
        .ok_or_else(|| anyhow!("No data in response! Errors: {:?}", response_body.errors))?;
    Ok(response_data.agent_request_capability)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct RetrieveCapability;

pub async fn retrieve_capability(
    executor_url: String,
    request_id: String,
    rand: String,
) -> Result<String> {
    let query =
        RetrieveCapability::build_query(retrieve_capability::Variables { request_id, rand });
    let response_body: Response<retrieve_capability::ResponseData> = reqwest::Client::new()
        .post(executor_url)
        .json(&query)
        .send()
        .await?
        .json()
        .await?;

    let response_data = response_body
        .data
        .ok_or_else(|| anyhow!("No data in response! Errors: {:?}", response_body.errors))?;
    Ok(response_data.agent_generate_jwt)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct Me;

pub async fn me(executor_url: String, cap_token: String) -> Result<me::MeAgent> {
    let response_data: me::ResponseData =
        query(executor_url, cap_token, Me::build_query(me::Variables {}))
            .await
            .with_context(|| "Failed to run agent->me query")?;
    Ok(response_data.agent)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct GetApps;

pub async fn get_apps(
    executor_url: String,
    cap_token: String,
) -> Result<Vec<get_apps::GetAppsAgentGetApps>> {
    let response_data: get_apps::ResponseData = query(
        executor_url,
        cap_token,
        GetApps::build_query(get_apps::Variables {}),
    )
    .await
    .with_context(|| "Failed to run agent->get apps query")?;
    Ok(response_data.agent_get_apps)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct RevokeToken;

pub async fn revoke_token(
    executor_url: String,
    cap_token: String,
    request_id: String,
) -> Result<Vec<revoke_token::RevokeTokenAgentRevokeToken>> {
    let response_data: revoke_token::ResponseData = query(
        executor_url,
        cap_token,
        RevokeToken::build_query(revoke_token::Variables { request_id }),
    )
    .await
    .with_context(|| "Failed to run agent->revoke_token query")?;
    Ok(response_data.agent_revoke_token)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct RemoveApp;

pub async fn remove_app(
    executor_url: String,
    cap_token: String,
    request_id: String,
) -> Result<Vec<remove_app::RemoveAppAgentRemoveApp>> {
    let response_data: remove_app::ResponseData = query(
        executor_url,
        cap_token,
        RemoveApp::build_query(remove_app::Variables { request_id }),
    )
    .await
    .with_context(|| "Failed to run agent->remove_app query")?;
    Ok(response_data.agent_remove_app)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct AgentStatus;

pub async fn status(
    executor_url: String,
    cap_token: String,
) -> Result<agent_status::AgentStatusAgentStatus> {
    let response_data: agent_status::ResponseData = query(
        executor_url,
        cap_token,
        AgentStatus::build_query(agent_status::Variables {}),
    )
    .await
    .with_context(|| "Failed to run agent->me query")?;
    Ok(response_data.agent_status)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct Lock;

pub async fn lock(
    executor_url: String,
    cap_token: String,
    passphrase: String,
) -> Result<lock::LockAgentLock> {
    let response_data: lock::ResponseData = query(
        executor_url,
        cap_token,
        Lock::build_query(lock::Variables { passphrase }),
    )
    .await
    .with_context(|| "Failed to run agent->lock")?;
    Ok(response_data.agent_lock)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct Unlock;

pub async fn unlock(
    executor_url: String,
    cap_token: String,
    passphrase: String,
    holochain: bool
) -> Result<unlock::UnlockAgentUnlock> {
    let response_data: unlock::ResponseData = query(
        executor_url,
        cap_token,
        Unlock::build_query(unlock::Variables { passphrase, holochain }),
    )
    .await
    .with_context(|| "Failed to run agent->unlock")?;
    Ok(response_data.agent_unlock)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct ByDID;

pub async fn by_did(
    executor_url: String,
    cap_token: String,
    did: String,
) -> Result<Option<by_did::ByDidAgentByDid>> {
    let response_data: by_did::ResponseData = query(
        executor_url,
        cap_token,
        ByDID::build_query(by_did::Variables { did }),
    )
    .await
    .with_context(|| "Failed to run agent->byDID query")?;
    Ok(response_data.agent_by_did)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct Generate;

pub async fn generate(
    executor_url: String,
    cap_token: String,
    passphrase: String,
) -> Result<generate::GenerateAgentGenerate> {
    let response_data: generate::ResponseData = query(
        executor_url,
        cap_token,
        Generate::build_query(generate::Variables { passphrase }),
    )
    .await
    .with_context(|| "Failed to run agent->generate")?;
    Ok(response_data.agent_generate)
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct SignMessage;

pub async fn sign_message(
    executor_url: String,
    cap_token: String,
    message: String,
) -> Result<sign_message::SignMessageAgentSignMessage> {
    let response: sign_message::ResponseData = query(
        executor_url,
        cap_token,
        SignMessage::build_query(sign_message::Variables { message }),
    )
    .await
    .with_context(|| "Failed to run agent->sign_message")?;

    Ok(response.agent_sign_message)
}

#[derive(GraphQLQuery, Debug, Clone)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct AddEntanglementProofs;

pub async fn add_entanglement_proofs(
    executor_url: String,
    cap_token: String,
    proofs: Vec<add_entanglement_proofs::EntanglementProofInput>,
) -> Result<add_entanglement_proofs::ResponseData> {
    query(
        executor_url,
        cap_token,
        AddEntanglementProofs::build_query(add_entanglement_proofs::Variables { proofs }),
    )
    .await
    .with_context(|| "Failed to run runtime->add-trusted-agents query")
}

#[derive(GraphQLQuery, Debug, Clone)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct DeleteEntanglementProofs;

pub async fn delete_entanglement_proofs(
    executor_url: String,
    cap_token: String,
    proofs: Vec<delete_entanglement_proofs::EntanglementProofInput>,
) -> Result<delete_entanglement_proofs::ResponseData> {
    query(
        executor_url,
        cap_token,
        DeleteEntanglementProofs::build_query(delete_entanglement_proofs::Variables { proofs }),
    )
    .await
    .with_context(|| "Failed to run runtime->add-trusted-agents query")
}

#[derive(GraphQLQuery, Debug, Clone)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct EntanglementProofPreFlight;

pub async fn entanglement_proof_pre_flight(
    executor_url: String,
    cap_token: String,
    device_key: String,
    device_key_type: String,
) -> Result<entanglement_proof_pre_flight::ResponseData> {
    query(
        executor_url,
        cap_token,
        EntanglementProofPreFlight::build_query(entanglement_proof_pre_flight::Variables {
            device_key,
            device_key_type,
        }),
    )
    .await
    .with_context(|| "Failed to run runtime->add-trusted-agents query")
}

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.gql",
    query_path = "src/agent.gql",
    response_derives = "Debug"
)]
pub struct SubscriptionAgentStatusChanged;

pub async fn watch(executor_url: String, cap_token: String) -> Result<()> {
    let mut client = create_websocket_client(executor_url, cap_token)
        .await
        .with_context(|| "Failed to create websocket client")?;

    println!("Successfully created websocket client");
    let mut stream = client
        .streaming_operation({
            StreamingOperation::<SubscriptionAgentStatusChanged>::new(
                subscription_agent_status_changed::Variables {},
            )
        })
        .await
        .with_context(|| "Failed to subscribe to agentStatusChanged")?;

    println!("Successfully subscribed agentStatusChanged",);
    println!("Waiting for events...");

    while let Some(item) = stream.next().await {
        match item {
            Ok(response) => {
                if let Some(data) = response.data.and_then(|data| data.agent_status_changed) {
                    println!("Received agentStatusChanged: {:?}", data);
                }
            }
            Err(e) => {
                println!("Received Error: {:?}", e);
            }
        }
    }

    println!("Stream ended. Exiting...");

    Ok(())
}

pub struct AgentClient {
    info: Arc<ClientInfo>,
}

impl AgentClient {
    pub fn new(info: Arc<ClientInfo>) -> Self {
        Self { info }
    }

    pub async fn request_capability(
        &self,
        app_name: String,
        app_desc: String,
        app_domain: String,
        app_url: Option<String>,
        app_icon_path: Option<String>,
        capabilities: Option<Vec<Capability>>,
    ) -> Result<String> {
        request_capability(
            self.info.executor_url.clone(),
            app_name,
            app_desc,
            app_domain,
            app_url,
            app_icon_path,
            capabilities,
        )
        .await
    }

    pub async fn retrieve_capability(&self, request_id: String, rand: String) -> Result<String> {
        retrieve_capability(self.info.executor_url.clone(), request_id, rand).await
    }

    pub async fn me(&self) -> Result<me::MeAgent> {
        me(self.info.executor_url.clone(), self.info.cap_token.clone()).await
    }

    pub async fn status(&self) -> Result<agent_status::AgentStatusAgentStatus> {
        status(self.info.executor_url.clone(), self.info.cap_token.clone()).await
    }

    pub async fn get_apps(&self) -> Result<Vec<get_apps::GetAppsAgentGetApps>> {
        get_apps(self.info.executor_url.clone(), self.info.cap_token.clone()).await
    }

    pub async fn lock(&self, passphrase: String) -> Result<lock::LockAgentLock> {
        lock(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            passphrase,
        )
        .await
    }

    pub async fn unlock(&self, passphrase: String, holochain: bool) -> Result<unlock::UnlockAgentUnlock> {
        unlock(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            passphrase,
            holochain
        )
        .await
    }

    pub async fn by_did(&self, did: String) -> Result<Option<by_did::ByDidAgentByDid>> {
        by_did(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            did,
        )
        .await
    }

    pub async fn generate(&self, passphrase: String) -> Result<generate::GenerateAgentGenerate> {
        generate(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            passphrase,
        )
        .await
    }

    pub async fn sign_message(
        &self,
        message: String,
    ) -> Result<sign_message::SignMessageAgentSignMessage> {
        sign_message(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            message,
        )
        .await
    }

    pub async fn add_entanglement_proofs(
        &self,
        proofs: Vec<add_entanglement_proofs::EntanglementProofInput>,
    ) -> Result<add_entanglement_proofs::ResponseData> {
        add_entanglement_proofs(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            proofs,
        )
        .await
    }

    pub async fn delete_entanglement_proofs(
        &self,
        proofs: Vec<delete_entanglement_proofs::EntanglementProofInput>,
    ) -> Result<delete_entanglement_proofs::ResponseData> {
        delete_entanglement_proofs(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            proofs,
        )
        .await
    }

    pub async fn entanglement_proof_pre_flight(
        &self,
        device_key: String,
        device_key_type: String,
    ) -> Result<entanglement_proof_pre_flight::ResponseData> {
        entanglement_proof_pre_flight(
            self.info.executor_url.clone(),
            self.info.cap_token.clone(),
            device_key,
            device_key_type,
        )
        .await
    }

    pub async fn watch(&self) -> Result<()> {
        watch(self.info.executor_url.clone(), self.info.cap_token.clone()).await
    }
}
