use super::load::path;
use super::Complete;
use crate::config;
use crate::config::Direction;
use crate::repository::Repository;

use std::fs;
use std::io::Write;

use anyhow::{bail, Context};
use dialoguer::{Confirm, FuzzySelect, Input};
use keyring::Entry;
use log::{debug, warn};
use once_cell::sync::OnceCell;
use thiserror::Error;
use thrussh_keys::key::PublicKey;

pub static NO_CERT_CHECK: OnceCell<bool> = OnceCell::new();

#[derive(Error, Debug)]
pub enum IdentityCreateError {
    #[error("Could not prove identity {0}. Please check your credentials & network connection. If you are on an enterprise network, perhaps try running with `--no-cert-check`")]
    ProveFailed(String),
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

impl Complete {
    /// Prompt the user to make changes to an identity, returning the new identity
    ///
    /// # Arguments
    /// * `replace_current` - If the new identity will replace an existing one
    pub async fn prompt_changes(
        &self,
        replace_current: bool,
        link_remote: bool,
    ) -> Result<Self, anyhow::Error> {
        let mut new_identity = self.clone();
        let config = config::load_theme()?;

        new_identity.name = Input::with_theme(config.as_ref())
            .with_prompt("Unique identity name")
            .default(String::from("default"))
            .allow_empty(false)
            .with_initial_text(if replace_current {
                self.name.clone()
            } else {
                String::new()
            })
            .validate_with(move |input: &String| -> Result<(), String> {
                match Self::load(input) {
                    Ok(existing_identity) => {
                        if replace_current {
                            // The user is trying to edit an existing identity
                            Ok(())
                        } else {
                            Err(format!("That identity already exists: {existing_identity}",))
                        }
                    }
                    Err(_) => Ok(()),
                }
            })
            .interact_text()?;

        new_identity.config.author.display_name = Input::with_theme(config.as_ref())
            .with_prompt("Display name")
            .allow_empty(true)
            .with_initial_text(&self.config.author.display_name)
            .interact_text()?;

        new_identity.config.author.email = Input::with_theme(config.as_ref())
            .with_prompt("Email (leave blank for none)")
            .allow_empty(true)
            .with_initial_text(&self.config.author.email)
            .validate_with(move |input: &String| -> Result<(), &str> {
                if input.is_empty() || validator::validate_email(input) {
                    Ok(())
                } else {
                    Err("Invalid email address")
                }
            })
            .interact_text()?;

        if Confirm::with_theme(config.as_ref())
            .with_prompt(&format!(
                "Do you want to change the encryption? (Current status: {})",
                self.credentials
                    .clone()
                    .unwrap()
                    .secret_key
                    .encryption
                    .map_or("not encrypted", |_| "encrypted")
            ))
            .default(false)
            .interact()?
        {
            new_identity.change_password()?;
        }

        // Update the expiry AFTER potential secret key reset
        new_identity.prompt_expiry()?;

        if Confirm::with_theme(config.as_ref())
            .with_prompt("Do you want to link this identity to a remote?")
            .default(true)
            .interact()?
        {
            new_identity.prompt_remote(link_remote).await?;
        } else {
            // The user wants an 'offline' identity, so make sure not to store login info
            new_identity.config.key_path = None;
            new_identity.config.author.username = String::new();
            new_identity.config.author.origin = String::new();
        }

        new_identity.last_modified = chrono::offset::Utc::now();

        Ok(new_identity)
    }

    async fn prompt_ssh(&mut self) -> Result<(), anyhow::Error> {
        let mut ssh_agent = thrussh_keys::agent::client::AgentClient::connect_env().await?;
        let identities = ssh_agent.request_identities().await?;
        let ssh_dir = dirs_next::home_dir().unwrap().join(".ssh");

        let selection = FuzzySelect::with_theme(config::load_theme()?.as_ref())
            .with_prompt("Select key")
            .items(
                &identities
                    .iter()
                    .map(|id| {
                        format!(
                            "{}: {} ({})",
                            id.name(),
                            id.fingerprint(),
                            ssh_dir
                                .join(match id {
                                    PublicKey::Ed25519(_) =>
                                        thrussh_keys::key::ED25519.identity_file(),
                                    PublicKey::RSA { ref hash, .. } => hash.name().identity_file(),
                                })
                                .display(),
                        )
                    })
                    .collect::<Vec<_>>(),
            )
            .default(0)
            .interact()?;

        self.config.key_path = Some(ssh_dir.join(match identities[selection] {
            PublicKey::Ed25519(_) => thrussh_keys::key::ED25519.identity_file(),
            PublicKey::RSA { ref hash, .. } => hash.name().identity_file(),
        }));

        Ok(())
    }

    async fn prompt_remote(&mut self, link_remote: bool) -> Result<(), IdentityCreateError> {
        let config = config::load_theme()?;

        self.config.author.username = Input::with_theme(config.as_ref())
            .with_prompt("Remote username")
            .default(whoami::username())
            .with_initial_text(&self.config.author.username)
            .interact_text()
            .unwrap();

        self.config.author.origin = Input::with_theme(config.as_ref())
            .with_prompt("Remote URL")
            .with_initial_text(&self.config.author.origin)
            .default(String::from("ssh.pijul.com"))
            .interact_text()
            .unwrap();

        if Confirm::with_theme(config.as_ref())
            .with_prompt(&format!(
                "Do you want to change the default SSH key? (Current key: {})",
                if let Some(path) = &self.config.key_path {
                    format!("{path:#?}")
                } else {
                    String::from("none")
                }
            ))
            .default(false)
            .interact()
            .unwrap()
        {
            self.prompt_ssh().await?;
        }

        // Prove the identity to the server
        if link_remote
            && self
                .prove(*NO_CERT_CHECK.get_or_init(|| false))
                .await
                .is_err()
        {
            return Err(IdentityCreateError::ProveFailed(self.name.clone()));
        }

        Ok(())
    }

    fn prompt_expiry(&mut self) -> Result<(), anyhow::Error> {
        let config = config::load_theme()?;

        let expiry_message = self
            .public_key
            .expires
            .map(|date| date.format("%Y-%m-%d %H:%M:%S").to_string());

        self.public_key.expires = if Confirm::with_theme(config.as_ref())
            .with_prompt(format!(
                "Do you want this key to expire? (Current expiry: {})",
                expiry_message
                    .clone()
                    .unwrap_or_else(|| String::from("never"))
            ))
            .default(false)
            .interact()?
        {
            let time_stamp: String = Input::with_theme(config.as_ref())
                .with_prompt("Expiry date (YYYY-MM-DD)")
                .with_initial_text(expiry_message.unwrap_or_default())
                .validate_with(move |input: &String| -> Result<(), &str> {
                    let parsed_date = dateparser::parse_with_timezone(input, &chrono::offset::Utc);
                    if parsed_date.is_err() {
                        return Err("Invalid date");
                    }

                    let date = parsed_date.unwrap();
                    if chrono::offset::Utc::now().timestamp_millis() > date.timestamp_millis() {
                        Err("Date is in the past")
                    } else {
                        Ok(())
                    }
                })
                .interact_text()?;

            Some(dateparser::parse_with_timezone(
                &time_stamp,
                &chrono::offset::Utc,
            )?)
        } else {
            None
        };

        Ok(())
    }

    /// Write an identity to disk.
    ///
    /// # Arguments
    /// * `replace_current` - If the new identity will replace an existing one
    fn write(&self, replace_current: bool) -> Result<(), anyhow::Error> {
        if let Ok(existing_identity) = Self::load(&self.name) {
            if !replace_current {
                bail!("An identity with that name already exists: {existing_identity}");
            }
        }

        // Write the relevant identity files
        let identity_dir = path(&self.name, false)?;

        std::fs::create_dir_all(&identity_dir)?;

        let config_data = toml::to_string_pretty(&self)?;
        let mut config_file = std::fs::File::create(identity_dir.join("identity.toml"))?;
        config_file.write_all(config_data.as_bytes())?;

        let key_data = serde_json::to_string_pretty(&self.secret_key())?;
        let mut key_file = super::load::open_secret_file(&identity_dir.join("secret_key.json"))?;
        key_file.write_all(key_data.as_bytes())?;

        Ok(())
    }

    /// Associate a generated key with a remote identity. Patches authored
    /// by unproven keys will only display the key as the author.
    pub async fn prove(&self, no_cert_check: bool) -> Result<(), anyhow::Error> {
        let mut stderr = std::io::stderr();
        writeln!(
            stderr,
            "Linking identity {}@{}",
            &self.config.author.username, &self.config.author.origin
        )?;

        let remote = self.config.author.origin.clone();
        let mut remote = if let Ok(repo) = Repository::find_root(None) {
            use crate::remote::RemoteRepo;
            if let RemoteRepo::Ssh(ssh) = repo
                .remote(
                    None,
                    &remote,
                    crate::DEFAULT_CHANNEL,
                    Direction::Pull,
                    no_cert_check,
                    false,
                )
                .await?
            {
                ssh
            } else {
                bail!("No such remote: {}", remote)
            }
        } else if let Some(mut ssh) = crate::remote::ssh::ssh_remote(&remote, false) {
            if let Some(c) = ssh.connect(&remote, crate::DEFAULT_CHANNEL).await? {
                c
            } else {
                bail!("No such remote: {}", remote)
            }
        } else {
            bail!("No such remote: {}", remote)
        };

        let (key, _password) = self.credentials.clone().unwrap().decrypt(&self.name)?;
        remote.prove(key).await?;

        Ok(())
    }

    /// Create a complete identity, including writing to disk & exchanging key with origin.
    ///
    /// # Arguments
    /// * `no_prompt` - If the user should not be prompted for input.
    /// * `link_remote` - Override if the identity should be exchanged with the remote.
    pub async fn create(
        &self,
        no_prompt: bool,
        link_remote: bool,
    ) -> Result<(), IdentityCreateError> {
        // Prompt the user to edit changes interactively
        let confirmed_identity = if no_prompt {
            self.clone()
        } else {
            self.prompt_changes(false, link_remote).await?
        };

        confirmed_identity.write(false)?;

        Ok(())
    }

    /// Replace an existing identity with a new one.
    ///
    /// # Arguments
    /// * `new_identity` - The new identity that will be created
    pub fn replace_with(self, new_identity: Self) -> Result<Self, anyhow::Error> {
        // Remove the old data
        let old_identity_path = path(&self.name, true)?;
        debug!("Removing old directory: {old_identity_path:?}");
        fs::remove_dir_all(old_identity_path).context("Could not remove old identity.")?;

        // Write the new data
        new_identity.write(true)?;

        // Remove the old keychain entry if name has changed
        if self.name != new_identity.name {
            if let Err(e) = Entry::new("pijul", &self.name).delete_password() {
                warn!("Unable to delete password: {e:?}");
            }
        }

        // Update the password
        if let Some(password) = new_identity.credentials.clone().unwrap().password.get() {
            Entry::new("pijul", &new_identity.name).set_password(&password)?;
        } else if let Err(e) = Entry::new("pijul", &new_identity.name).delete_password() {
            warn!("Unable to delete password: {e:?}");
        }

        Ok(new_identity)
    }
}