use clap::Parser;
use keepassxc_proxy_lib::{Connection, Error, KeePassXCErrorCode};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;

mod filter;
use filter::{apply_filters, parse_filters};

#[derive(Parser)]
#[command(name = "keepassxc-proxy-getpw")]
#[command(version)]
#[command(about = "Get passwords from a running KeePassXC instance")]
struct Args {
    /// URL or search term to get credentials for
    url: String,

    /// Print username instead of password
    #[arg(short, long)]
    username: bool,

    /// Filter entries (field=value for exact match, field~value for contains).
    /// Can be specified multiple times. Fields: name, login, uuid, group
    #[arg(short, long, value_name = "FILTER")]
    filter: Vec<String>,

    /// Print all matched entries as JSON array
    #[arg(short, long)]
    json: bool,

    /// Do not wait for database unlock and fail immediately if database is locked
    #[arg(long)]
    no_wait_unlock: bool,

    /// Path to association file (default: ~/.config/keepassxc-proxy-getpw/association.json)
    #[arg(short, long, value_name = "PATH")]
    association_file: Option<PathBuf>,

    /// Force creation of a new association (will prompt in `KeePassXC`)
    #[arg(long)]
    new_association: bool,

    /// Custom socket path for `KeePassXC` proxy
    #[arg(long, value_name = "PATH")]
    socket: Option<String>,
}

fn get_default_association_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("keepassxc-proxy-getpw")
        .join("association.json")
}

fn load_association(path: &PathBuf) -> Option<keepassxc_proxy_lib::Association> {
    fs::read_to_string(path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
}

fn save_association(
    path: &PathBuf,
    assoc: &keepassxc_proxy_lib::Association,
) -> Result<(), std::io::Error> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
        }
    }

    let json = serde_json::to_string_pretty(assoc)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        let mut file = fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600)
            .open(path)?;
        file.write_all(json.as_bytes())?;
    }

    #[cfg(not(unix))]
    {
        fs::write(path, json)?;
    }

    Ok(())
}

fn is_database_locked(err: &Error) -> bool {
    matches!(err, Error::KeePassXC(KeePassXCErrorCode::DatabaseNotOpened))
}

fn handle_locked_database(conn: &mut Connection, should_wait: bool) -> Result<(), Error> {
    if !should_wait {
        return Err(Error::KeePassXC(KeePassXCErrorCode::DatabaseNotOpened));
    }
    eprintln!("Waiting for database unlock...");
    conn.wait_for_unlock()
}

fn run(args: Args) -> Result<(), Error> {
    let filters = parse_filters(&args.filter)
        .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)))?;

    let assoc_path = args
        .association_file
        .unwrap_or_else(get_default_association_path);

    let should_wait = !args.no_wait_unlock;

    let mut conn = Connection::new();
    conn.connect(args.socket.as_deref())?;

    let mut need_associate = args.new_association;

    if !need_associate {
        if let Some(assoc) = load_association(&assoc_path) {
            if let Err(e) = conn.load_association(&assoc) {
                eprintln!("Warning: failed to load association: {e}");
                need_associate = true;
            } else {
                match conn.test_associate(should_wait) {
                    Ok(true) => {}
                    Err(e) if is_database_locked(&e) => {
                        handle_locked_database(&mut conn, should_wait)?;
                        // Re-test association after unlock
                        if !conn.test_associate(false).unwrap_or(false) {
                            eprintln!("Warning: stored association is no longer valid");
                            need_associate = true;
                        }
                    }
                    Ok(false) | Err(_) => {
                        eprintln!("Warning: stored association is no longer valid");
                        need_associate = true;
                    }
                }
            }
        } else {
            need_associate = true;
        }
    }

    if need_associate {
        if let Err(e) = conn.get_databasehash() {
            if is_database_locked(&e) {
                handle_locked_database(&mut conn, should_wait)?;
            } else {
                return Err(e);
            }
        }

        eprintln!("Creating new association (check KeePassXC for prompt)...");
        conn.associate()?;
        let assoc = conn.dump_association()?;
        save_association(&assoc_path, &assoc).map_err(Error::Io)?;
        eprintln!("Association saved to {}", assoc_path.display());
    }

    let logins = apply_filters(conn.get_logins(&args.url)?, &filters);

    if args.json {
        let json_str = serde_json::to_string_pretty(&logins)?;
        print!("{json_str}");
        return Ok(());
    }

    if logins.is_empty() {
        // this should not ever happen, since keepassxc should send an error we catch earlier
        return Err(Error::KeePassXC(KeePassXCErrorCode::NoLoginsFound));
    }

    if logins.len() > 1 {
        eprintln!(
            "Warning: {} matches found, using first ({})",
            logins.len(),
            logins[0].name
        );
    }

    let output = if args.username {
        &logins[0].login
    } else {
        &logins[0].password
    };

    print!("{output}");

    Ok(())
}

fn main() -> ExitCode {
    let args = Args::parse();

    match run(args) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("Error: {e}");
            ExitCode::FAILURE
        }
    }
}
