//! `relicario add <kind>` — create a new item of the given type.
//!
//! `cmd_add` does the common save / manifest upsert / commit dance. The seven
//! per-type `build_*_item` helpers each return a fully-populated `Item`. The
//! `Document` builder is the only one that needs the unlocked vault (for the
//! attachment-cap settings + writing the encrypted blob alongside the item).

use std::path::PathBuf;

use anyhow::{Context, Result};

use crate::AddKind;
use crate::parse::{base32_decode_lenient, guess_mime, parse_month_year};
use crate::prompt::{prompt, prompt_optional, prompt_or_flag, prompt_or_flag_optional, prompt_secret};

pub fn cmd_add(kind: AddKind) -> Result<()> {
    let vault = crate::session::UnlockedVault::unlock_interactive()?;
    let mut manifest = vault.load_manifest()?;

    let item = match kind {
        AddKind::Login { title, username, url, password_prompt, password, group, tags, favorite, totp_qr } =>
            build_login_item(title, username, url, password_prompt, password, group, tags, favorite, totp_qr)?,
        AddKind::SecureNote { title, body_prompt, group, tags } =>
            build_secure_note_item(title, body_prompt, group, tags)?,
        AddKind::Identity { title, full_name, email, phone, date_of_birth, group, tags } =>
            build_identity_item(title, full_name, email, phone, date_of_birth, group, tags)?,
        AddKind::Card { title, holder, expiry, kind, group, tags } =>
            build_card_item(title, holder, expiry, kind, group, tags)?,
        AddKind::Key { title, label, algorithm, group, tags } =>
            build_key_item(title, label, algorithm, group, tags)?,
        AddKind::Document { title, file, group, tags } =>
            build_document_item(&vault, title, file, group, tags)?,
        AddKind::Totp { title, issuer, label, secret, period, digits, algorithm, group, tags } =>
            build_totp_item(title, issuer, label, secret, period, digits, algorithm, group, tags)?,
    };

    vault.save_item(&item)?;
    manifest.upsert(&item);
    vault.after_manifest_change(&manifest)?;

    let mut paths: Vec<String> = vec![
        format!("items/{}.enc", item.id.as_str()),
        "manifest.enc".into(),
    ];
    for att in &item.attachments {
        paths.push(format!("attachments/{}/{}.enc", item.id.as_str(), att.id.as_str()));
    }
    let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
    super::commit_paths(&vault, &format!("add: {} ({})", crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()), &path_refs)?;

    eprintln!("Added: {} (id={})", item.title, item.id.as_str());
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn build_login_item(
    title: Option<String>,
    username: Option<String>,
    url: Option<String>,
    password_prompt: bool,
    password: Option<String>,
    group: Option<String>,
    tags: Vec<String>,
    favorite: bool,
    totp_qr: Option<PathBuf>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::{LoginCore, TotpAlgorithm, TotpConfig, TotpKind};
    use relicario_core::{Item, ItemCore};
    use zeroize::Zeroizing;

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    let username = prompt_or_flag_optional(username, "Username", |s| Ok(s.to_string()))?;
    let url = prompt_or_flag_optional(url, "URL", |s| Ok(s.to_string()))?;
    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_prompt {
        Some(Zeroizing::new(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
    };
    let mut item = Item::new(title, ItemCore::Login(LoginCore {
        username, password, url: parsed_url, totp,
    }));
    item.group = group;
    item.tags = tags;
    item.favorite = favorite;
    Ok(item)
}

fn build_secure_note_item(
    title: Option<String>,
    body_prompt: bool,
    group: Option<String>,
    tags: Vec<String>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::SecureNoteCore;
    use relicario_core::{Item, ItemCore};
    use zeroize::Zeroizing;

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    let body = if body_prompt {
        eprintln!("Enter note body; end with Ctrl-D on a blank line:");
        let mut s = String::new();
        std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?;
        s
    } else {
        prompt("Body")?
    };
    let mut item = Item::new(title, ItemCore::SecureNote(SecureNoteCore {
        body: Zeroizing::new(body),
    }));
    item.group = group;
    item.tags = tags;
    Ok(item)
}

fn build_identity_item(
    title: Option<String>,
    full_name: Option<String>,
    email: Option<String>,
    phone: Option<String>,
    date_of_birth: Option<String>,
    group: Option<String>,
    tags: Vec<String>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::IdentityCore;
    use relicario_core::{Item, ItemCore};

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    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,
    };
    let mut item = Item::new(title, ItemCore::Identity(IdentityCore {
        full_name, address: None, phone, email, date_of_birth: dob,
    }));
    item.group = group;
    item.tags = tags;
    Ok(item)
}

fn build_card_item(
    title: Option<String>,
    holder: Option<String>,
    expiry: Option<String>,
    kind: String,
    group: Option<String>,
    tags: Vec<String>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::{CardCore, CardKind};
    use relicario_core::{Item, ItemCore};
    use zeroize::Zeroizing;

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    let number = Zeroizing::new(prompt_secret("Card number: ")?);
    let cvv = Zeroizing::new(prompt_secret("CVV (blank to skip): ")?);
    let cvv = if cvv.is_empty() { None } else { Some(cvv) };
    let pin = Zeroizing::new(prompt_secret("PIN (blank to skip): ")?);
    let pin = if pin.is_empty() { None } else { Some(pin) };

    let parsed_expiry = match expiry {
        Some(s) => Some(parse_month_year(&s)?),
        None => None,
    };
    let parsed_kind = match kind.as_str() {
        "credit"  => CardKind::Credit,
        "debit"   => CardKind::Debit,
        "gift"    => CardKind::Gift,
        "loyalty" => CardKind::Loyalty,
        "other"   => CardKind::Other,
        other => anyhow::bail!("unknown card kind: {other}"),
    };

    let mut item = Item::new(title, ItemCore::Card(CardCore {
        number: Some(number), holder, expiry: parsed_expiry, cvv, pin, kind: parsed_kind,
    }));
    item.group = group;
    item.tags = tags;
    Ok(item)
}

fn build_key_item(
    title: Option<String>,
    label: Option<String>,
    algorithm: Option<String>,
    group: Option<String>,
    tags: Vec<String>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::KeyCore;
    use relicario_core::{Item, ItemCore};
    use zeroize::Zeroizing;

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    eprintln!("Paste key material; end with Ctrl-D on a blank line:");
    let mut key_material = String::new();
    std::io::Read::read_to_string(&mut std::io::stdin(), &mut key_material)?;
    if key_material.trim().is_empty() { anyhow::bail!("key material required"); }
    let public_key = prompt_optional("Public key (blank to skip)")?;

    let mut item = Item::new(title, ItemCore::Key(KeyCore {
        key_material: Zeroizing::new(key_material),
        label, public_key, algorithm,
    }));
    item.group = group;
    item.tags = tags;
    Ok(item)
}

fn build_document_item(
    vault: &crate::session::UnlockedVault,
    title: Option<String>,
    file: PathBuf,
    group: Option<String>,
    tags: Vec<String>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::DocumentCore;
    use relicario_core::{encrypt_attachment, AttachmentRef, Item, ItemCore};
    use std::fs;

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    let bytes = fs::read(&file)
        .with_context(|| format!("failed to read {}", file.display()))?;
    let caps = vault.load_settings()?.attachment_caps;
    let enc = encrypt_attachment(&bytes, vault.key(), caps.per_attachment_max_bytes)?;

    let filename = file.file_name()
        .ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", file.display()))?
        .to_string_lossy()
        .into_owned();
    let mime_type = guess_mime(&filename);

    let primary_attachment = enc.id.clone();
    let mut item = Item::new(title, ItemCore::Document(DocumentCore {
        filename: filename.clone(),
        mime_type: mime_type.clone(),
        primary_attachment: primary_attachment.clone(),
    }));
    item.group = group;
    item.tags = tags;
    item.attachments.push(AttachmentRef {
        id: primary_attachment.clone(),
        filename, mime_type,
        size: bytes.len() as u64,
        created: item.created,
    });

    let att_dir = vault.root().join("attachments").join(item.id.as_str());
    fs::create_dir_all(&att_dir)?;
    fs::write(att_dir.join(format!("{}.enc", primary_attachment.as_str())), &enc.bytes)?;
    Ok(item)
}

#[allow(clippy::too_many_arguments)]
fn build_totp_item(
    title: Option<String>,
    issuer: Option<String>,
    label: Option<String>,
    secret: Option<String>,
    period: u32,
    digits: u8,
    algorithm: String,
    group: Option<String>,
    tags: Vec<String>,
) -> Result<relicario_core::Item> {
    use relicario_core::item_types::{TotpAlgorithm, TotpConfig, TotpCore, TotpKind};
    use relicario_core::{Item, ItemCore};
    use zeroize::Zeroizing;

    let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
    let secret_b32 = match secret {
        Some(s) => s,
        None => prompt_secret("TOTP secret (base32): ")?,
    };
    let secret_bytes = base32_decode_lenient(&secret_b32)?;
    let algo = match algorithm.to_ascii_lowercase().as_str() {
        "sha1"   => TotpAlgorithm::Sha1,
        "sha256" => TotpAlgorithm::Sha256,
        "sha512" => TotpAlgorithm::Sha512,
        other => anyhow::bail!("unknown algorithm: {other}"),
    };

    let mut item = Item::new(title, ItemCore::Totp(TotpCore {
        config: TotpConfig {
            secret: Zeroizing::new(secret_bytes),
            algorithm: algo,
            digits,
            period_seconds: period,
            kind: TotpKind::Totp,
        },
        issuer, label,
    }));
    item.group = group;
    item.tags = tags;
    Ok(item)
}
