//! `relicario attach` / `attachments` / `extract` / `detach` — per-attachment ops.

use std::path::PathBuf;

use anyhow::{Context, Result};

use crate::parse::guess_mime;

pub fn cmd_attach(query: String, file: PathBuf) -> Result<()> {
    use std::fs;
    use relicario_core::{encrypt_attachment, AttachmentRef};
    use relicario_core::time::now_unix;

    let vault = crate::session::UnlockedVault::unlock_interactive()?;
    let mut manifest = vault.load_manifest()?;
    let entry = super::resolve_query(&manifest, &query)?;
    let id = entry.id.clone();
    let _ = entry;
    let mut item = vault.load_item(&id)?;
    let settings = vault.load_settings()?;
    let caps = settings.attachment_caps;

    if item.attachments.len() as u32 >= caps.per_item_max_count {
        anyhow::bail!("item already has {} attachments (max {})",
                      item.attachments.len(), caps.per_item_max_count);
    }

    let bytes = fs::read(&file)
        .with_context(|| format!("failed to read {}", file.display()))?;

    // Check per-vault total attachment bytes cap (audit I3).
    let current_total: u64 = manifest.items.values()
        .flat_map(|e| &e.attachment_summaries)
        .map(|s| s.size)
        .sum();
    let new_size = bytes.len() as u64;
    let hard_cap = caps.per_vault_hard_cap_bytes;
    let soft_cap = caps.per_vault_soft_cap_bytes;
    if current_total + new_size > hard_cap {
        anyhow::bail!(
            "attachment would exceed vault hard cap ({} + {} > {} bytes)",
            current_total, new_size, hard_cap
        );
    }
    if current_total + new_size > soft_cap {
        eprintln!(
            "warning: vault attachments will exceed soft cap ({} bytes)",
            soft_cap
        );
    }

    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 aref = AttachmentRef {
        id: enc.id.clone(),
        filename,
        mime_type,
        size: bytes.len() as u64,
        created: now_unix(),
    };

    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", enc.id.as_str())), &enc.bytes)?;

    item.attachments.push(aref);
    item.modified = now_unix();
    vault.save_item(&item)?;
    manifest.upsert(&item);
    vault.after_manifest_change(&manifest)?;

    let paths = [
        format!("items/{}.enc", item.id.as_str()),
        "manifest.enc".into(),
        format!("attachments/{}/{}.enc", item.id.as_str(), enc.id.as_str()),
    ];
    let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
    super::commit_paths(&vault, &format!("attach: {} → {} ({})",
                                  crate::helpers::sanitize_for_commit(&file.display().to_string()),
                                  crate::helpers::sanitize_for_commit(&item.title),
                                  item.id.as_str()), &path_refs)?;
    eprintln!("Attached {} to {} (aid={})", file.display(), item.title, enc.id.as_str());
    Ok(())
}

pub fn cmd_attachments(query: String) -> Result<()> {
    let vault = crate::session::UnlockedVault::unlock_interactive()?;
    let manifest = vault.load_manifest()?;
    let entry = super::resolve_query(&manifest, &query)?;
    let item = vault.load_item(&entry.id)?;
    if item.attachments.is_empty() { eprintln!("(no attachments)"); return Ok(()); }
    println!("{:<17}  {:>12}  {:<22}  FILENAME", "AID", "SIZE", "MIME");
    for a in &item.attachments {
        println!("{:<17}  {:>12}  {:<22}  {}", a.id.as_str(), a.size, a.mime_type, a.filename);
    }
    Ok(())
}

pub fn cmd_extract(query: String, aid: String, out: Option<PathBuf>) -> Result<()> {
    use std::fs;
    use relicario_core::decrypt_attachment;

    let vault = crate::session::UnlockedVault::unlock_interactive()?;
    let manifest = vault.load_manifest()?;
    let entry = super::resolve_query(&manifest, &query)?;
    let item = vault.load_item(&entry.id)?;

    let aref = item.attachments.iter().find(|a| a.id.as_str() == aid)
        .ok_or_else(|| anyhow::anyhow!("no attachment {aid} on {}", item.title))?;
    let path = vault.root().join("attachments").join(item.id.as_str())
        .join(format!("{}.enc", aid));
    let bytes = fs::read(&path)
        .with_context(|| format!("failed to read {}", path.display()))?;
    let plaintext = decrypt_attachment(&bytes, vault.key())?;
    let out_path = out.unwrap_or_else(|| PathBuf::from(&aref.filename));
    fs::write(&out_path, plaintext.as_slice())
        .with_context(|| format!("failed to write {}", out_path.display()))?;
    eprintln!("Wrote {} bytes to {}", plaintext.len(), out_path.display());
    Ok(())
}

pub fn cmd_detach(query: String, aid: String) -> Result<()> {
    use std::fs;
    use relicario_core::ItemCore;
    use relicario_core::time::now_unix;

    let vault = crate::session::UnlockedVault::unlock_interactive()?;
    let mut manifest = vault.load_manifest()?;
    let entry = super::resolve_query(&manifest, &query)?;
    let id = entry.id.clone();
    let _ = entry;
    let mut item = vault.load_item(&id)?;

    let pos = item.attachments.iter().position(|a| a.id.as_str() == aid)
        .ok_or_else(|| anyhow::anyhow!("no attachment {aid} on {}", item.title))?;

    // Document items keep their primary blob in the core; refuse to orphan it.
    if let ItemCore::Document(d) = &item.core {
        if d.primary_attachment.as_str() == aid {
            anyhow::bail!(
                "cannot detach the primary attachment of a Document item; \
                 use `purge {}` to delete the whole item",
                item.title,
            );
        }
    }

    let removed = item.attachments.remove(pos);
    let blob_path = vault.root().join("attachments").join(item.id.as_str())
        .join(format!("{}.enc", removed.id.as_str()));
    if blob_path.exists() {
        fs::remove_file(&blob_path)
            .with_context(|| format!("failed to delete {}", blob_path.display()))?;
    }

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

    let item_path = format!("items/{}.enc", item.id.as_str());
    let blob_relpath = format!("attachments/{}/{}.enc", item.id.as_str(), removed.id.as_str());
    super::commit_paths(
        &vault,
        &format!("detach: {} from {} ({})", crate::helpers::sanitize_for_commit(&removed.filename), crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()),
        &[&item_path, "manifest.enc", &blob_relpath],
    )?;
    eprintln!("Detached {} (aid={}) from {}", removed.filename, aid, item.title);
    Ok(())
}
