use clap::Parser;

use log::debug;
use obnam::{
    chunk::{Chunk, ChunkError, DataChunk, MetadataFactory},
    cipher::{Engine, EngineKind},
    client::ClientChunk,
    config::Config,
    credential::Credential,
    repository::RepositoryError,
};

use super::{Args, Leaf, MainError};

/// Create a new client chunk.
#[derive(Parser)]
pub struct InitClient {
    /// Name of the new client. Default is the name configured via
    /// configuration file.
    #[clap(long)]
    client_name: Option<String>,

    /// Create a credential chunk using this credential specification
    /// in the configuration file.
    #[clap(long)]
    credential: Option<String>,
}

impl Leaf for InitClient {
    type Error = ClientCmdError;

    fn run(&self, args: &Args, config: &Config) -> Result<(), Self::Error> {
        let client_name = self
            .client_name
            .as_deref()
            .unwrap_or(config.client_name(self.client_name.as_deref()));
        debug!("Create a client chunk for client {}", client_name);
        let repo = Self::open_repository(config)?;
        let mut client = ClientChunk::new(client_name);

        let client_key = if let Ok(client_key) = Self::client_key(args) {
            client_key
        } else {
            client
                .generate_key("default")
                .map_err(ClientCmdError::GenerateKey)?
        };

        let engine = Engine::new(EngineKind::Aead, client_key.clone());

        if repo.open_client(&engine, client_name).is_ok() {
            return Err(ClientCmdError::AleadyExists(client_name.to_string()));
        }

        let plaintext = client.to_plaintext().map_err(ClientCmdError::ToPlaintext)?;
        let mut factory = MetadataFactory::default();
        let metadata = factory
            .from_label("client")
            .map_err(ClientCmdError::Metadata)?;

        let data =
            DataChunk::encrypt(&engine, &plaintext, metadata).map_err(ClientCmdError::Encrypt)?;
        let chunk = Chunk::Client(data);

        let repo = Self::open_repository(config)?;
        repo.add_chunk(&chunk).map_err(ClientCmdError::AddChunk)?;

        if let Some(cred) = &self.credential
            && let Some(spec) = config.credentials().get(cred)
        {
            debug!("Create a credential for client {}", client_name);
            let method = spec.method(config);
            let credential =
                Credential::new(&method, &client_key).map_err(ClientCmdError::NewCredential)?;
            let metadata = factory
                .from_label("credential")
                .map_err(ClientCmdError::Metadata)?;
            let chunk = Chunk::credential(metadata, credential);
            repo.add_chunk(&chunk).map_err(ClientCmdError::AddChunk)?;
        }

        Ok(())
    }
}

/// List all client chunks.
#[derive(Parser)]
pub struct ListClients {}

impl Leaf for ListClients {
    type Error = ClientCmdError;

    fn run(&self, args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!("List all clients whose client chunks we can open");
        let repo = Self::open_repository(config)?;
        let engine = Engine::new(
            EngineKind::Aead,
            Self::client_key_from_repo(args, config, &repo)?.clone(),
        );
        let clients = repo
            .find_our_client_chunks(&engine)
            .map_err(ClientCmdError::ListClients)?;
        for client in clients {
            println!("{}", client.name());
        }
        Ok(())
    }
}

/// Show contents of a given client chunk.
#[derive(Parser)]
pub struct ShowClient {
    /// Name of client to show.
    #[clap(long)]
    client_name: Option<String>,
}

impl Leaf for ShowClient {
    type Error = ClientCmdError;

    fn run(&self, args: &Args, config: &Config) -> Result<(), Self::Error> {
        let client_name = config.client_name(self.client_name.as_deref());
        debug!("Show contents of client chunk for {}", client_name);
        let repo = Self::open_repository(config)?;
        let engine = Engine::new(
            EngineKind::Aead,
            Self::client_key_from_repo(args, config, &repo)?.clone(),
        );
        let client = repo
            .open_client(&engine, client_name)
            .map_err(|err| ClientCmdError::OpenClient(client_name.to_string(), err))?;
        println!(
            "{}",
            serde_json::to_string_pretty(&client).map_err(ClientCmdError::ToJson)?
        );
        Ok(())
    }
}

/// Generate a new chunk key and add it to the client chunk.
#[derive(Parser)]
pub struct GenerateKey {
    /// Name of client.
    client_name: String,

    /// Name of new key.
    key_name: String,
}

impl Leaf for GenerateKey {
    type Error = ClientCmdError;

    fn run(&self, args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!(
            "Generate a new chunk key {} for client {}",
            self.key_name, self.client_name
        );
        let repo = Self::open_repository(config)?;
        let engine = Engine::new(
            EngineKind::Aead,
            Self::client_key_from_repo(args, config, &repo)?.clone(),
        );
        let (id, existing) = repo
            .open_client(&engine, &self.client_name)
            .map_err(|err| ClientCmdError::OpenClient(self.client_name.clone(), err))?;

        let mut new = ClientChunk::new(existing.name());
        new.add_old_versions(existing.old_versions());
        new.add_old_versions(&[id]);
        new.generate_key(&self.key_name)
            .map_err(|err| ClientCmdError::Generate(self.client_name.clone(), err))?;
        assert_eq!(new.old_versions().len(), existing.old_versions().len() + 1);

        let plaintext = new.to_plaintext().map_err(ClientCmdError::ToPlaintext)?;
        let mut factory = MetadataFactory::default();
        let metadata = factory
            .from_label("client")
            .map_err(ClientCmdError::Metadata)?;
        let data =
            DataChunk::encrypt(&engine, &plaintext, metadata).map_err(ClientCmdError::Encrypt)?;
        let chunk = Chunk::Client(data);
        repo.add_chunk(&chunk).map_err(ClientCmdError::AddChunk)?;

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ClientCmdError {
    #[error(transparent)]
    Main(#[from] MainError),

    #[error("failed to add chunk to repository")]
    AddChunk(#[source] RepositoryError),

    #[error("failed to convert client chunk to plain text data")]
    ToPlaintext(#[source] obnam::client::ClientChunkError),

    #[error("failed to encrypt chunk")]
    Encrypt(#[source] ChunkError),

    #[error("can't create another client named {0}")]
    AleadyExists(String),

    #[error("can't convert client chunk to JSON")]
    ToJson(#[source] serde_json::Error),

    #[error("failed to generate a chunk key for client {0}")]
    Generate(String, #[source] obnam::client::ClientChunkError),

    #[error("failed to generate a default client key")]
    GenerateKey(#[source] obnam::client::ClientChunkError),

    #[error("failed to create a new credential value")]
    NewCredential(#[source] obnam::credential::CredentialError),

    #[error("failed to get list of clients from backup repository")]
    ListClients(#[source] obnam::repository::RepositoryError),

    #[error("failed to open client {0}")]
    OpenClient(String, #[source] obnam::repository::RepositoryError),

    #[error("failed to create chunk metadata")]
    Metadata(#[source] ChunkError),
}
