// args.rs

// *************************************************************************
// * Copyright (C) 2020 Daniel Mueller (deso@posteo.net)                   *
// *                                                                       *
// * This program is free software: you can redistribute it and/or modify  *
// * it under the terms of the GNU General Public License as published by  *
// * the Free Software Foundation, either version 3 of the License, or     *
// * (at your option) any later version.                                   *
// *                                                                       *
// * This program is distributed in the hope that it will be useful,       *
// * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
// * GNU General Public License for more details.                          *
// *                                                                       *
// * You should have received a copy of the GNU General Public License     *
// * along with this program.  If not, see <http://www.gnu.org/licenses/>. *
// *************************************************************************

/// Provides access to a Nitrokey device
#[derive(structopt::StructOpt)]
#[structopt(name = "nitrocli")]
pub struct Args {
  /// Increases the log level (can be supplied multiple times)
  #[structopt(short, long, global = true, parse(from_occurrences))]
  pub verbose: u8,
  /// Selects the device model to connect to
  #[structopt(short, long, global = true, possible_values = &DeviceModel::all_str())]
  pub model: Option<DeviceModel>,
  #[structopt(subcommand)]
  pub cmd: Command,
}

/// The available Nitrokey models.
#[allow(unused_doc_comments)]
Enum! {DeviceModel, [
  Pro => "pro",
  Storage => "storage",
]}

impl DeviceModel {
  pub fn as_user_facing_str(&self) -> &str {
    match self {
      DeviceModel::Pro => "Pro",
      DeviceModel::Storage => "Storage",
    }
  }
}

impl From<DeviceModel> for nitrokey::Model {
  fn from(model: DeviceModel) -> nitrokey::Model {
    match model {
      DeviceModel::Pro => nitrokey::Model::Pro,
      DeviceModel::Storage => nitrokey::Model::Storage,
    }
  }
}

/// A top-level command for nitrocli.
#[allow(unused_doc_comments)]
Command! {Command, [
  /// Reads or writes the device configuration
  Config(ConfigArgs) => |ctx, args: ConfigArgs| args.subcmd.execute(ctx),
  /// Interacts with the device's encrypted volume
  Encrypted(EncryptedArgs) => |ctx, args: EncryptedArgs| args.subcmd.execute(ctx),
  /// Interacts with the device's hidden volume
  Hidden(HiddenArgs) => |ctx, args: HiddenArgs| args.subcmd.execute(ctx),
  /// Lists the attached Nitrokey devices
  List(ListArgs) => |ctx, args: ListArgs| crate::commands::list(ctx, args.no_connect),
  /// Locks the connected Nitrokey device
  Lock => crate::commands::lock,
  /// Accesses one-time passwords
  Otp(OtpArgs) => |ctx, args: OtpArgs| args.subcmd.execute(ctx),
  /// Manages the Nitrokey PINs
  Pin(PinArgs) => |ctx, args: PinArgs| args.subcmd.execute(ctx),
  /// Accesses the password safe
  Pws(PwsArgs) => |ctx, args: PwsArgs| args.subcmd.execute(ctx),
  /// Performs a factory reset
  Reset => crate::commands::reset,
  /// Prints the status of the connected Nitrokey device
  Status => crate::commands::status,
  /// Interacts with the device's unencrypted volume
  Unencrypted(UnencryptedArgs) => |ctx, args: UnencryptedArgs| args.subcmd.execute(ctx),
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct ConfigArgs {
  #[structopt(subcommand)]
  subcmd: ConfigCommand,
}

Command! {ConfigCommand, [
  /// Prints the Nitrokey configuration
  Get => crate::commands::config_get,
  /// Changes the Nitrokey configuration
  Set(ConfigSetArgs) => crate::commands::config_set,
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct ConfigSetArgs {
  /// Sets the numlock option to the given HOTP slot
  #[structopt(short = "n", long)]
  pub numlock: Option<u8>,
  /// Unsets the numlock option
  #[structopt(short = "N", long, conflicts_with("numlock"))]
  pub no_numlock: bool,
  /// Sets the capslock option to the given HOTP slot
  #[structopt(short = "c", long)]
  pub capslock: Option<u8>,
  /// Unsets the capslock option
  #[structopt(short = "C", long, conflicts_with("capslock"))]
  pub no_capslock: bool,
  /// Sets the scrollock option to the given HOTP slot
  #[structopt(short = "s", long)]
  pub scrollock: Option<u8>,
  /// Unsets the scrollock option
  #[structopt(short = "S", long, conflicts_with("scrollock"))]
  pub no_scrollock: bool,
  /// Requires the user PIN to generate one-time passwords
  #[structopt(short = "o", long)]
  pub otp_pin: bool,
  /// Allows one-time password generation without PIN
  #[structopt(short = "O", long, conflicts_with("otp-pin"))]
  pub no_otp_pin: bool,
}

#[derive(Clone, Copy, Debug)]
pub enum ConfigOption<T> {
  Enable(T),
  Disable,
  Ignore,
}

impl<T> ConfigOption<T> {
  pub fn try_from(disable: bool, value: Option<T>, name: &'static str) -> Result<Self, String> {
    if disable {
      if value.is_some() {
        Err(format!(
          "--{name} and --no-{name} are mutually exclusive",
          name = name
        ))
      } else {
        Ok(ConfigOption::Disable)
      }
    } else {
      match value {
        Some(value) => Ok(ConfigOption::Enable(value)),
        None => Ok(ConfigOption::Ignore),
      }
    }
  }

  pub fn or(self, default: Option<T>) -> Option<T> {
    match self {
      ConfigOption::Enable(value) => Some(value),
      ConfigOption::Disable => None,
      ConfigOption::Ignore => default,
    }
  }
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct EncryptedArgs {
  #[structopt(subcommand)]
  subcmd: EncryptedCommand,
}

Command! {EncryptedCommand, [
  /// Closes the encrypted volume on a Nitrokey Storage
  Close => crate::commands::encrypted_close,
  /// Opens the encrypted volume on a Nitrokey Storage
  Open => crate::commands::encrypted_open,
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct HiddenArgs {
  #[structopt(subcommand)]
  subcmd: HiddenCommand,
}

Command! {HiddenCommand, [
  /// Closes the hidden volume on a Nitrokey Storage
  Close => crate::commands::hidden_close,
  /// Creates a hidden volume on a Nitrokey Storage
  Create(HiddenCreateArgs) => |ctx, args: HiddenCreateArgs| {
    crate::commands::hidden_create(ctx, args.slot, args.start, args.end)
  },
  /// Opens the hidden volume on a Nitrokey Storage
  Open => crate::commands::hidden_open,
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct HiddenCreateArgs {
  /// The hidden volume slot to use
  pub slot: u8,
  /// The start location of the hidden volume as a percentage of the encrypted volume's size (0-99)
  pub start: u8,
  /// The end location of the hidden volume as a percentage of the encrypted volume's size (1-100)
  pub end: u8,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct ListArgs {
  /// Only print the information that is available without connecting to a device
  #[structopt(short, long)]
  pub no_connect: bool,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct OtpArgs {
  #[structopt(subcommand)]
  subcmd: OtpCommand,
}

Command! {OtpCommand, [
  /// Clears a one-time password slot
  Clear(OtpClearArgs) => |ctx, args: OtpClearArgs| {
    crate::commands::otp_clear(ctx, args.slot, args.algorithm)
  },
  /// Generates a one-time password
  Get(OtpGetArgs) => |ctx, args: OtpGetArgs| {
    crate::commands::otp_get(ctx, args.slot, args.algorithm, args.time)
  },
  /// Configures a one-time password slot
  Set(OtpSetArgs) => crate::commands::otp_set,
  /// Prints the status of the one-time password slots
  Status(OtpStatusArgs) => |ctx, args: OtpStatusArgs| crate::commands::otp_status(ctx, args.all),
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct OtpClearArgs {
  /// The OTP algorithm to use
  #[structopt(short, long, default_value = OtpAlgorithm::Totp.as_ref(),
              possible_values = &OtpAlgorithm::all_str())]
  pub algorithm: OtpAlgorithm,
  /// The OTP slot to clear
  pub slot: u8,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct OtpGetArgs {
  /// The OTP algorithm to use
  #[structopt(short, long, default_value = OtpAlgorithm::Totp.as_ref(),
              possible_values = &OtpAlgorithm::all_str())]
  pub algorithm: OtpAlgorithm,
  /// The time to use for TOTP generation (Unix timestamp) [default: system time]
  #[structopt(short, long)]
  pub time: Option<u64>,
  /// The OTP slot to use
  pub slot: u8,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct OtpSetArgs {
  /// The OTP algorithm to use
  #[structopt(short, long, default_value = OtpAlgorithm::Totp.as_ref(),
              possible_values = &OtpAlgorithm::all_str())]
  pub algorithm: OtpAlgorithm,
  /// The number of digits to use for the one-time password
  #[structopt(short, long, default_value = OtpMode::SixDigits.as_ref(),
              possible_values = &OtpMode::all_str())]
  pub digits: OtpMode,
  /// The counter value for HOTP
  #[structopt(short, long, default_value = "0")]
  pub counter: u64,
  /// The time window for TOTP
  #[structopt(short, long, default_value = "30")]
  pub time_window: u16,
  /// The format of the secret
  #[structopt(short, long, default_value = OtpSecretFormat::Hex.as_ref(),
              possible_values = &OtpSecretFormat::all_str())]
  pub format: OtpSecretFormat,
  /// The OTP slot to use
  pub slot: u8,
  /// The name of the slot
  pub name: String,
  /// The secret to store on the slot as a hexadecimal string (or in the format set with the
  /// --format option)
  pub secret: String,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct OtpStatusArgs {
  /// Shows slots that are not programmed
  #[structopt(short, long)]
  pub all: bool,
}

Enum! {OtpAlgorithm, [
  Hotp => "hotp",
  Totp => "totp",
]}

Enum! {OtpMode, [
  SixDigits => "6",
  EightDigits => "8",
]}

impl From<OtpMode> for nitrokey::OtpMode {
  fn from(mode: OtpMode) -> Self {
    match mode {
      OtpMode::SixDigits => nitrokey::OtpMode::SixDigits,
      OtpMode::EightDigits => nitrokey::OtpMode::EightDigits,
    }
  }
}

Enum! {OtpSecretFormat, [
  Ascii => "ascii",
  Base32 => "base32",
  Hex => "hex",
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PinArgs {
  #[structopt(subcommand)]
  subcmd: PinCommand,
}

Command! {PinCommand, [
  /// Clears the cached PINs
  Clear => crate::commands::pin_clear,
  /// Changes a PIN
  Set(PinSetArgs) => |ctx, args: PinSetArgs| crate::commands::pin_set(ctx, args.pintype),
  /// Unblocks and resets the user PIN
  Unblock => crate::commands::pin_unblock,
]}

/// PIN type requested from pinentry.
///
/// The available PIN types correspond to the PIN types used by the Nitrokey devices:  user and
/// admin.
#[allow(unused_doc_comments)]
Enum! {PinType, [
  Admin => "admin",
  User => "user",
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PinSetArgs {
  /// The PIN type to change
  #[structopt(name = "type", possible_values = &PinType::all_str())]
  pub pintype: PinType,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PwsArgs {
  #[structopt(subcommand)]
  subcmd: PwsCommand,
}

Command! {PwsCommand, [
  /// Clears a password safe slot
  Clear(PwsClearArgs) => |ctx, args: PwsClearArgs| crate::commands::pws_clear(ctx, args.slot),
  /// Reads a password safe slot
  Get(PwsGetArgs) => |ctx, args: PwsGetArgs| {
    crate::commands::pws_get(ctx, args.slot, args.name, args.login, args.password, args.quiet)
  },
  /// Writes a password safe slot
  Set(PwsSetArgs) => |ctx, args: PwsSetArgs| {
    crate::commands::pws_set(ctx, args.slot, &args.name, &args.login, &args.password)
  },
  /// Prints the status of the password safe slots
  Status(PwsStatusArgs) => |ctx, args: PwsStatusArgs| crate::commands::pws_status(ctx, args.all),
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PwsClearArgs {
  /// The PWS slot to clear
  pub slot: u8,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PwsGetArgs {
  /// Shows the name stored on the slot
  #[structopt(short, long)]
  pub name: bool,
  /// Shows the login stored on the slot
  #[structopt(short, long)]
  pub login: bool,
  /// Shows the password stored on the slot
  #[structopt(short, long)]
  pub password: bool,
  /// Prints the stored data without description
  #[structopt(short, long)]
  pub quiet: bool,
  /// The PWS slot to read
  pub slot: u8,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PwsSetArgs {
  /// The PWS slot to write
  pub slot: u8,
  /// The name to store on the slot
  pub name: String,
  /// The login to store on the slot
  pub login: String,
  /// The password to store on the slot
  pub password: String,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct PwsStatusArgs {
  /// Shows slots that are not programmed
  #[structopt(short, long)]
  pub all: bool,
}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct UnencryptedArgs {
  #[structopt(subcommand)]
  subcmd: UnencryptedCommand,
}

Command! {UnencryptedCommand, [
  /// Changes the configuration of the unencrypted volume on a Nitrokey Storage
  Set(UnencryptedSetArgs) => |ctx, args: UnencryptedSetArgs| {
    crate::commands::unencrypted_set(ctx, args.mode)
  },
]}

#[derive(Debug, PartialEq, structopt::StructOpt)]
pub struct UnencryptedSetArgs {
  /// The mode to change to
  #[structopt(name = "type", possible_values = &UnencryptedVolumeMode::all_str())]
  pub mode: UnencryptedVolumeMode,
}

Enum! {UnencryptedVolumeMode, [
  ReadWrite => "read-write",
  ReadOnly => "read-only",
]}