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

use clap::{Parser, ValueEnum};
use env_logger::{Builder, Env};
use git_testament::git_testament;
use log::{LevelFilter, error, info, trace};

use obnam::repository::{BackupRepository, RepositoryError};

use obnam_server::{
    api::*,
    config::*,
    token::{ObnamClaim, TokenFactory, TokenFactoryError},
};

// Name of environment variable to control logging.
const LOG_VAR: &str = "OBNAM_LOG";

// A placeholder user name until we add user management.
const PLACEHOLDER_USER: &str = "obnam";

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

async fn fallible_main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    if args.version {
        VersionCmd::report_version();
    } else {
        args.setup_logging();
        info!("{} starts", env!("CARGO_PKG_NAME"));
        trace!("{args:#?}");
        let config = args.config()?;
        if let Some(cmd) = &args.cmd {
            cmd.run(&config, &args).await?;
        }
    }

    Ok(())
}

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

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

/// Server component of the Obnam backup program.
///
/// Provide an HTTP API for accessing a remote backup repository.
#[derive(Debug, Parser)]
struct Args {
    /// Load configuration from this file.
    #[clap(long, value_name = "FILE")]
    config: Vec<PathBuf>,

    /// 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 std::env::var(LOG_VAR).is_ok() {
            Builder::from_env(Env::new().filter(LOG_VAR))
        } else {
            let mut builder = Builder::new();
            builder.filter_level(LevelFilter::Info);
            builder
        };
        builder.init();
    }

    fn config(&self) -> Result<ServerConfig, MainError> {
        let config = ServerConfig::load(&self.config)?;
        Ok(config)
    }
}

#[derive(Debug, Parser)]
enum Cmd {
    Version(VersionCmd),
    Init(InitCmd),
    Serve(ServeCmd),
    Token(TokenCmd),
    Validate(ValidateCmd),
}

impl Subcommand for Cmd {
    async fn run(&self, config: &ServerConfig, args: &Args) -> Result<(), AnyError> {
        match self {
            Self::Version(x) => x.run(config, args).await?,
            Self::Init(x) => x.run(config, args).await?,
            Self::Serve(x) => x.run(config, args).await?,
            Self::Token(x) => x.run(config, args).await?,
            Self::Validate(x) => x.run(config, args).await?,
        }
        Ok(())
    }
}

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

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

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

/// Initialize an Obnam server instance.
#[derive(Debug, Parser)]
struct InitCmd {}

impl Subcommand for InitCmd {
    async fn run(&self, config: &ServerConfig, _args: &Args) -> Result<(), AnyError> {
        // Create server backup repository if it doesn't exist.
        let dirname = config.repository().to_path_buf();
        if !dirname.exists() {
            std::fs::create_dir(&dirname)
                .map_err(|err| MainError::CreateRepositoryDirectory(dirname.clone(), err))?;
            let _repo = BackupRepository::init(&dirname)
                .map_err(|err| MainError::InitBackupRepo(dirname, err))?;
        }

        // Create server key pair.
        let filename = config.key_pair_file();
        if filename.exists() {
            Err(MainError::KeyPairFileExists(filename.to_path_buf()))?
        } else {
            let factory = TokenFactory::new(config.listen(), config.listen())?;
            let key_pair_file = factory.key_pair();
            key_pair_file.write(filename)?;
        }

        println!("init OK");
        Ok(())
    }
}

/// Generate a new token.
#[derive(Debug, Parser)]
struct TokenCmd {
    /// What class of API actions should token allow? Choose one. Choose wisely.
    #[clap(long)]
    allow: Allow,

    /// Write token to this file instead of stdout.
    #[clap(long)]
    output: Option<PathBuf>,
}

impl TokenCmd {
    fn extra_claims(&self) -> Vec<ObnamClaim> {
        let mut extra_claims = vec![
            ObnamClaim::ReadChunk,
            ObnamClaim::CreateChunk,
            ObnamClaim::FindChunks,
        ];
        match self.allow {
            Allow::Append => (),
            Allow::Delete => extra_claims.push(ObnamClaim::DeleteChunk),
        }
        extra_claims
    }
}

impl Subcommand for TokenCmd {
    async fn run(&self, config: &ServerConfig, _args: &Args) -> Result<(), AnyError> {
        let factory = TokenFactory::try_from(config)?;
        let token = factory.create_token(PLACEHOLDER_USER, &self.extra_claims())?;
        if let Some(filename) = &self.output {
            std::fs::write(filename, token.as_bytes())
                .map_err(|err| MainError::TokenWrite(filename.to_path_buf(), err))?;
        } else {
            println!("{token}");
        }
        Ok(())
    }
}

#[derive(Debug, Copy, Clone, ValueEnum)]
enum Allow {
    Append,
    Delete,
}

/// Generate a new token.
#[derive(Debug, Parser)]
struct ValidateCmd {
    /// Token to validate.
    #[clap(long, required_unless_present = "file")]
    token: Option<String>,

    /// Read token to validate from this file.
    #[clap(long)]
    file: Option<PathBuf>,
}

impl Subcommand for ValidateCmd {
    async fn run(&self, config: &ServerConfig, _args: &Args) -> Result<(), AnyError> {
        let factory = TokenFactory::try_from(config)?;
        let token = match (&self.token, &self.file) {
            (Some(token), None) => token.to_string(),
            (None, Some(filename)) => {
                let token = std::fs::read(filename)
                    .map_err(|err| MainError::TokenRead(filename.to_path_buf(), err))?;
                String::from_utf8(token)
                    .map_err(|err| MainError::TokenUtf8(filename.to_path_buf(), err))?
            }
            _ => panic!(),
        };
        let valid = factory.validate(&token)?;
        println!("{valid:#?}");
        Ok(())
    }
}

/// Run Obnam server HTTP API.
#[derive(Debug, Parser)]
struct ServeCmd {}

impl Subcommand for ServeCmd {
    async fn run(&self, config: &ServerConfig, _args: &Args) -> Result<(), AnyError> {
        let api = HttpApi::try_from(config).map_err(|err| MainError::HttpApi(Box::new(err)))?;
        api.serve().await?;
        Ok(())
    }
}

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

    #[error(transparent)]
    Token(#[from] TokenFactoryError),

    #[error("can't init: key pair file already exists: {0}")]
    KeyPairFileExists(PathBuf),

    #[error("failed to write token to {0}")]
    TokenWrite(PathBuf, #[source] std::io::Error),

    #[error("failed to read token from {0}")]
    TokenRead(PathBuf, #[source] std::io::Error),

    #[error("failed to parse as UTF8 token from {0}")]
    TokenUtf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("failed to create HTTP data structure from configuration")]
    HttpApi(#[source] Box<ApiError>),

    #[error("failed to create directory for server backup repository {0}")]
    CreateRepositoryDirectory(PathBuf, #[source] std::io::Error),

    #[error("failed to initialize server backup repository {0}")]
    InitBackupRepo(PathBuf, #[source] RepositoryError),
}
