use std::{path::PathBuf, process::exit};

use clap::Parser;
use env_logger::Builder;
use git_testament::git_testament;
use log::{LevelFilter, debug, error};

mod cmd;
use cmd::Leaf;

use obnam::{
    config::{self, Config},
    id::Id,
};

fn main() {
    if let Err(e) = fallible_main() {
        error!("ERROR: {e}");
        let mut e = e.source();
        while let Some(source) = e {
            error!("caused by: {source}");
            e = source.source();
        }
        exit(1);
    }
}

fn fallible_main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    if args.version {
        VersionCmd::report_version();
    } else {
        args.setup_logging();
        let config = args.config()?;
        debug!("Run-time configuration: {config:#?}");
        if let Some(cmd) = &args.cmd {
            cmd.run(&args, &config)?;
        }
    }

    Ok(())
}

type AnyError = Box<dyn std::error::Error>;

trait Subcommand {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError>;
}

/// Exploration platform for experimenting with the implementation of
/// fundamental backup implementation components.
///
/// This the third generation of Obnam. It exists for me to experiment
/// with the implementation of fundamental parts of backup software.
/// This is not a backup program, at this time, but in the far future
/// it may become one.
#[derive(Parser)]
struct Args {
    /// Load this additional configuration file. Can be used any
    /// number of times.
    #[clap(short, long)]
    config: Option<Vec<PathBuf>>,

    /// Location of backup repository.
    #[clap(long, short)]
    repository: Option<PathBuf>,

    /// Key for the client chunk. This is meant to be used in situations
    /// when key is known, but no credential chunk can be opened, and in
    /// the program automated test suite.
    #[clap(long)]
    client_key: Option<String>,

    /// Log level to use.c
    ///
    /// One of "off" (no logging),
    /// "trace" (debug information for Obnam developers),
    /// "debug" (debug info for Obnam users),
    /// "info" (high-level info ow),
    /// "warn" (problems that don't prevent continuing), or
    /// "error" (problems that prevent Obnam from continuing).
    #[clap(long)]
    log_level: Option<log::LevelFilter>,

    /// Report program version.
    #[clap(long, short)]
    version: bool,

    #[clap(subcommand)]
    cmd: Option<Cmd>,
}

impl Args {
    fn setup_logging(&self) {
        let mut builder = if let Some(filter) = self.log_level {
            let mut builder = Builder::new();
            builder.filter_level(filter);
            builder
        } else if let Ok(env) = std::env::var("OBNAM_LOG") {
            Builder::from_env(env)
        } else {
            let mut builder = Builder::new();
            builder.filter_level(LevelFilter::Info);
            builder
        };
        builder.init();
        let v = env!("CARGO_PKG_VERSION");
        log::trace!("obnam version {v}");
        log::debug!("obnam version {v}");
        log::info!("obnam version {v}");
        log::warn!("obnam version {v}");
        log::error!("obnam version {v}");
    }

    fn config(&self) -> Result<Config, MainError> {
        let extra = self.config.as_ref().map(|v| v.to_vec()).unwrap_or_default();

        let overrides = config::File {
            repository: self.repository.clone(),
            ..Default::default()
        };

        Config::load(&extra, &overrides).map_err(MainError::LoadConfig)
    }
}

#[derive(Parser)]
enum Cmd {
    Version(VersionCmd),
    Chunk(ChunkCmd),
    Config(ConfigCmd),
    Client(ClientCmd),
    Credential(CredentialCmd),
    Remote(RemoteCmd),
    Sop(SopCmd),
    Repo(RepoCmd),
}

impl Subcommand for Cmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match self {
            Self::Version(x) => x.run(args, config)?,
            Self::Chunk(x) => x.run(args, config)?,
            Self::Config(x) => x.run(args, config)?,
            Self::Client(x) => x.run(args, config)?,
            Self::Credential(x) => x.run(args, config)?,
            Self::Remote(x) => x.run(args, config)?,
            Self::Sop(x) => x.run(args, config)?,
            Self::Repo(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

/// Report version of program.
#[derive(Parser)]
struct VersionCmd {}

impl Subcommand for VersionCmd {
    fn run(&self, _args: &Args, _config: &Config) -> Result<(), AnyError> {
        Self::report_version();
        Ok(())
    }
}

impl VersionCmd {
    fn report_version() {
        git_testament!(VERSION);
        println!("{} {VERSION}", env!("CARGO_BIN_NAME"));
    }
}

/// Manage chunks of backed up content.
#[derive(Parser)]
struct ChunkCmd {
    #[clap(subcommand)]
    cmd: ChunkSubCmd,
}

impl Subcommand for ChunkCmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match &self.cmd {
            ChunkSubCmd::Decrypt(x) => x.run(args, config)?,
            ChunkSubCmd::Encrypt(x) => x.run(args, config)?,
            ChunkSubCmd::Inspect(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

#[derive(Parser)]
enum ChunkSubCmd {
    Decrypt(cmd::chunk::Decrypt),
    Encrypt(cmd::chunk::Encrypt),
    Inspect(cmd::chunk::Inspect),
}

/// Show run time configuration.
#[derive(Parser)]
struct ConfigCmd {}

impl Subcommand for ConfigCmd {
    fn run(&self, _args: &Args, config: &Config) -> Result<(), AnyError> {
        debug!("Show run-time config to stdout");
        println!("{}", config.to_json()?);
        Ok(())
    }
}

/// Manage client chunks.
#[derive(Parser)]
struct ClientCmd {
    #[clap(subcommand)]
    cmd: ClientSubCmd,
}

impl Subcommand for ClientCmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match &self.cmd {
            ClientSubCmd::Generate(x) => x.run(args, config)?,
            ClientSubCmd::Init(x) => x.run(args, config)?,
            ClientSubCmd::List(x) => x.run(args, config)?,
            ClientSubCmd::Show(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

#[derive(Parser)]
enum ClientSubCmd {
    Generate(cmd::client::GenerateKey),
    Init(cmd::client::InitClient),
    List(cmd::client::ListClients),
    Show(cmd::client::ShowClient),
}

/// Manage credential chunks.
#[derive(Parser)]
struct CredentialCmd {
    #[clap(subcommand)]
    cmd: CredentialSubCmd,
}

impl Subcommand for CredentialCmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match &self.cmd {
            CredentialSubCmd::List(x) => x.run(args, config)?,
            CredentialSubCmd::OpenPgpSoft(x) => x.run(args, config)?,
            CredentialSubCmd::OpenPgpCard(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

#[derive(Parser)]
enum CredentialSubCmd {
    #[clap(id = "openpgp-soft")]
    OpenPgpSoft(cmd::credential::OpenPgpSoftCmd),
    #[clap(id = "openpgp-card")]
    OpenPgpCard(cmd::credential::OpenPgpCardCmd),
    List(cmd::credential::ListCmd),
}

/// Use a remote Obnam server chunk storage.
#[derive(Parser)]
struct RemoteCmd {
    #[clap(subcommand)]
    cmd: RemoteSubCmd,
}

impl Subcommand for RemoteCmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match &self.cmd {
            RemoteSubCmd::Get(x) => x.run(args, config)?,
            RemoteSubCmd::Put(x) => x.run(args, config)?,
            RemoteSubCmd::Find(x) => x.run(args, config)?,
            RemoteSubCmd::Delete(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

#[derive(Parser)]
enum RemoteSubCmd {
    Get(cmd::remote::GetRemote),
    Put(cmd::remote::PutRemote),
    Find(cmd::remote::FindRemote),
    Delete(cmd::remote::DeleteRemote),
}

/// Use SOP.
#[derive(Parser)]
struct SopCmd {
    #[clap(subcommand)]
    cmd: SopSubCmd,
}

impl Subcommand for SopCmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match &self.cmd {
            SopSubCmd::ExtractCert(x) => x.run(args, config)?,
            SopSubCmd::Encrypt(x) => x.run(args, config)?,
            SopSubCmd::Decrypt(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

#[derive(Parser)]
enum SopSubCmd {
    ExtractCert(cmd::sop::ExtractCert),
    Encrypt(cmd::sop::Encrypt),
    Decrypt(cmd::sop::Decrypt),
}

/// Manage backup repository.
#[derive(Parser)]
struct RepoCmd {
    #[clap(subcommand)]
    cmd: RepoSubCmd,
}

impl Subcommand for RepoCmd {
    fn run(&self, args: &Args, config: &Config) -> Result<(), AnyError> {
        match &self.cmd {
            RepoSubCmd::Is(x) => x.run(args, config)?,
            RepoSubCmd::Init(x) => x.run(args, config)?,
            RepoSubCmd::List(x) => x.run(args, config)?,
            RepoSubCmd::Find(x) => x.run(args, config)?,
            RepoSubCmd::Path(x) => x.run(args, config)?,
            RepoSubCmd::Remove(x) => x.run(args, config)?,
        }
        Ok(())
    }
}

#[derive(Parser)]
enum RepoSubCmd {
    Is(cmd::repo::IsRepo),
    Init(cmd::repo::InitRepo),
    List(cmd::repo::ListRepoChunks),
    Find(cmd::repo::FindRepoChunks),
    Path(cmd::repo::RepoChunkPath),
    Remove(cmd::repo::RepoRemoveChunk),
}

#[derive(Debug, thiserror::Error)]
pub enum MainError {
    /// Repository needed, but not given by user.
    #[error("no repository given with --repository")]
    NoRepository,

    /// Can't open given repository.
    #[error("failed to open repository")]
    OpenRepository(#[source] obnam::repository::RepositoryError),

    /// Client key is needed.
    #[error("need client key for operation, use global option --client-key")]
    NeedClientKey,

    /// Can't load configuration files.
    #[error("failed to load configuration files")]
    LoadConfig(#[source] obnam::config::ObnamConfigError),

    /// Can't find credential chunks.
    #[error("failed to find credential chunks")]
    FindCredential(#[source] obnam::repository::RepositoryError),

    /// Can't get a specific credential.
    #[error("failed to get credential chunk from backup repository: {0}")]
    GetCredential(Id, #[source] obnam::repository::RepositoryError),
}
