use crate::commands::common_opts::RepoPath;
use jiff::tz::TimeZone;
use pijul_config::author::Author;
use pijul_identity::{
    self as identity, Complete, CreateParams, choose_identity_name, fix_identities,
};
use pijul_remote as remote;

use std::io::Write;
use std::path::Path;

use anyhow::bail;
use clap::Parser;
use jiff::Timestamp;
use keyring::Entry;
use log::{info, warn};
use pijul_interaction::Confirm;
use ptree::{TreeBuilder, print_tree};

mod subcmd {
    use anyhow::{anyhow, bail};
    use clap::{ArgGroup, Parser, ValueHint};
    use jiff::Timestamp;

    fn validate_email(input: &str) -> Result<String, anyhow::Error> {
        use validator::ValidateEmail;

        if input.validate_email() {
            Ok(input.to_string())
        } else {
            bail!("Invalid email address");
        }
    }

    fn valid_name(input: &str) -> Result<(), anyhow::Error> {
        if input.is_empty() {
            bail!("Name is empty");
        } else {
            Ok(())
        }
    }

    fn name_does_not_exist(input: &str) -> Result<String, anyhow::Error> {
        valid_name(&input)?;

        if pijul_identity::Complete::load(input).is_ok() {
            bail!("Name already exists")
        } else {
            Ok(input.to_string())
        }
    }

    fn name_exists(input: &str) -> Result<String, anyhow::Error> {
        valid_name(&input)?;

        if pijul_identity::Complete::load(input).is_err() {
            bail!("Name does not exist");
        } else {
            Ok(input.to_string())
        }
    }

    fn parse_expiry(input: &str) -> Result<Timestamp, anyhow::Error> {
        let parsed_date =
            pijul_identity::parse_expiry_date(input).map_err(|_| anyhow!("Invalid date"))?;

        if parsed_date <= Timestamp::now() {
            Err(anyhow!("Date is in the past"))
        } else {
            Ok(parsed_date)
        }
    }

    #[derive(Clone, Parser, Debug)]
    #[clap(group(
        ArgGroup::new("edit_data")
            .multiple(true)
            .args(&["display_name", "email", "expiry", "username", "remote", "name", "password"]),
    ))]
    pub struct New {
        /// Do not automatically link keys with the remote
        #[clap(long = "no-link", display_order = 1)]
        pub no_link: bool,
        /// Abort rather than prompt for input
        #[clap(long = "no-prompt", requires("edit_data"), display_order = 1)]
        pub no_prompt: bool,
        /// Set the username
        #[clap(long = "username", display_order = 3, value_hint = ValueHint::Username)]
        pub username: Option<String>,
        /// Set the default remote
        #[clap(long = "remote", display_order = 3)]
        pub remote: Option<String>,
        /// Set the display name
        #[clap(long = "display-name", display_order = 3)]
        pub display_name: Option<String>,
        /// Set the email
        #[clap(long = "email", value_parser = validate_email, display_order = 3)]
        pub email: Option<String>,
        /// Set the new identity name
        #[clap(value_parser = name_does_not_exist)]
        pub name: Option<String>,
        /// Set the expiry
        #[clap(long = "expiry", value_parser = parse_expiry, display_order = 3)]
        pub expiry: Option<Timestamp>,
        /// Encrypt using a password from standard input. Requires --no-prompt
        #[clap(long = "read-password", display_order = 2, requires = "no_prompt")]
        pub password: bool,
        /// Disable the use of the system keyring.
        #[clap(long = "no-keyring")]
        pub no_keyring: bool,
    }

    #[derive(Clone, Parser, Debug)]
    #[clap(group(
        ArgGroup::new("edit_data")
            .multiple(true)
            .args(&["display_name", "email", "new_name", "expiry", "username", "remote", "password"]),
    ))]
    pub struct Edit {
        /// Set the name of the identity to edit
        #[clap(group("name"), value_parser = name_exists)]
        pub old_name: Option<String>,
        /// Do not automatically link keys with the remote
        #[clap(long = "no-link", display_order = 1)]
        pub no_link: bool,
        /// Abort rather than prompt for input
        #[clap(
            long = "no-prompt",
            requires("name"),
            requires("edit_data"),
            display_order = 1
        )]
        pub no_prompt: bool,
        /// Set the username
        #[clap(long = "username", display_order = 3, value_hint = ValueHint::Username)]
        pub username: Option<String>,
        /// Set the default remote
        #[clap(long = "remote", display_order = 3)]
        pub remote: Option<String>,
        /// Set the display name
        #[clap(long = "display-name", display_order = 3)]
        pub display_name: Option<String>,
        /// Set the email
        #[clap(long = "email", value_parser = validate_email, display_order = 3)]
        pub email: Option<String>,
        /// Set the identity name
        #[clap(long = "new-name", display_order = 3)]
        pub new_name: Option<String>,
        /// Set the expiry
        #[clap(long = "expiry", value_parser = parse_expiry, display_order = 3)]
        pub expiry: Option<Timestamp>,
        /// Encrypt using a password from standard input. Requires --no-prompt
        #[clap(long = "read-password", display_order = 2, requires = "no_prompt")]
        pub password: bool,
        /// Disable the use of the system keyring.
        #[clap(long = "no-keyring")]
        pub no_keyring: bool,
    }
}

#[derive(Clone, Parser, Debug)]
pub enum SubCommand {
    /// Create a new identity
    New(subcmd::New),
    /// Repair the identity state on disk, including migration from older versions of Pijul
    Repair {
        #[clap(long = "no-keyring")]
        no_keyring: bool,
    },
    /// Prove an identity to the server
    Prove {
        /// Set the name used to prove the identity
        #[clap(long = "name")]
        identity_name: Option<String>,
        /// Set the target server
        server: Option<String>,
        /// Disable the use of the system keyring
        #[clap(long = "no-keyring")]
        no_keyring: bool,
    },
    /// Pretty-print all valid identities on disk
    List,
    /// Edit an existing identity
    Edit(subcmd::Edit),
    /// Remove an existing identity
    #[clap(alias = "rm")]
    Remove {
        /// Set the name of the identity to remove
        #[clap(long = "name")]
        identity_name: Option<String>,
        /// Remove the matching identity without confirmation
        #[clap(long = "no-confirm")]
        no_confirm: bool,
        /// Disable the use of the system keyring.
        #[clap(long = "no-keyring")]
        no_keyring: bool,
    },
}

#[derive(Parser, Debug)]
pub struct IdentityCommand {
    #[clap(subcommand)]
    subcmd: SubCommand,
    #[clap(flatten)]
    repository_path: RepoPath,
    /// Do not verify certificates (use with caution)
    #[clap(long = "no-cert-check", short = 'k')]
    no_cert_check: bool,
}

fn unwrap_args(
    config: &pijul_config::Config,
    default: Complete,
    identity_name: Option<String>,
    login: Option<String>,
    display_name: Option<String>,
    origin: Option<String>,
    email: Option<String>,
    expiry: Option<Timestamp>,
    no_prompt: bool,
    use_keyring: bool,
    password: bool,
) -> Result<Complete, anyhow::Error> {
    let mut public_key = default.public_key.clone();
    if let Some(expiry) = expiry.as_ref() {
        public_key.expires = Some(expiry.clone());
    }

    let pw = if password {
        Some(read_password(config, no_prompt)?)
    } else {
        None
    };

    let credentials = if let Some(mut key) = default.secret_key() {
        if let Some(password) = pw.as_deref() {
            let (secret_key, _) = default.decrypt(config, use_keyring)?;
            key = secret_key.save(Some(password));
        }
        if let Some(expiry) = expiry.as_ref() {
            key.expires = Some(expiry.clone());
        }
        Some(identity::Credentials::new(key, pw))
    } else {
        None
    };

    Ok(Complete::new(
        identity_name.unwrap_or(default.name),
        identity::IdentityConfig {
            key_path: None,
            author: Author {
                username: login.unwrap_or(default.config.author.username),
                display_name: display_name.unwrap_or(default.config.author.display_name),
                email: email.unwrap_or(default.config.author.email),
                origin: origin.unwrap_or(default.config.author.origin),
                key_path: None,
            },
        },
        public_key,
        credentials,
    ))
}

fn read_password(config: &pijul_config::Config, no_prompt: bool) -> Result<String, anyhow::Error> {
    if no_prompt {
        use std::io::{BufRead, Write};

        fn read_line(prompt: &str) -> Result<String, anyhow::Error> {
            let mut stderr = std::io::stderr();
            writeln!(stderr, "{prompt}")?;
            stderr.flush()?;

            let mut input = String::new();
            let read = std::io::stdin().lock().read_line(&mut input)?;
            if read == 0 {
                anyhow::bail!("Unexpected end of standard input")
            }
            while matches!(input.chars().last(), Some('\n' | '\r')) {
                input.pop();
            }
            Ok(input)
        }

        loop {
            let password = read_line("New password")?;
            let confirmation = read_line("Confirm password")?;
            if password == confirmation {
                return Ok(password);
            }

            let mut stderr = std::io::stderr();
            writeln!(stderr, "Password mismatch")?;
            stderr.flush()?;
        }
    } else {
        Ok(pijul_interaction::Password::new(config)?
            .with_prompt("New password")
            .with_confirmation("Confirm password", "Password mismatch")
            .interact()?)
    }
}

impl IdentityCommand {
    pub fn repository_path(&self) -> Option<&Path> {
        self.repository_path.repo_path()
    }

    pub async fn run(self, config: &pijul_config::Config) -> Result<(), anyhow::Error> {
        let mut stderr = std::io::stderr();

        match self.subcmd {
            SubCommand::New(options) => {
                let identity = unwrap_args(
                    config,
                    Complete::default(config)?,
                    options.name,
                    options.username,
                    options.display_name,
                    options.remote,
                    options.email,
                    options.expiry,
                    options.no_prompt,
                    !options.no_keyring,
                    options.password,
                )?;

                identity
                    .create(
                        config,
                        CreateParams {
                            use_keyring: !options.no_keyring,
                            link_remote: !options.no_link,
                            no_prompt: options.no_prompt,
                        },
                    )
                    .await?;

                if !options.no_link {
                    if let Err(_) = remote::prove(
                        config,
                        &identity,
                        None,
                        self.no_cert_check,
                        !options.no_keyring,
                    )
                    .await
                    {
                        warn!(
                            "Could not prove identity `{}`. Please check your credentials & network connection. If you are on an enterprise network, perhaps try running with `--no-cert-check`. Your data is safe but will not be connected to {} without runnning `pijul identity prove {}`",
                            identity.name, identity.config.author.origin, identity.name
                        );
                    } else {
                        info!("Identity `{}` was proved to the server", identity);
                    }
                }
            }
            SubCommand::Repair { no_keyring } => fix_identities(config, !no_keyring).await?,
            SubCommand::Prove {
                identity_name,
                server,
                no_keyring,
            } => {
                let identity_name =
                    &identity_name.unwrap_or(choose_identity_name(config, !no_keyring).await?);
                let loaded_identity = Complete::load(identity_name)?;
                remote::prove(
                    config,
                    &loaded_identity,
                    server.as_deref(),
                    self.no_cert_check,
                    !no_keyring,
                )
                .await?;
            }
            SubCommand::List => {
                let identities = Complete::load_all()?;

                if identities.is_empty() {
                    let mut stderr = std::io::stderr();
                    writeln!(
                        stderr,
                        "No identities found. Use `pijul identity new` to create one."
                    )?;
                    writeln!(
                        stderr,
                        "If you have created a key in the past, you may need to migrate via `pijul identity repair`"
                    )?;

                    return Ok(());
                }

                let mut tree = TreeBuilder::new("Identities".to_string());
                for identity in identities {
                    tree.begin_child(identity.name.clone());

                    tree.add_empty_child(format!(
                        "Display name: {}",
                        if identity.config.author.display_name.is_empty() {
                            "<NO NAME>"
                        } else {
                            &identity.config.author.display_name
                        }
                    ));

                    tree.add_empty_child(format!(
                        "Email: {}",
                        if identity.config.author.email.is_empty() {
                            "<NO EMAIL>"
                        } else {
                            &identity.config.author.email
                        }
                    ));

                    let username = if identity.config.author.username.is_empty() {
                        "<NO USERNAME>"
                    } else {
                        &identity.config.author.username
                    };

                    let origin = if identity.config.author.origin.is_empty() {
                        "<NO ORIGIN>"
                    } else {
                        &identity.config.author.origin
                    };
                    tree.add_empty_child(format!("Login: {username}@{origin}"));

                    tree.begin_child("Public key".to_string());
                    tree.add_empty_child(format!("Key: {}", identity.public_key.key));
                    tree.add_empty_child(format!("Version: {}", identity.public_key.version));
                    tree.add_empty_child(format!(
                        "Algorithm: {:#?}",
                        identity.public_key.algorithm
                    ));
                    tree.add_empty_child(format!(
                        "Expiry: {}",
                        identity
                            .public_key
                            .expires
                            .map(|date| date.strftime("%Y-%m-%d %H:%M:%S (%Z)").to_string())
                            .unwrap_or_else(|| "Never".to_string())
                    ));
                    tree.end_child();

                    tree.begin_child("Secret key".to_string());
                    tree.add_empty_child(format!(
                        "Version: {}",
                        identity.secret_key().unwrap().version
                    ));
                    tree.add_empty_child(format!(
                        "Algorithm: {:#?}",
                        identity.secret_key().unwrap().algorithm
                    ));

                    let encryption_message =
                        if let Some(encryption) = identity.secret_key().unwrap().encryption {
                            format!(
                                "{} (Stored in keyring: {})",
                                match encryption {
                                    pijul_core::key::Encryption::Aes128(_) => "AES 128-bit",
                                },
                                keyring::Entry::new("pijul", &identity.name)?
                                    .get_password()
                                    .is_ok()
                            )
                        } else {
                            String::from("None")
                        };

                    tree.add_empty_child(format!("Encryption: {encryption_message}"));
                    tree.end_child();

                    tree.add_empty_child(format!(
                        "Last updated: {}",
                        identity
                            .last_modified
                            .to_zoned(TimeZone::system())
                            .strftime("%Y-%m-%d %H:%M:%S (%Z)")
                    ));
                    tree.end_child();
                }

                print_tree(&tree.build())?;
            }
            SubCommand::Edit(options) => {
                let old_id_name = if let Some(id_name) = options.old_name {
                    id_name
                } else {
                    choose_identity_name(config, !options.no_keyring).await?
                };
                writeln!(std::io::stderr(), "Editing identity: {old_id_name}")?;

                let old_identity = Complete::load(&old_id_name)?;
                let cli_args = unwrap_args(
                    config,
                    old_identity.clone(),
                    options.new_name,
                    options.username,
                    options.display_name,
                    options.remote,
                    options.email,
                    options.expiry,
                    options.no_prompt,
                    !options.no_keyring,
                    options.password,
                )?;

                let new_identity = if options.no_prompt {
                    cli_args
                } else {
                    cli_args
                        .prompt_changes(
                            config,
                            Some(old_identity.name.clone()),
                            !options.no_link,
                            !options.no_keyring,
                        )
                        .await?
                };

                old_identity.clone().replace_with(new_identity.clone())?;

                // There are 2 cases that require re-proving:
                // 1: new secret key
                // 2. new username/origin
                if !options.no_link {
                    if new_identity.secret_key() != old_identity.secret_key()
                        || old_identity.config.author != new_identity.config.author
                    {
                        let prove_result = remote::prove(
                            config,
                            &new_identity,
                            None,
                            self.no_cert_check,
                            !options.no_keyring,
                        )
                        .await;

                        if let Err(_) = prove_result {
                            warn!(
                                "Could not prove identity `{}`. Please check your credentials & network connection. If you are on an enterprise network, perhaps try running with `--no-cert-check`. Your data is safe but will not be connected to {} without runnning `pijul identity prove {}`",
                                new_identity.name,
                                new_identity.config.author.origin,
                                new_identity.name
                            );
                        } else {
                            info!("Identity `{}` was proved to the server", new_identity);
                        }
                    }
                }
            }
            SubCommand::Remove {
                identity_name,
                no_confirm: no_prompt,
                no_keyring,
            } => {
                if Complete::load_all()?.is_empty() {
                    writeln!(stderr, "No identities to remove!")?;

                    return Ok(());
                }

                let identity = Complete::load(
                    &identity_name.unwrap_or(choose_identity_name(config, !no_keyring).await?),
                )?;

                let path = pijul_config::global_config_directory()
                    .unwrap()
                    .join("identities")
                    .join(&identity.name);

                writeln!(stderr, "Removing identity: {identity} at {path:?}")?;

                // Ask the user to confirm
                if !no_prompt
                    && !Confirm::new(config)?
                        .with_prompt("Do you wish to continue?")
                        .with_default(false)
                        .interact()?
                {
                    bail!("User did not wish to continue");
                }

                // The user has confirmed, safe to continue
                std::fs::remove_dir_all(path)?;
                writeln!(stderr, "Identity removed.")?;

                if identity.secret_key().unwrap().encryption.is_some() {
                    if let Err(e) =
                        Entry::new("pijul", &identity.name).and_then(|x| x.delete_credential())
                    {
                        warn!("Unable to delete password: {e:?}");
                    }
                }
            }
        }

        Ok(())
    }
}