use std::path::PathBuf;

use clap::Parser;

use log::debug;
use obnam::{
    chunk::{Chunk, ChunkError, Metadata, MetadataFactory},
    config::Config,
    credential::{Credential, CredentialMethod},
    id::Id,
    sop::{OpenPgpKey, Sop, SopCard},
};

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

/// Create a new OpenPGP software credential.
#[derive(Parser)]
pub struct OpenPgpSoftCmd {
    openpgp_key: PathBuf,
}

impl Leaf for OpenPgpSoftCmd {
    type Error = CredentialCmdError;

    fn run(&self, args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!("Create an OpenPGP software key credential");
        let openpgp_key = OpenPgpKey::read(&self.openpgp_key)
            .map_err(|err| CredentialCmdError::ReadOpenPgpKey(self.openpgp_key.clone(), err))?;

        let repo = Self::open_repository(config)?;
        let client_key = Self::client_key_from_repo(args, config, &repo)?;

        let sop = Sop::new(config.sop());
        let method = CredentialMethod::openpgp_soft(sop, openpgp_key);

        let credential = Credential::new(&method, &client_key).unwrap();

        let mut factory = MetadataFactory::default();
        let metadata = factory
            .from_label("credential")
            .map_err(CredentialCmdError::Metadata)?;
        let chunk = Chunk::credential(metadata, credential);

        repo.add_chunk(&chunk)
            .map_err(CredentialCmdError::AddChunk)?;

        Ok(())
    }
}

/// Create a new OpenPGP credential using a key on an OpenPGP card.
#[derive(Parser)]
pub struct OpenPgpCardCmd {
    openpgp_cert: PathBuf,
}

impl Leaf for OpenPgpCardCmd {
    type Error = CredentialCmdError;

    fn run(&self, args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!("Create an OpenPGP card credential");
        let openpgp_cert = obnam::sop::read_cert(&self.openpgp_cert)
            .map_err(|err| CredentialCmdError::ReadOpenPgpCert(self.openpgp_cert.clone(), err))?;

        let repo = Self::open_repository(config)?;
        let client_key = Self::client_key_from_repo(args, config, &repo)?;

        let sop_card = SopCard::new(config.sop(), config.sop_card());
        let method = CredentialMethod::openpgp_card(sop_card, openpgp_cert);

        let credential = Credential::new(&method, &client_key).unwrap();

        let mut factory = MetadataFactory::default();
        let metadata = factory
            .from_label("credential")
            .map_err(CredentialCmdError::Metadata)?;
        let chunk = Chunk::credential(metadata, credential);

        repo.add_chunk(&chunk)
            .map_err(CredentialCmdError::AddChunk)?;

        Ok(())
    }
}

/// List all credential in the backup repository.
#[derive(Parser)]
pub struct ListCmd {
    /// List all credentials. Default is to list only ones user can
    /// open using credentials defined in the configuration file.
    #[clap(long)]
    all: bool,
}

impl Leaf for ListCmd {
    type Error = CredentialCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let repo = Self::open_repository(config)?;

        let credentials = repo
            .find_credential_chunks()
            .map_err(CredentialCmdError::FindCredential)?;

        let metas: Vec<&Metadata> = if self.all {
            debug!("List all credentials in the chunk repository");
            credentials.iter().collect()
        } else {
            debug!("List credentials that we can open");
            let methods = config.credential_methods();

            let mut metas = vec![];
            for meta in credentials.iter() {
                let credential = repo
                    .get_credential_chunk(meta.id())
                    .map_err(|err| CredentialCmdError::GetCredential(meta.id().clone(), err))?;
                for method in methods.iter() {
                    if credential.decrypt(method).is_ok() {
                        metas.push(meta);
                    }
                }
            }
            metas
        };

        for meta in metas {
            println!("{}", meta.id());
        }

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum CredentialCmdError {
    #[error("failed to read OpenPGP key file")]
    ReadOpenPgpKey(PathBuf, #[source] obnam::sop::SopError),

    #[error("failed to read OpenPGP cert file")]
    ReadOpenPgpCert(PathBuf, #[source] obnam::sop::SopError),

    #[error(transparent)]
    Main(#[from] MainError),

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

    #[error("failed to find credential chunks")]
    FindCredential(#[source] obnam::repository::RepositoryError),

    #[error("failed to get credential chunk from backup repository: {0}")]
    GetCredential(Id, #[source] obnam::repository::RepositoryError),

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