//! Commands to manage information about hosts for SSH CA tool.

use crate::cert::{parse_validity, CertificateAuthority, HostCa};
use crate::cmd::Runnable;
use crate::config::Config;
use crate::error::CAError;
use crate::info::{self, Info, SafeHost};
use crate::key::{KeyKind, KeyPair, PublicKey, SshKey};
use crate::store::{KeyStore, KeyStoreError};
use crate::util::{format_timestamp, read_as_string};

use clap::{ArgAction, Parser};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::time::SystemTime;
use time::{macros::format_description, Duration};

const DEFAULT_VALIDITY: &str = "90d";
const SHORT_TIME: Duration = Duration::hours(1);

/// Manage information about hosts.
#[derive(Debug, Parser)]
pub struct Host {
    #[clap(subcommand)]
    cmd: Subcommand,
}

impl Runnable for Host {
    fn run(&mut self, config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        match &mut self.cmd {
            Subcommand::List(x) => x.run(config, store)?,
            Subcommand::Show(x) => x.run(config, store)?,
            Subcommand::New(x) => x.run(config, store)?,
            Subcommand::Certify(x) => x.run(config, store)?,
            Subcommand::Rename(x) => x.run(config, store)?,
            Subcommand::Remove(x) => x.run(config, store)?,
            Subcommand::PublicKey(x) => x.run(config, store)?,
            Subcommand::PrivateKey(x) => x.run(config, store)?,
            Subcommand::Generate(x) => x.run(config, store)?,
            Subcommand::Regenerate(x) => x.run(config, store)?,
            Subcommand::Principals(x) => x.run(config, store)?,
        }
        Ok(())
    }
}

#[allow(missing_docs)]
#[derive(Debug, Parser)]
pub enum Subcommand {
    Certify(Certify),
    Show(Show),
    List(List),
    New(New),
    Rename(Rename),
    #[clap(alias = "delete")]
    Remove(Remove),
    PublicKey(PublicKeyCmd),
    PrivateKey(PrivateKey),
    Generate(Generate),
    Regenerate(Regenerate),
    Principals(Principals),
}

/// Show information about a single host, as JSON.
#[derive(Debug, Parser)]
pub struct Show {
    /// Name of host to show.
    name: String,
}

impl Runnable for Show {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        for host in store.hosts() {
            if host.name() == self.name {
                let json = serde_json::to_string_pretty(host).map_err(CAError::Json)?;
                println!("{json}");
                return Ok(());
            }
        }
        Err(CAError::NoSuchHost(self.name.clone()))
    }
}

/// List hosts.
#[derive(Debug, Parser)]
pub struct List {
    /// Output JSON.
    #[clap(long)]
    json: bool,
}

impl Runnable for List {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        let mut list: info::List<SafeHost> = info::List::new();
        for host in store.hosts() {
            let host = SafeHost::from(host);
            list.push(host.clone());
        }
        list.sort();
        if self.json {
            let json = serde_json::to_string_pretty(&list).map_err(CAError::Json)?;
            println!("{json}");
        } else {
            for host in list.iter() {
                println!("{}", host.name());
            }
        }
        Ok(())
    }
}

/// Add a new host by importing a public key.
#[derive(Debug, Parser)]
pub struct New {
    /// Name of host.
    hostname: String,
    /// File where public key is.
    keyfile: PathBuf,

    /// All the principals for this host. Defaults to HOSTNAME if none
    /// given.
    #[clap(short, long = "principal", action = ArgAction::Append)]
    principals: Vec<String>,
}

impl Runnable for New {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        let public = read_as_string(&self.keyfile)?;
        let public = PublicKey::new(public);
        let principals = if self.principals.is_empty() {
            vec![self.hostname.clone()]
        } else {
            self.principals.to_vec()
        };
        let host = info::Host::new(public, None, self.hostname.clone()).with_principals(principals);
        store.insert_host(host).map_err(CAError::KeyStoreError)?;
        Ok(())
    }
}

/// Certify a host's public key.
#[derive(Debug, Parser)]
pub struct Certify {
    /// Write output to this file.
    #[clap(long)]
    output: Option<PathBuf>,

    /// Set validity period.
    ///
    /// Default unit is day. Suffix 'w', 'd', 'h', or 'm' can specify
    /// weeks, days, hours, or minutes.
    #[clap(long, default_value = DEFAULT_VALIDITY)]
    expires_in: String,

    /// Name of CA.
    #[clap(long)]
    ca: String,

    /// Name of host.
    hostname: String,
}

impl Runnable for Certify {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        let ca = if let Some(ca) = store.get_host_ca(&self.ca) {
            ca
        } else {
            return Err(CAError::NoSuchHostCA(self.ca.clone()));
        };
        let host = if let Some(host) = store.get_host(&self.hostname) {
            host
        } else {
            return Err(CAError::NoSuchHost(self.hostname.clone()));
        };

        let ca = HostCa::new(self.ca.clone(), ca.keypair().clone());
        let valid_for = parse_validity(&self.expires_in)?;
        let cert = ca.certify(host.public(), &valid_for, host.principals())?;
        if let Some(output) = &self.output {
            let mut file = File::create(output).map_err(|e| CAError::Create(output.into(), e))?;
            write!(file, "{cert}").map_err(|e| CAError::Write(output.into(), e))?;
        } else {
            print!("{cert}");
        }
        Ok(())
    }
}

/// Rename host.
#[derive(Debug, Parser)]
pub struct Rename {
    /// Old name of host.
    oldname: String,

    /// New name of host.
    newname: String,
}

impl Runnable for Rename {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        store
            .rename_host(&self.oldname, &self.newname)
            .map_err(CAError::KeyStoreError)?;
        Ok(())
    }
}

/// Remove host.
#[derive(Debug, Parser)]
pub struct Remove {
    /// Name of host.
    hostname: String,
}

impl Runnable for Remove {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        store
            .remove_host(&self.hostname)
            .map_err(CAError::KeyStoreError)?;
        Ok(())
    }
}

/// Show host's stored public key.
#[derive(Debug, Parser)]
pub struct PublicKeyCmd {
    /// Name of host.
    hostname: String,

    /// Pretend it is a different time. This is for testing purposes.
    #[clap(long)]
    now: Option<String>,
}

impl PublicKeyCmd {
    fn now(&self) -> String {
        if let Some(ts) = &self.now {
            ts.into()
        } else {
            let now = SystemTime::now();
            let now = time::OffsetDateTime::from(now);

            let format = format_description!("[year]:[month]:[day]T[hour]:[minute]:[second]");

            now.format(&format).unwrap()
        }
    }
}

impl Runnable for PublicKeyCmd {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host(&self.hostname) {
            if let Some(valid_until) = host.valid_until() {
                if self.now().as_str() >= valid_until {
                    return Err(CAError::ExpiredHostKey(
                        host.name().into(),
                        valid_until.to_string(),
                    ));
                }
            }
            print!("{}", host.public().as_str());
        } else {
            return Err(CAError::NoSuchHost(self.hostname.clone()));
        }
        Ok(())
    }
}

/// Show host's stored private key, if known.
#[derive(Debug, Parser)]
pub struct PrivateKey {
    /// Name of host.
    hostname: String,
}

impl Runnable for PrivateKey {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host(&self.hostname) {
            if let Some(key) = host.private() {
                println!("{}", key.as_str());
            } else {
                return Err(CAError::NoPrivateKey(self.hostname.clone()));
            }
        } else {
            return Err(CAError::NoSuchHost(self.hostname.clone()));
        }
        Ok(())
    }
}

/// Add a new host and generate a new key pair for it.
#[derive(Debug, Parser)]
pub struct Generate {
    /// Name of host.
    hostname: String,

    /// Make the generated host key short-lived.
    #[clap(long)]
    temporary: bool,

    /// All the principals for this host. Defaults to HOSTNAME if none
    /// given.
    #[clap(short, long = "principal", action = ArgAction::Append)]
    principals: Vec<String>,
}

impl Runnable for Generate {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        let pair = KeyPair::generate(KeyKind::Ed25519).map_err(CAError::KeyError)?;
        let principals = if self.principals.is_empty() {
            vec![self.hostname.clone()]
        } else {
            self.principals.to_vec()
        };
        let mut host = info::Host::new(
            pair.public().clone(),
            Some(pair.private().clone()),
            self.hostname.clone(),
        )
        .with_principals(principals);
        if self.temporary {
            let now = std::time::SystemTime::now();
            if let Some(valid_until) = time::OffsetDateTime::from(now).checked_add(SHORT_TIME) {
                let valid_until = format_timestamp(valid_until)?;
                host.set_valid_until(valid_until);
            } else {
                return Err(CAError::ShortTime);
            };
        }
        store.insert_host(host).map_err(CAError::KeyStoreError)?;
        Ok(())
    }
}

/// Generate a new key pair for an existing host.
#[derive(Debug, Parser)]
pub struct Regenerate {
    /// Name of host.
    hostname: String,

    /// Make the generated host key short-lived.
    #[clap(long)]
    temporary: bool,

    /// All the principals for this host. Defaults to HOSTNAME if none
    /// given.
    #[clap(short, long = "principal", action = ArgAction::Append)]
    principals: Vec<String>,
}

impl Runnable for Regenerate {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host_mut(&self.hostname) {
            if host.private().is_none() {
                return Err(CAError::NoPrivateKey(self.hostname.clone()));
            }
            let pair = KeyPair::generate(KeyKind::Ed25519).map_err(CAError::KeyError)?;
            host.set_keys(pair.public().clone(), pair.private().clone());
            if self.temporary {
                let now = std::time::SystemTime::now();
                if let Some(valid_until) = time::OffsetDateTime::from(now).checked_add(SHORT_TIME) {
                    let valid_until = format_timestamp(valid_until)?;
                    host.set_valid_until(valid_until);
                } else {
                    return Err(CAError::ShortTime);
                };
            }
            Ok(())
        } else {
            Err(CAError::KeyStoreError(KeyStoreError::UnknownHost(
                self.hostname.clone(),
            )))
        }
    }
}

/// Manage a host's principals.
#[derive(Debug, Parser)]
pub struct Principals {
    #[clap(subcommand)]
    command: PrincipalsCommand,
}

impl Runnable for Principals {
    fn run(&mut self, config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        match &mut self.command {
            PrincipalsCommand::Add(x) => x.run(config, store)?,
            PrincipalsCommand::List(x) => x.run(config, store)?,
            PrincipalsCommand::Remove(x) => x.run(config, store)?,
            PrincipalsCommand::Set(x) => x.run(config, store)?,
        }
        Ok(())
    }
}

#[allow(missing_docs)]
#[derive(Debug, Parser)]
enum PrincipalsCommand {
    List(ListPrincipals),
    Set(SetPrincipals),
    Add(AddPrincipals),
    Remove(RemovePrincipals),
}

/// Set a host's principals.
#[derive(Debug, Parser)]
pub struct ListPrincipals {
    /// Which host to operate on?
    hostname: String,
}

impl Runnable for ListPrincipals {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host(&self.hostname) {
            for p in host.principals() {
                println!("{p}");
            }
            Ok(())
        } else {
            Err(CAError::KeyStoreError(KeyStoreError::UnknownHost(
                self.hostname.clone(),
            )))
        }
    }
}

/// Set a host's principals.
#[derive(Debug, Parser)]
pub struct SetPrincipals {
    /// Which host to operate on?
    #[clap(long, alias = "host")]
    hostname: String,

    /// A principal for the host.
    #[clap(value_name = "PRINCIPAL")]
    principals: Vec<String>,
}

impl Runnable for SetPrincipals {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host_mut(&self.hostname) {
            host.set_principals(&self.principals);
            Ok(())
        } else {
            Err(CAError::KeyStoreError(KeyStoreError::UnknownHost(
                self.hostname.clone(),
            )))
        }
    }
}

/// Add to a host's principals.
#[derive(Debug, Parser)]
pub struct AddPrincipals {
    /// Which host to operate on?
    #[clap(long, alias = "host")]
    hostname: String,

    /// A principal for the host.
    #[clap(value_name = "PRINCIPAL")]
    principals: Vec<String>,
}

impl Runnable for AddPrincipals {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host_mut(&self.hostname) {
            host.add_principals(&self.principals);
            assert!(store.is_dirty());
            Ok(())
        } else {
            Err(CAError::KeyStoreError(KeyStoreError::UnknownHost(
                self.hostname.clone(),
            )))
        }
    }
}

/// Remove from a host's principals.
#[derive(Debug, Parser)]
pub struct RemovePrincipals {
    /// Which host to operate on?
    #[clap(long, alias = "host")]
    hostname: String,

    /// A principal for the host.
    #[clap(value_name = "PRINCIPAL")]
    principals: Vec<String>,
}

impl Runnable for RemovePrincipals {
    fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> {
        if let Some(host) = store.get_host_mut(&self.hostname) {
            host.remove_principals(&self.principals);
            Ok(())
        } else {
            Err(CAError::KeyStoreError(KeyStoreError::UnknownHost(
                self.hostname.clone(),
            )))
        }
    }
}
