//! Generate a bcrypt hash for a plaintext password and optionally patch
//! the `web_password` field in `users.toml` in place.
//!
//! Modes:
//!   hash-password                           # prompt silently, print hash
//!   hash-password <password>                # print hash for given password
//!   hash-password --user <slug> [--file PATH]
//!                                           # prompt silently, patch file in place
//!
//! The file patch uses toml_edit to preserve comments, ordering, and
//! formatting, and writes atomically (tmp file + rename).

use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();

    match parse_args(&args) {
        Ok(Mode::PrintHash { password }) => {
            let pw = match password {
                Some(p) => p,
                None => match read_password_twice() {
                    Ok(p) => p,
                    Err(e) => {
                        eprintln!("{e}");
                        return ExitCode::from(1);
                    }
                },
            };
            println!("{}", hash(&pw));
            ExitCode::SUCCESS
        }
        Ok(Mode::PatchFile { slug, file }) => match read_password_twice() {
            Ok(pw) => match patch_users_toml(&file, &slug, &hash(&pw)) {
                Ok(()) => {
                    eprintln!(
                        "Updated web_password for slug \"{slug}\" in {}",
                        file.display()
                    );
                    ExitCode::SUCCESS
                }
                Err(e) => {
                    eprintln!("Failed to patch {}: {e}", file.display());
                    ExitCode::from(1)
                }
            },
            Err(e) => {
                eprintln!("{e}");
                ExitCode::from(1)
            }
        },
        Err(msg) => {
            eprintln!("{msg}");
            eprintln!();
            print_usage();
            ExitCode::from(2)
        }
    }
}

enum Mode {
    PrintHash { password: Option<String> },
    PatchFile { slug: String, file: PathBuf },
}

fn parse_args(args: &[String]) -> Result<Mode, String> {
    let mut slug: Option<String> = None;
    let mut file: Option<PathBuf> = None;
    let mut positional: Option<String> = None;

    let mut i = 0;
    while i < args.len() {
        match args[i].as_str() {
            "--user" => {
                i += 1;
                slug = Some(args.get(i).ok_or("--user requires a value")?.clone());
            }
            "--file" => {
                i += 1;
                file = Some(PathBuf::from(args.get(i).ok_or("--file requires a value")?));
            }
            "-h" | "--help" => return Err("help".into()),
            other if other.starts_with("--") => {
                return Err(format!("unknown flag: {other}"));
            }
            _ => {
                if positional.is_some() {
                    return Err("unexpected extra positional argument".into());
                }
                positional = Some(args[i].clone());
            }
        }
        i += 1;
    }

    match (slug, positional) {
        (Some(_), Some(_)) => Err("--user cannot be combined with a positional password".into()),
        (Some(slug), None) => Ok(Mode::PatchFile {
            slug,
            file: file.unwrap_or_else(|| PathBuf::from("users.toml")),
        }),
        (None, pw) => Ok(Mode::PrintHash { password: pw }),
    }
}

fn print_usage() {
    eprintln!("Usage:");
    eprintln!("  hash-password                           # prompt silently, print hash");
    eprintln!("  hash-password <password>                # print hash for given password");
    eprintln!("  hash-password --user <slug> [--file PATH]");
    eprintln!("                                          # prompt silently, patch users.toml");
}

fn hash(password: &str) -> String {
    bcrypt::hash(password, bcrypt::DEFAULT_COST).expect("bcrypt hashing failed")
}

fn read_password_twice() -> Result<String, String> {
    let a = rpassword::prompt_password("Password: ")
        .map_err(|e| format!("Failed to read password: {e}"))?;
    let b = rpassword::prompt_password("Repeat:   ")
        .map_err(|e| format!("Failed to read password: {e}"))?;
    if a != b {
        return Err("Passwords do not match".into());
    }
    if a.is_empty() {
        return Err("Password must not be empty".into());
    }
    Ok(a)
}

/// Patch `web_password` for the `[[user]]` entry whose `slug` matches.
/// Preserves comments and formatting via `toml_edit`. Writes atomically.
fn patch_users_toml(path: &Path, slug: &str, new_hash: &str) -> Result<(), String> {
    let content = std::fs::read_to_string(path).map_err(|e| format!("read: {e}"))?;
    let mut doc: toml_edit::DocumentMut = content.parse().map_err(|e| format!("parse: {e}"))?;

    let users = doc
        .get_mut("user")
        .and_then(|u| u.as_array_of_tables_mut())
        .ok_or_else(|| "No [[user]] array in file".to_string())?;

    let mut found = false;
    for table in users.iter_mut() {
        let matches = table.get("slug").and_then(|v| v.as_str()) == Some(slug);
        if matches {
            table["web_password"] = toml_edit::value(new_hash);
            found = true;
            break;
        }
    }
    if !found {
        return Err(format!("No [[user]] entry with slug \"{slug}\""));
    }

    // Atomic write: tmp file in same dir, then rename.
    let tmp = path.with_extension("toml.tmp");
    {
        let mut f = std::fs::File::create(&tmp).map_err(|e| format!("create tmp: {e}"))?;
        f.write_all(doc.to_string().as_bytes())
            .map_err(|e| format!("write tmp: {e}"))?;
        f.sync_all().map_err(|e| format!("sync tmp: {e}"))?;
    }
    std::fs::rename(&tmp, path).map_err(|e| format!("rename: {e}"))?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    const SAMPLE: &str = r#"# header comment

[[user]]
slug = "dr_a"
api_key = "key-a"
web_password = "$2b$12$OLD"
role = "doctor"

[[user]]
slug = "dr_b"
api_key = "key-b"
web_password = "$2b$12$KEEP"
role = "doctor"
"#;

    fn tmpfile(content: &str) -> PathBuf {
        let p = std::env::temp_dir().join(format!(
            "hp-test-{}-{}.toml",
            std::process::id(),
            uuid::Uuid::new_v4()
        ));
        std::fs::write(&p, content).unwrap();
        p
    }

    #[test]
    fn patches_matching_user_and_keeps_others() {
        let p = tmpfile(SAMPLE);
        patch_users_toml(&p, "dr_a", "$2b$12$NEW").unwrap();
        let out = std::fs::read_to_string(&p).unwrap();
        assert!(out.contains(r#"web_password = "$2b$12$NEW""#), "got: {out}");
        assert!(
            out.contains(r#"web_password = "$2b$12$KEEP""#),
            "dr_b changed: {out}"
        );
        assert!(out.contains("# header comment"), "comment lost: {out}");
        std::fs::remove_file(&p).ok();
    }

    #[test]
    fn rejects_unknown_slug() {
        let p = tmpfile(SAMPLE);
        let err = patch_users_toml(&p, "ghost", "$2b$12$X").unwrap_err();
        assert!(err.contains("ghost"), "got: {err}");
        // File must be untouched on error.
        assert_eq!(std::fs::read_to_string(&p).unwrap(), SAMPLE);
        std::fs::remove_file(&p).ok();
    }
}
