use crate::db::Database;
use crate::identity::IdKeyData;
use anyhow::bail;
use pico_args::Arguments;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::process::{Command, exit};
use std::str::FromStr;
use tempfile::NamedTempFile;

#[derive(Debug)]
enum Mode {
    Encrypt,
    Decrypt,
}

pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
    let verbose = pargs.contains(["-v", "--verbose"]);
    let contains_encrypt = pargs.contains(["-e", "--encrypt"]);
    let contains_decrypt = pargs.contains(["-d", "--decrypt"]);

    if contains_encrypt && contains_decrypt {
        bail!("cannot specify both --encrypt and --decrypt, pick one");
    }

    let mode = if contains_decrypt {
        Mode::Decrypt
    } else {
        Mode::Encrypt
    };
    let output: Option<PathBuf> = pargs.opt_value_from_str(["-o", "--output"])?;
    let armor = pargs.contains(["-a", "--armor"]);
    let passphrase = pargs.contains(["-p", "--passphrase"]);

    let mut recipients = vec![];
    loop {
        let maybe_r: Option<String> = pargs.opt_value_from_str(["-r", "--recipient"])?;

        if let Some(r) = maybe_r {
            recipients.push(r);
        } else {
            break;
        }
    }

    if let Ok(Some(f)) = pargs.opt_value_from_str::<[&str; 2], PathBuf>(["-R", "--recipients-file"])
    {
        recipients.extend(load_keyfile(&f)?);
    }

    let mut identities = vec![];
    loop {
        let maybe_i: Option<String> = pargs.opt_value_from_str(["-i", "--identity"])?;

        if let Some(i) = maybe_i {
            let Ok(path) = PathBuf::from_str(&i);
            if path.exists() {
                identities.extend(load_keyfile(&path)?);
            } else {
                identities.push(i);
            }
        } else {
            break;
        }
    }

    let input: Option<PathBuf> = pargs.opt_free_from_str()?;

    if verbose {
        println!(
            "resolved args: {:?} output={:?} armor?{:?} passphrase?{:?} recipients=>{:?} identities=>{:?} input={:?}",
            mode, output, armor, passphrase, recipients, identities, input
        );
    }

    // Resolve recipients and identities to the database
    let db = Database::load_or_create(db_path)?;

    // resolve recipients
    let resolved_recipients = recipients
        .iter()
        .map(|u| {
            // is this already a valid key?
            if u.starts_with("age1") {
                // regular ol' pubkey
                return u.clone();
            }
            // otherwise, resolve it in the database
            let results = db.fuzzy_search(u.clone());
            if results.is_empty() {
                eprintln!("no results for recipient {u}");
                exit(1);
            }
            if results.len() > 1 {
                eprintln!("recipient {u} is ambiguous, please narrow it down");
                exit(1);
            }
            let r = &results[0];

            if verbose {
                eprintln!(
                    "resolved rcpt {u} => {:?} (keyid {})",
                    r,
                    r.keys.keyid().unwrap()
                );
            }

            r.keys.pk().unwrap()
        })
        .collect::<Vec<_>>();
    // resolve identities
    let mut resolved_identities = identities.iter()
        .map(|u| {
            // is this already a valid key?
            if u.starts_with("AGE-SECRET-KEY") {
                // regular ol' sk
                return u.clone()
            }
            // otherwise, resolve it in the database
            let results = db.fuzzy_search(u.clone());
            if results.is_empty() {
                eprintln!("no results for identity {u}");
                exit(1);
            }
            if results.len() > 1 {
                eprintln!("identity {u} is ambiguous, please narrow it down");
                exit(1);
            }
            let r = &results[0];

            if verbose {
                eprintln!("resolved id {u} => {:?} (keyid {})", r, r.keys.keyid().unwrap());
            }

            match &r.keys {
                IdKeyData::Local(sk) => sk.clone(),
                IdKeyData::Peer(_) => {
                    eprintln!("resolved identity is a peer key, not a local key. perhaps revise your search terms?");
                    exit(1);
                }
            }
        })
        .collect::<Vec<_>>();

    if matches!(mode, Mode::Decrypt) && resolved_identities.is_empty() {
        // include all local identities instead
        for key in &db.keys {
            if let IdKeyData::Local(sk) = &key.keys {
                resolved_identities.push(sk.into());
                if verbose {
                    eprintln!("including identity {:?}", key);
                }
            }
        }
    }

    if verbose {
        eprintln!("resolved recipients: {:#?}", resolved_recipients);
    }

    // write recipients to a temp file
    let r_tmpfile = NamedTempFile::new()?;
    std::fs::write(r_tmpfile.path(), resolved_recipients.join("\n"))?;
    let i_tmpfile = NamedTempFile::new()?;
    std::fs::write(i_tmpfile.path(), resolved_identities.join("\n"))?;

    // execute age
    let mut args = vec![];
    args.push(
        match mode {
            Mode::Encrypt => "--encrypt",
            Mode::Decrypt => "--decrypt",
        }
        .to_string(),
    );
    if let Some(o) = output {
        args.push("--output".to_string());
        args.push(o.display().to_string());
    }
    if armor {
        args.push("--armor".to_string());
    }
    if passphrase {
        args.push("--passphrase".to_string());
    }
    if !resolved_recipients.is_empty() {
        args.push("--recipients-file".to_string());
        args.push(r_tmpfile.path().display().to_string());
    }
    if !resolved_identities.is_empty() {
        args.push("--identity".to_string());
        args.push(i_tmpfile.path().display().to_string());
    }

    if let Some(i) = input {
        args.push(i.display().to_string());
    }

    if verbose {
        eprintln!("exec age {:?}", args);
    }

    let mut c = match Command::new("age").args(args).spawn() {
        Ok(c) => c,
        Err(e) if e.kind() == ErrorKind::NotFound => {
            eprintln!("{e}");
            eprintln!("is `age` installed?");
            exit(0);
        }
        Err(e) => {
            return Err(e)?;
        }
    };

    c.wait()?;

    Ok(())
}

fn load_keyfile(path: &Path) -> anyhow::Result<Vec<String>> {
    let contents = std::fs::read_to_string(path)?;
    Ok(contents
        .lines()
        .filter(|u| !u.is_empty())
        .filter(|u| !u.starts_with("#"))
        .map(|u| u.to_string())
        .collect())
}
