//! `relicario backup export` / `relicario backup restore` — pack/unpack the
//! encrypted `.relbak` envelope.

use std::path::PathBuf;

use anyhow::{Context, Result};

use crate::BackupAction;

pub fn cmd_backup(action: BackupAction) -> Result<()> {
    match action {
        BackupAction::Export { out, include_image, image, no_history } => {
            cmd_backup_export(out, include_image, image, no_history)
        }
        BackupAction::Restore { input, target } => cmd_backup_restore(input, target),
    }
}

pub(super) fn cmd_backup_export(
    out: PathBuf,
    include_image: bool,
    image: Option<PathBuf>,
    no_history: bool,
) -> Result<()> {
    use std::fs;
    use relicario_core::{backup, validate_passphrase_strength};
    use zeroize::Zeroizing;

    let root = crate::helpers::vault_dir()?;

    // Backup passphrase — prompt twice, gate on zxcvbn (audit H3).
    let passphrase = if let Some(p) = crate::test_backup_passphrase_override() {
        Zeroizing::new(p)
    } else {
        Zeroizing::new(rpassword::prompt_password("Backup passphrase: ")?)
    };
    let confirm = if crate::test_backup_passphrase_override().is_some() {
        passphrase.clone()
    } else {
        Zeroizing::new(rpassword::prompt_password("Confirm passphrase: ")?)
    };
    if passphrase.as_str() != confirm.as_str() {
        anyhow::bail!("passphrases do not match");
    }
    if let Err(e) = validate_passphrase_strength(&passphrase) {
        anyhow::bail!("backup {}. Choose a longer or more entropic phrase.", e);
    }

    // Read everything from disk that the envelope needs.
    let salt = fs::read(root.join(".relicario").join("salt"))
        .with_context(|| "failed to read .relicario/salt")?;
    let params_json = fs::read_to_string(root.join(".relicario").join("params.json"))
        .with_context(|| "failed to read .relicario/params.json")?;
    // devices.json was removed in the B1 security audit fix; fall back to
    // an empty array so backups of post-B1 vaults still pack cleanly.
    // Task 12 will remove the devices field from the backup format entirely.
    let devices_json = fs::read_to_string(root.join(".relicario").join("devices.json"))
        .unwrap_or_else(|_| "[]".to_string());
    let manifest_enc = fs::read(root.join("manifest.enc"))
        .with_context(|| "failed to read manifest.enc")?;
    let settings_enc = fs::read(root.join("settings.enc"))
        .with_context(|| "failed to read settings.enc")?;

    // Items.
    let mut item_files = Vec::new();
    let items_dir = root.join("items");
    if items_dir.is_dir() {
        for entry in fs::read_dir(&items_dir)? {
            let p = entry?.path();
            if p.extension().and_then(|s| s.to_str()) != Some("enc") { continue; }
            let id = p.file_stem()
                .and_then(|s| s.to_str())
                .ok_or_else(|| anyhow::anyhow!("bad item filename: {}", p.display()))?
                .to_string();
            let bytes = fs::read(&p)?;
            item_files.push((id, bytes));
        }
    }

    // Attachments. Layout: attachments/<item_id>/<aid>.enc
    let mut attach_files = Vec::new();
    let attach_dir = root.join("attachments");
    if attach_dir.is_dir() {
        for entry in fs::read_dir(&attach_dir)? {
            let item_dir = entry?.path();
            if !item_dir.is_dir() { continue; }
            let item_id = item_dir.file_name()
                .and_then(|s| s.to_str())
                .ok_or_else(|| anyhow::anyhow!("bad attachment dir: {}", item_dir.display()))?
                .to_string();
            for sub in fs::read_dir(&item_dir)? {
                let p = sub?.path();
                if p.extension().and_then(|s| s.to_str()) != Some("enc") { continue; }
                let aid = p.file_stem()
                    .and_then(|s| s.to_str())
                    .ok_or_else(|| anyhow::anyhow!("bad attachment filename: {}", p.display()))?
                    .to_string();
                let bytes = fs::read(&p)?;
                attach_files.push((item_id.clone(), aid, bytes));
            }
        }
    }

    // Optional reference image.
    let image_bytes = if include_image {
        let path = match image {
            Some(p) => p,
            None => crate::session::get_image_path()?,
        };
        Some(fs::read(&path)
            .with_context(|| format!("failed to read reference image {}", path.display()))?)
    } else {
        None
    };

    // Optional .git/ tar.
    let git_archive = if no_history { None } else { Some(tar_directory(&root.join(".git"))?) };

    let items_refs: Vec<backup::BackupItem> = item_files.iter()
        .map(|(id, bytes)| backup::BackupItem { id: id.clone(), ciphertext: bytes })
        .collect();
    let attach_refs: Vec<backup::BackupAttachment> = attach_files.iter()
        .map(|(iid, aid, bytes)| backup::BackupAttachment {
            item_id: iid.clone(),
            attachment_id: aid.clone(),
            ciphertext: bytes,
        })
        .collect();

    let input = backup::BackupInput {
        salt: &salt,
        params_json: &params_json,
        devices_json: &devices_json,
        manifest_enc: &manifest_enc,
        settings_enc: &settings_enc,
        items: items_refs,
        attachments: attach_refs,
        reference_jpg: image_bytes.as_deref(),
        git_archive: git_archive.as_deref(),
    };

    let bytes = backup::pack_backup(input, &passphrase)?;

    // atomic_write via the existing pattern: write `.tmp`, rename.
    let tmp = {
        let mut t = out.as_os_str().to_owned();
        t.push(".tmp");
        PathBuf::from(t)
    };
    fs::write(&tmp, &bytes)
        .with_context(|| format!("failed to write {}", tmp.display()))?;
    fs::rename(&tmp, &out)
        .with_context(|| format!("failed to rename {}", out.display()))?;

    // Marker file for `cmd_status`. Format: ISO-8601 UTC line.
    let now_iso = crate::helpers::iso8601(relicario_core::now_unix());
    fs::write(root.join(".relicario").join("last_backup"), format!("{now_iso}\n"))?;

    let mib = (bytes.len() as f64) / (1024.0 * 1024.0);
    eprintln!(
        "Wrote {} ({:.2} MiB). Delete after restore is verified.",
        out.display(), mib
    );
    Ok(())
}

/// Tar a directory into an in-memory `Vec<u8>`. Used for `.git/` bundling.
fn tar_directory(dir: &std::path::Path) -> Result<Vec<u8>> {
    let mut buf = Vec::new();
    {
        let mut builder = tar::Builder::new(&mut buf);
        builder.append_dir_all(".", dir)
            .with_context(|| format!("failed to tar {}", dir.display()))?;
        builder.finish().with_context(|| "failed to finalize git tar")?;
    }
    Ok(buf)
}

pub(super) fn cmd_backup_restore(input: PathBuf, target: PathBuf) -> Result<()> {
    use std::fs;
    use relicario_core::backup;
    use relicario_core::{ItemId, AttachmentId};
    use zeroize::Zeroizing;

    let target = if target.is_absolute() {
        target
    } else {
        std::env::current_dir()?.join(&target)
    };

    if target.join(".relicario").exists() {
        anyhow::bail!(
            "target dir already contains a Relicario vault; restore refuses to overwrite — use an empty directory: {}",
            target.display()
        );
    }
    fs::create_dir_all(&target)
        .with_context(|| format!("failed to create target {}", target.display()))?;

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

    // Backup passphrase prompt.
    let passphrase = if let Some(p) = crate::test_backup_passphrase_override() {
        Zeroizing::new(p)
    } else {
        Zeroizing::new(rpassword::prompt_password("Backup passphrase: ")?)
    };

    let unpacked = backup::unpack_backup(&bytes, &passphrase)
        .map_err(|e| match e {
            relicario_core::RelicarioError::Decrypt =>
                anyhow::anyhow!("wrong backup passphrase, or the file is corrupt"),
            other => anyhow::anyhow!(other),
        })?;

    // Write vault layout.
    let relicario_dir = target.join(".relicario");
    fs::create_dir_all(&relicario_dir)?;
    fs::create_dir_all(target.join("items"))?;
    fs::create_dir_all(target.join("attachments"))?;

    fs::write(relicario_dir.join("salt"), unpacked.salt)?;
    fs::write(relicario_dir.join("params.json"), &unpacked.params_json)?;
    fs::write(relicario_dir.join("devices.json"), &unpacked.devices_json)?;
    fs::write(target.join("manifest.enc"), &unpacked.manifest_enc)?;
    fs::write(target.join("settings.enc"), &unpacked.settings_enc)?;

    for item in &unpacked.items {
        let item_id = ItemId(item.id.clone());
        if !item_id.is_valid() {
            anyhow::bail!("invalid item ID in backup: {} (path traversal blocked)", item.id);
        }
        fs::write(target.join("items").join(format!("{}.enc", item.id)), &item.ciphertext)?;
    }
    for a in &unpacked.attachments {
        let item_id = ItemId(a.item_id.clone());
        let att_id = AttachmentId(a.attachment_id.clone());
        if !item_id.is_valid() || !att_id.is_valid() {
            anyhow::bail!("invalid attachment ID in backup (path traversal blocked)");
        }
        let dir = target.join("attachments").join(&a.item_id);
        fs::create_dir_all(&dir)?;
        fs::write(dir.join(format!("{}.enc", a.attachment_id)), &a.ciphertext)?;
    }

    // Reference image (if present).
    if let Some(jpg) = &unpacked.reference_jpg {
        let path = target.join("reference.jpg");
        fs::write(&path, jpg)
            .with_context(|| format!("failed to write reference image {}", path.display()))?;
    }

    // .git/ history.
    if let Some(tar_bytes) = &unpacked.git_archive {
        // Cap: 100× the compressed bundle size, or 1 GiB, whichever is lower.
        let cap = std::cmp::min(
            (tar_bytes.len() as u64).saturating_mul(100),
            relicario_core::DEFAULT_MAX_UNCOMPRESSED,
        );
        let entries = relicario_core::safe_unpack_git_archive(tar_bytes, cap)
            .with_context(|| "failed to safely unpack .git/ archive")?;
        let git_dir = target.join(".git");
        for (rel_path, body) in entries {
            let dest = git_dir.join(&rel_path);
            // Paranoid OS-level check even after textual validation in core.
            if !dest.starts_with(&git_dir) {
                anyhow::bail!(
                    "tar entry {} resolved outside .git/ (path traversal blocked)",
                    rel_path.display()
                );
            }
            if let Some(parent) = dest.parent() {
                fs::create_dir_all(parent).with_context(|| {
                    format!("create parent {}", parent.display())
                })?;
            }
            fs::write(&dest, &body).with_context(|| {
                format!("write {}", dest.display())
            })?;
        }
    } else {
        // No history bundled — start a fresh git repo.
        crate::helpers::git_run(&target, &["init"], "backup restore: git init")?;

        // .gitignore — exclude reference image if present.
        if target.join("reference.jpg").exists() {
            fs::write(target.join(".gitignore"), "reference.jpg\n")?;
        }

        let _ = crate::helpers::git_command(&target, &["add", "."]).status()?;
        let now_iso = crate::helpers::iso8601(relicario_core::now_unix());
        let msg = format!("restore from backup {now_iso}");
        let _ = crate::helpers::git_command(&target, &["commit", "-m", &msg]).status()?;
    }

    eprintln!(
        "Restored vault to {}. Unlock with your passphrase + reference image.",
        target.display()
    );
    Ok(())
}
