//! Shared per-type item construction + interactive editing for both the
//! personal vault (`commands/add.rs`, `commands/edit.rs`) and the org vault
//! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use zeroize::Zeroizing;

use relicario_core::item::FieldHistoryEntry;
use relicario_core::item_types::{CardKind, TotpAlgorithm};
use relicario_core::time::now_unix;
use relicario_core::{EncryptedAttachment, FieldId, Item, ItemCore};

use crate::parse::base32_decode_lenient;
use crate::prompt::{prompt_keep_opt, prompt_secret, prompt_yesno};

pub(crate) type FieldHistory = HashMap<FieldId, Vec<FieldHistoryEntry>>;

/// Resolve a single-line secret: from stdin when `from_stdin`, else an
/// interactive masked prompt (which honours `RELICARIO_TEST_ITEM_SECRET`).
pub(crate) fn resolve_secret_line(from_stdin: bool, label: &str) -> Result<String> {
    if from_stdin {
        let mut s = String::new();
        std::io::stdin().read_line(&mut s)?;
        Ok(s.trim_end_matches(['\n', '\r']).to_string())
    } else {
        crate::prompt::prompt_secret(&format!("{label}: "))
    }
}

/// Resolve a multiline secret (key material, note body). Both paths read stdin
/// to EOF; the interactive path first prints `hint` to stderr.
pub(crate) fn resolve_secret_multiline(from_stdin: bool, hint: &str) -> Result<String> {
    if !from_stdin {
        eprintln!("{hint}");
    }
    let mut s = String::new();
    std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?;
    Ok(s)
}

pub(crate) fn parse_card_kind(s: &str) -> Result<CardKind> {
    Ok(match s {
        "credit" => CardKind::Credit,
        "debit" => CardKind::Debit,
        "gift" => CardKind::Gift,
        "loyalty" => CardKind::Loyalty,
        "other" => CardKind::Other,
        other => anyhow::bail!("unknown card kind: {other}"),
    })
}

pub(crate) fn parse_totp_algorithm(s: &str) -> Result<TotpAlgorithm> {
    Ok(match s.to_ascii_lowercase().as_str() {
        "sha1" => TotpAlgorithm::Sha1,
        "sha256" => TotpAlgorithm::Sha256,
        "sha512" => TotpAlgorithm::Sha512,
        other => anyhow::bail!("unknown algorithm: {other}"),
    })
}

// --- Per-type interactive edit helpers (moved from commands/edit.rs). Each
//     mutates its core slice in place; history-tracked variants take the
//     item's field_history map so they can record the prior value.

pub(crate) fn edit_login(
    l: &mut relicario_core::item_types::LoginCore,
    history: &mut FieldHistory,
    totp_qr: Option<PathBuf>,
) -> Result<()> {
    use relicario_core::item_types::{TotpConfig, TotpKind};
    if let Some(v) = prompt_keep_opt("Username", l.username.as_deref())? { l.username = Some(v); }
    if let Some(v) = prompt_keep_opt("URL", l.url.as_ref().map(|u| u.as_str()))? {
        l.url = Some(url::Url::parse(&v).with_context(|| format!("invalid URL: {v}"))?);
    }
    if prompt_yesno("Change password?")? {
        let old = l.password.clone();
        l.password = Some(Zeroizing::new(prompt_secret("New password: ")?));
        if let Some(old_pw) = old {
            push_history(history, "login_password", Zeroizing::new(old_pw.as_str().to_string()));
        }
    }
    if let Some(path) = totp_qr {
        let secret_b32 = crate::helpers::decode_totp_qr(&path)?;
        let secret_bytes = base32_decode_lenient(&secret_b32)?;
        l.totp = Some(TotpConfig {
            secret: Zeroizing::new(secret_bytes),
            algorithm: TotpAlgorithm::Sha1,
            digits: 6,
            period_seconds: 30,
            kind: TotpKind::Totp,
        });
        eprintln!("TOTP secret set from QR image.");
    }
    Ok(())
}

pub(crate) fn edit_secure_note(n: &mut relicario_core::item_types::SecureNoteCore, history: &mut FieldHistory) -> Result<()> {
    if prompt_yesno("Edit body?")? {
        let old = n.body.clone();
        let s = resolve_secret_multiline(false, "Enter new body; end with Ctrl-D:")?;
        n.body = Zeroizing::new(s);
        push_history(history, "secure_note_body", Zeroizing::new(old.as_str().to_string()));
    }
    Ok(())
}

pub(crate) fn edit_identity(i: &mut relicario_core::item_types::IdentityCore) -> Result<()> {
    if let Some(v) = prompt_keep_opt("Full name", i.full_name.as_deref())? { i.full_name = Some(v); }
    if let Some(v) = prompt_keep_opt("Email", i.email.as_deref())? { i.email = Some(v); }
    if let Some(v) = prompt_keep_opt("Phone", i.phone.as_deref())? { i.phone = Some(v); }
    Ok(())
}

pub(crate) fn edit_card(c: &mut relicario_core::item_types::CardCore, history: &mut FieldHistory) -> Result<()> {
    if let Some(v) = prompt_keep_opt("Holder", c.holder.as_deref())? { c.holder = Some(v); }
    if prompt_yesno("Change card number?")? {
        let old = c.number.clone();
        c.number = Some(Zeroizing::new(prompt_secret("New number: ")?));
        if let Some(o) = old {
            push_history(history, "card_number", Zeroizing::new(o.as_str().to_string()));
        }
    }
    Ok(())
}

pub(crate) fn edit_key(k: &mut relicario_core::item_types::KeyCore, history: &mut FieldHistory) -> Result<()> {
    if prompt_yesno("Replace key material?")? {
        let s = resolve_secret_multiline(false, "Paste new key material; end with Ctrl-D:")?;
        let old = k.key_material.clone();
        k.key_material = Zeroizing::new(s);
        push_history(history, "key_material", Zeroizing::new(old.as_str().to_string()));
    }
    Ok(())
}

pub(crate) fn edit_document_message() {
    eprintln!("Document items: use `relicario attach` / `relicario extract` instead.");
}

pub(crate) fn edit_totp(t: &mut relicario_core::item_types::TotpCore, history: &mut FieldHistory) -> Result<()> {
    if let Some(v) = prompt_keep_opt("Issuer", t.issuer.as_deref())? { t.issuer = Some(v); }
    if let Some(v) = prompt_keep_opt("Label", t.label.as_deref())? { t.label = Some(v); }
    if prompt_yesno("Change TOTP secret?")? {
        let old_b32 = data_encoding::BASE32.encode(&t.config.secret);
        let new_b32 = prompt_secret("New TOTP secret (base32): ")?;
        let new_bytes = base32_decode_lenient(&new_b32)?;
        t.config.secret = Zeroizing::new(new_bytes);
        push_history(history, "totp_secret", Zeroizing::new(old_b32));
    }
    Ok(())
}

pub(crate) fn build_login(
    title: String, username: Option<String>, url: Option<String>,
    password: Option<String>, password_stdin: bool, password_prompt: bool,
    totp_qr: Option<PathBuf>,
) -> Result<Item> {
    use relicario_core::item_types::{LoginCore, TotpConfig, TotpKind};
    let parsed_url = match url {
        Some(s) => Some(url::Url::parse(&s).with_context(|| format!("invalid URL: {s}"))?),
        None => None,
    };
    let password = if let Some(p) = password {
        Some(Zeroizing::new(p))
    } else if password_stdin {
        Some(Zeroizing::new(resolve_secret_line(password_stdin, "Password")?))
    } else if password_prompt {
        Some(Zeroizing::new(crate::prompt::prompt_secret("Password: ")?))
    } else {
        None
    };
    let totp = if let Some(path) = totp_qr {
        let secret_b32 = crate::helpers::decode_totp_qr(&path)?;
        let secret_bytes = base32_decode_lenient(&secret_b32)?;
        Some(TotpConfig {
            secret: Zeroizing::new(secret_bytes), algorithm: TotpAlgorithm::Sha1,
            digits: 6, period_seconds: 30, kind: TotpKind::Totp,
        })
    } else { None };
    Ok(Item::new(title, ItemCore::Login(LoginCore { username, password, url: parsed_url, totp })))
}

pub(crate) fn build_secure_note(title: String, body: Option<String>, body_stdin: bool) -> Result<Item> {
    use relicario_core::item_types::SecureNoteCore;
    let body = match body {
        Some(b) => b,
        None => resolve_secret_multiline(body_stdin, "Enter note body; end with Ctrl-D on a blank line:")?,
    };
    Ok(Item::new(title, ItemCore::SecureNote(SecureNoteCore { body: Zeroizing::new(body) })))
}

pub(crate) fn build_identity(
    title: String, full_name: Option<String>, email: Option<String>,
    phone: Option<String>, date_of_birth: Option<String>,
) -> Result<Item> {
    use relicario_core::item_types::IdentityCore;
    let dob = match date_of_birth {
        Some(s) => Some(chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d")
            .with_context(|| format!("invalid date {s} (expected YYYY-MM-DD)"))?),
        None => None,
    };
    Ok(Item::new(title, ItemCore::Identity(IdentityCore {
        full_name, address: None, phone, email, date_of_birth: dob,
    })))
}

pub(crate) fn build_card(
    title: String, holder: Option<String>, expiry: Option<String>, kind: &str,
    number_stdin: bool, cvv_stdin: bool, pin_stdin: bool,
) -> Result<Item> {
    use relicario_core::item_types::CardCore;
    let number = Zeroizing::new(resolve_secret_line(number_stdin, "Card number")?);
    let cvv = resolve_secret_line(cvv_stdin, "CVV (blank to skip)")?;
    let cvv = if cvv.is_empty() { None } else { Some(Zeroizing::new(cvv)) };
    let pin = resolve_secret_line(pin_stdin, "PIN (blank to skip)")?;
    let pin = if pin.is_empty() { None } else { Some(Zeroizing::new(pin)) };
    let parsed_expiry = match expiry { Some(s) => Some(crate::parse::parse_month_year(&s)?), None => None };
    Ok(Item::new(title, ItemCore::Card(CardCore {
        number: Some(number), holder, expiry: parsed_expiry, cvv, pin, kind: parse_card_kind(kind)?,
    })))
}

pub(crate) fn build_key(
    title: String, label: Option<String>, algorithm: Option<String>,
    public_key: Option<String>, material_stdin: bool,
) -> Result<Item> {
    use relicario_core::item_types::KeyCore;
    let key_material = resolve_secret_multiline(material_stdin, "Paste key material; end with Ctrl-D on a blank line:")?;
    if key_material.trim().is_empty() { anyhow::bail!("key material required"); }
    Ok(Item::new(title, ItemCore::Key(KeyCore {
        key_material: Zeroizing::new(key_material), label, public_key, algorithm,
    })))
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn build_totp(
    title: String, issuer: Option<String>, label: Option<String>,
    secret: Option<String>, secret_stdin: bool, period: u32, digits: u8, algorithm: &str,
) -> Result<Item> {
    use relicario_core::item_types::{TotpConfig, TotpCore, TotpKind};
    let secret_b32 = match secret {
        Some(s) => s,
        None => resolve_secret_line(secret_stdin, "TOTP secret (base32)")?,
    };
    let secret_bytes = base32_decode_lenient(&secret_b32)?;
    Ok(Item::new(title, ItemCore::Totp(TotpCore {
        config: TotpConfig {
            secret: Zeroizing::new(secret_bytes), algorithm: parse_totp_algorithm(algorithm)?,
            digits, period_seconds: period, kind: TotpKind::Totp,
        },
        issuer, label,
    })))
}

/// Read a file and encrypt it as an attachment under `key`, deriving its display
/// metadata. The plaintext is held in a `Zeroizing` buffer so it is wiped after
/// encryption. Returns the encrypted blob plus (filename, mime_type, size).
pub(crate) fn encrypt_document_file(
    path: &Path,
    key: &Zeroizing<[u8; 32]>,
    max_bytes: u64,
) -> Result<(EncryptedAttachment, String, String, u64)> {
    use relicario_core::encrypt_attachment;
    let bytes = Zeroizing::new(
        std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?,
    );
    let enc = encrypt_attachment(&bytes, key, max_bytes)?;
    let filename = path
        .file_name()
        .ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", path.display()))?
        .to_string_lossy()
        .into_owned();
    let mime_type = crate::parse::guess_mime(&filename);
    Ok((enc, filename, mime_type, bytes.len() as u64))
}

pub(crate) fn build_document(
    title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64,
) -> Result<(Item, EncryptedAttachment)> {
    use relicario_core::item_types::DocumentCore;
    use relicario_core::AttachmentRef;
    let (enc, filename, mime_type, size) = encrypt_document_file(&file, key, max_bytes)?;
    let mut item = Item::new(title, ItemCore::Document(DocumentCore {
        filename: filename.clone(), mime_type: mime_type.clone(), primary_attachment: enc.id.clone(),
    }));
    item.attachments.push(AttachmentRef {
        id: enc.id.clone(), filename, mime_type, size, created: item.created,
    });
    Ok((item, enc))
}

pub(crate) fn push_history(
    history: &mut FieldHistory,
    synthetic_key: &str,
    old_value: zeroize::Zeroizing<String>,
) {
    // Synthetic FieldId for core-level fields — stable per-item (prefixed so
    // custom-field UUIDs can't collide).
    let fid = relicario_core::FieldId(format!("core:{synthetic_key}"));
    history.entry(fid).or_default().push(FieldHistoryEntry {
        value: old_value,
        replaced_at: now_unix(),
    });
}

#[cfg(test)]
mod tests {
    use super::*;
    use relicario_core::item_types::{CardKind, TotpAlgorithm};

    #[test]
    fn card_kind_parses_known_values() {
        assert_eq!(parse_card_kind("credit").unwrap(), CardKind::Credit);
        assert_eq!(parse_card_kind("loyalty").unwrap(), CardKind::Loyalty);
    }

    #[test]
    fn card_kind_rejects_unknown() {
        assert!(parse_card_kind("platinum").is_err());
    }

    #[test]
    fn totp_algorithm_is_case_insensitive() {
        assert_eq!(parse_totp_algorithm("SHA256").unwrap(), TotpAlgorithm::Sha256);
    }

    #[test]
    fn totp_algorithm_rejects_unknown() {
        assert!(parse_totp_algorithm("md5").is_err());
    }
}
