//! Trust Systems Tauri application — IPC commands and app entry.

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::fs;
use std::path::{Path, PathBuf};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use image::ImageReader;
use p256::ecdsa::SigningKey;
use p256::pkcs8::DecodePrivateKey;
use rfd::FileDialog;
use serde::{Deserialize, Serialize};
use trust_systems::c2pa_core::{self, VerificationResult};
use tauri::Manager;
use watermark_core::{apply_watermark, extract_watermark, ProvenancePayload};
use zeroize::Zeroizing;

mod vault;
mod signed_messages;
mod fingerprint;
mod nfc;
mod ntag424;
mod iso7816_t4t;
mod ev2_auth;
mod sdm;
mod vault_keys;
mod enrollment;
mod nfc_debug;

/// Sanitizes error messages for display in the UI: strips full paths to avoid
/// leaking filesystem layout. Keeps basenames for file-not-found style errors.
fn sanitize_error_for_frontend(e: impl std::fmt::Display) -> String {
    let s = e.to_string();

    // Map known patterns to user-facing messages without full paths.
    if s.contains("input file not found") {
        let basename = s
            .split(": ")
            .last()
            .and_then(|p| Path::new(p).file_name())
            .and_then(|n| n.to_str())
            .unwrap_or("file");
        return format!("Input file not found: {}.", basename);
    }
    if s.contains("output file already exists") {
        let basename = s
            .split(": ")
            .last()
            .and_then(|p| Path::new(p).file_name())
            .and_then(|n| n.to_str())
            .unwrap_or("file");
        return format!("Output file already exists: {}.", basename);
    }
    if s.contains("author cannot be empty") {
        return "Author cannot be empty.".to_string();
    }
    if s.contains("recipient name cannot be empty") {
        return "Recipient name cannot be empty.".to_string();
    }
    if s.contains("manifest validation failed") {
        return "Manifest validation failed.".to_string();
    }
    if s.contains("invalid vault password") {
        return "Invalid vault password.".to_string();
    }
    if s.contains("vault is not initialized") {
        return "Vault is not initialized. Initialize the vault first.".to_string();
    }
    if s.contains("master password cannot be empty") {
        return "Master password cannot be empty.".to_string();
    }
    if s.contains("no active signing key in vault") {
        return "No signing key in vault. Import a private key first.".to_string();
    }
    if s.contains("no active identity in vault") {
        return "No active identity in vault. Generate or activate an identity first.".to_string();
    }
    if s.contains("active identity has no certificate chain") {
        return "Active identity has no certificate chain. Generate a new identity or import a certificate chain in Settings.".to_string();
    }
    if s.contains("draft id") && s.contains("not found") {
        return "Selected artifact was not found in the vault.".to_string();
    }
    if s.contains("identity id") && s.contains("not found") {
        return "Selected identity was not found in the vault.".to_string();
    }
    if s.contains("contact id") && s.contains("not found") {
        return "Selected contact was not found.".to_string();
    }
    if s.contains("idx_trusted_contacts_certificate_fingerprint")
        || s.contains("UNIQUE constraint failed: trusted_contacts.certificate_fingerprint")
    {
        return "This certificate is already in Trusted Contacts.".to_string();
    }
    if s.contains("certificate does not match the private key in this vault slot")
        || s.contains("Certificate does not match the Private Key in this Vault slot")
    {
        return "Certificate does not match the Private Key in this Vault slot.".to_string();
    }
    if s.contains("tag is controlled by a different identity") {
        return "This tag is managed by another identity in this vault. Switch active identity in Settings to modify it.".to_string();
    }
    if s.contains("91AE") || s.contains("authentication error") {
        return "The tag could not be authenticated with the current key context. If it belongs to another identity, switch active identity in Settings and try again.".to_string();
    }
    if s.contains("country must be a 2-letter uppercase ISO-3166 code") {
        return "Country must be a 2-letter uppercase ISO-3166 code (for example: US).".to_string();
    }
    if s.contains("valid PEM private key") {
        return "The key file is not a valid PEM private key.".to_string();
    }
    // Only match vault/signing flow errors, not CMS verify errors (which often mention "certificate").
    if s.contains("load signing") || (s.contains("Failed to load") && (s.contains("certificate") || s.contains("key"))) {
        return "Failed to load signing certificate or key.".to_string();
    }
    if s.contains("read C2PA manifest") || s.contains("JUMBF") || s.contains("manifest") && s.contains("read") {
        return "Could not read C2PA manifest from file.".to_string();
    }
    if s.contains("decode image for perceptual hash") {
        return "Could not decode image for visual fingerprinting.".to_string();
    }
    if s.contains("New password must differ") {
        return "New password must differ from current password.".to_string();
    }
    if s.contains("rekey vault database") {
        return "Failed to change vault password. The vault may be corrupted—restore from backup if you have one.".to_string();
    }
    if s.contains("parse CMS") || s.contains("decode base64") {
        return "Could not parse signed message. Ensure it's a valid PEM or CMS structure.".to_string();
    }
    if s.contains("signature verification failed") {
        return "Signature verification failed. The message may have been altered.".to_string();
    }
    // OpenSSL CMS verify errors (chain/issuer issues still mean the signature is structurally valid).
    if s.contains("certificate verify failed") || s.contains("unable to get local issuer certificate")
        || s.contains("self signed certificate")
    {
        return "Signature verification failed. The signer's certificate could not be validated (missing or untrusted issuer).".to_string();
    }
    if s.contains("zip does not contain") {
        return "This zip is not a valid Trust Systems vault backup.".to_string();
    }
    if s.contains("exceeds maximum size") {
        return "Backup file is too large or corrupted.".to_string();
    }

    // Fallback: strip path-like substrings (absolute paths).
    let stripped = s
        .split_whitespace()
        .map(|w| {
            if (w.starts_with('/') || w.len() > 2 && &w[1..3] == ":\\")
                && (w.contains('.') || w.len() > 5)
            {
                Path::new(w)
                    .file_name()
                    .and_then(|n| n.to_str())
                    .map(String::from)
                    .unwrap_or_else(|| "[path]".to_string())
            } else {
                w.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join(" ");
    if stripped.is_empty() || stripped.len() < 3 {
        "An error occurred.".to_string()
    } else {
        stripped
    }
}

/// Verify a signed image and return C2PA manifest status and author.
#[tauri::command]
fn verify_image(input_path: String) -> Result<VerificationResult, String> {
    c2pa_core::verify_image(&input_path).map_err(sanitize_error_for_frontend)
}

/// Load a local image file and return a data URL for robust webview preview.
#[tauri::command]
fn load_image_data_url(input_path: String) -> Result<String, String> {
    let bytes = fs::read(&input_path).map_err(sanitize_error_for_frontend)?;
    let ext = Path::new(&input_path)
        .extension()
        .and_then(|s| s.to_str())
        .map(|s| s.to_ascii_lowercase())
        .unwrap_or_default();
    let mime = match ext.as_str() {
        "jpg" | "jpeg" => "image/jpeg",
        "png" => "image/png",
        "webp" => "image/webp",
        "gif" => "image/gif",
        "bmp" => "image/bmp",
        "tif" | "tiff" => "image/tiff",
        _ => "application/octet-stream",
    };
    let b64 = BASE64_STANDARD.encode(bytes);
    Ok(format!("data:{};base64,{}", mime, b64))
}

#[derive(Debug, Deserialize)]
struct SignImageFromVaultRequest {
    input_path: String,
    draft_id: i64,
    /// When set, use NFC assertion (uid, chip_family from vault) instead of draft.physical_hash.
    tag_id: Option<i64>,
    /// When true, embed blind provenance watermark before C2PA signing (must precede signing for content hash).
    #[serde(default)]
    invisible_watermark: bool,
}

#[derive(Debug, Serialize)]
struct SignImageFromVaultResponse {
    manifest_id: String,
    output_path: String,
    /// IPFS CID for the content (original file bytes). Present when CID was computed.
    ipfs_cid: Option<String>,
    /// Path to the original file that must be pinned to IPFS for the CID to resolve.
    input_path: Option<String>,
}

#[derive(Debug, Deserialize)]
struct TransferAssetRequest {
    input_path: String,
    recipient_contact_id: String,
}

#[derive(Debug, Serialize)]
struct TransferAssetResponse {
    output_path: String,
    manifest_id: String,
}

#[tauri::command]
fn list_nfc_readers() -> Result<Vec<String>, String> {
    nfc::list_readers().map_err(sanitize_error_for_frontend)
}

// ---------------------------------------------------------------------------
// NTAG 424 DNA debug/proof commands
// ---------------------------------------------------------------------------

const DEFAULT_SDM_BASE_URL: &str = "https://charm.farm/tap";

#[derive(Debug, Deserialize)]
struct PreflightTagRequest {
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
}

#[derive(Debug, Deserialize)]
struct DebugAuthRequest {
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
}

#[derive(Debug, Deserialize)]
struct BuildSdmTemplateRequest {
    base_url: Option<String>,
    policy: Option<enrollment::SdmTemplatePolicy>,
}

#[derive(Debug, Deserialize)]
struct DryRunChangeFileSettingsRequest {
    base_url: Option<String>,
    policy: Option<enrollment::SdmTemplatePolicy>,
    meta_read_key_no: Option<u8>,
    file_read_key_no: Option<u8>,
    /// If true, use NXP AN12196 example (known-good). If false, use custom template (experimental).
    use_nxp_example: Option<bool>,
}

#[tauri::command]
async fn preflight_tag_state(
    app: tauri::AppHandle,
    master_password: Option<String>,
    request: Option<PreflightTagRequest>,
) -> Result<nfc_debug::PreflightResult, String> {
    let vault_context = if let Some(master_password) = master_password.filter(|p| !p.trim().is_empty()) {
        let master_password = Zeroizing::new(master_password);
        let vault_dir = app_vault_dir(&app)?;
        let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str()).ok();
        Some((vault_dir, master_password, identity_id))
    } else {
        None
    };
    let request = request.unwrap_or(PreflightTagRequest {
        reader_name: None,
        timeout_ms: None,
    });
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(10_000);
    let vault_dir = vault_context.as_ref().map(|(dir, _, _)| dir.clone());
    let master_password = vault_context.as_ref().map(|(_, password, _)| password.clone());
    let active_identity_id = vault_context.as_ref().and_then(|(_, _, identity_id)| *identity_id);
    tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::preflight_tag_state(
            reader_name.as_deref(),
            timeout_ms,
            vault_dir.as_deref(),
            master_password.as_ref().map(|p| p.as_str()),
            active_identity_id,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join preflight task: {e}")))?
    .map_err(sanitize_error_for_frontend)
}

/// Read back enrollment state from a live tag: GetFileSettings(02), full NDEF bytes, classification.
/// Used as baseline reference and for regression testing.
#[tauri::command]
async fn read_back_enrollment_state(
    app: tauri::AppHandle,
    master_password: Option<String>,
    request: Option<PreflightTagRequest>,
) -> Result<nfc_debug::EnrollmentStateReadback, String> {
    let vault_context = if let Some(master_password) = master_password.filter(|p| !p.trim().is_empty()) {
        let master_password = Zeroizing::new(master_password);
        let vault_dir = app_vault_dir(&app)?;
        let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str()).ok();
        Some((vault_dir, master_password, identity_id))
    } else {
        None
    };
    let request = request.unwrap_or(PreflightTagRequest {
        reader_name: None,
        timeout_ms: None,
    });
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(10_000);
    let vault_dir = vault_context.as_ref().map(|(dir, _, _)| dir.clone());
    let master_password = vault_context.as_ref().map(|(_, password, _)| password.clone());
    let active_identity_id = vault_context.as_ref().and_then(|(_, _, identity_id)| *identity_id);
    tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::read_back_enrollment_state(
            reader_name.as_deref(),
            timeout_ms,
            vault_dir.as_deref(),
            master_password.as_ref().map(|p| p.as_str()),
            active_identity_id,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join read_back task: {e}")))?
    .map_err(sanitize_error_for_frontend)
}

/// Persist the given enrollment state as the known-good baseline (personalized_sdm reference).
#[tauri::command]
fn persist_enrollment_baseline(
    app: tauri::AppHandle,
    readback: nfc_debug::EnrollmentStateReadback,
) -> Result<(), String> {
    let baseline = nfc_debug::PersistedEnrollmentBaseline {
        saved_at: chrono::Utc::now().to_rfc3339(),
        readback: readback,
    };
    let mut path = app_vault_dir(&app)?;
    path.pop(); // vault -> app_data_dir
    path.push("ntag424_baseline.json");
    let json = serde_json::to_string_pretty(&baseline)
        .map_err(|e| format!("serialize baseline: {e}"))?;
    fs::write(&path, json).map_err(|e| format!("write baseline: {e}"))?;
    Ok(())
}

/// Load the persisted enrollment baseline, if any.
#[tauri::command]
fn get_enrollment_baseline(
    app: tauri::AppHandle,
) -> Result<Option<nfc_debug::PersistedEnrollmentBaseline>, String> {
    let mut path = app_vault_dir(&app)?;
    path.pop();
    path.push("ntag424_baseline.json");
    if !path.exists() {
        return Ok(None);
    }
    let json = fs::read_to_string(&path).map_err(|e| format!("read baseline: {e}"))?;
    let baseline: nfc_debug::PersistedEnrollmentBaseline =
        serde_json::from_str(&json).map_err(|e| format!("parse baseline: {e}"))?;
    Ok(Some(baseline))
}

#[tauri::command]
async fn debug_auth_ev2_first(
    request: Option<DebugAuthRequest>,
) -> Result<nfc_debug::AuthDebugResult, String> {
    let request = request.unwrap_or(DebugAuthRequest {
        reader_name: None,
        timeout_ms: None,
    });
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(10_000);
    tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::debug_auth_ev2_first(reader_name.as_deref(), timeout_ms)
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join auth debug task: {e}")))?
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn debug_build_sdm_template(
    request: Option<BuildSdmTemplateRequest>,
) -> Result<enrollment::SdmTemplateResult, String> {
    let request = request.unwrap_or(BuildSdmTemplateRequest {
        base_url: None,
        policy: None,
    });
    let base_url = request.base_url.as_deref().unwrap_or(DEFAULT_SDM_BASE_URL);
    let policy = request.policy.unwrap_or_default();
    Ok(nfc_debug::debug_build_sdm_template(base_url, &policy))
}

#[tauri::command]
fn debug_change_file_settings_dry_run(
    request: Option<DryRunChangeFileSettingsRequest>,
) -> Result<nfc_debug::DryRunChangeFileSettingsResult, String> {
    let request = request.unwrap_or(DryRunChangeFileSettingsRequest {
        base_url: None,
        policy: None,
        meta_read_key_no: None,
        file_read_key_no: None,
        use_nxp_example: None,
    });
    if request.use_nxp_example.unwrap_or(false) {
        Ok(nfc_debug::dry_run_change_file_settings_known_good())
    } else {
        let base_url = request.base_url.as_deref().unwrap_or(DEFAULT_SDM_BASE_URL);
        let policy = request.policy.unwrap_or_default();
        let meta_key = request.meta_read_key_no.unwrap_or(0x01);
        let file_key = request.file_read_key_no.unwrap_or(0x02);
        Ok(nfc_debug::dry_run_change_file_settings(base_url, &policy, meta_key, file_key))
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExecuteDebugRequest {
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
    base_url: Option<String>,
    /// If true, use exact NXP AN12196 CmdData (4000E0C1F121200000430000430000) for serializer verification.
    use_nxp_example: Option<bool>,
    /// When use_nxp_example=false: if true, use NXP-style access_rights/SDM refs (debug 919E).
    use_nxp_compat_for_custom: Option<bool>,
    /// When use_nxp_example=false: if true, use NXP offsets 32,67,67 instead of template offsets.
    use_nxp_offsets_for_custom: Option<bool>,
    /// If true, use CmdCtr=0 for first command (some sources say first command uses 0).
    try_cmd_ctr_zero: Option<bool>,
}

#[tauri::command]
async fn debug_change_file_settings_execute(
    request: Option<ExecuteDebugRequest>,
) -> Result<nfc_debug::ChangeFileSettingsExecuteResult, String> {
    let request = request.unwrap_or(ExecuteDebugRequest {
        reader_name: None,
        timeout_ms: None,
        base_url: None,
        use_nxp_example: None,
        use_nxp_compat_for_custom: None,
        use_nxp_offsets_for_custom: None,
        try_cmd_ctr_zero: None,
    });
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(12_000);
    let base_url = request.base_url.unwrap_or_else(|| DEFAULT_SDM_BASE_URL.to_string());
    let use_nxp_example = request.use_nxp_example.unwrap_or(true);
    let use_nxp_compat_for_custom = request.use_nxp_compat_for_custom.unwrap_or(false);
    let use_nxp_offsets_for_custom = request.use_nxp_offsets_for_custom.unwrap_or(false);
    let try_cmd_ctr_zero = request.try_cmd_ctr_zero.unwrap_or(true);
    tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::debug_change_file_settings_execute(
            reader_name.as_deref(),
            timeout_ms,
            &base_url,
            use_nxp_example,
            use_nxp_compat_for_custom,
            use_nxp_offsets_for_custom,
            try_cmd_ctr_zero,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join execute task: {e}")))?
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
async fn debug_write_ndef_template_execute(
    request: Option<ExecuteDebugRequest>,
) -> Result<nfc_debug::WriteNdefExecuteResult, String> {
    let request = request.unwrap_or(ExecuteDebugRequest {
        reader_name: None,
        timeout_ms: None,
        base_url: None,
        use_nxp_example: None,
        use_nxp_compat_for_custom: None,
        use_nxp_offsets_for_custom: None,
        try_cmd_ctr_zero: None,
    });
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(12_000);
    let base_url = request.base_url.unwrap_or_else(|| DEFAULT_SDM_BASE_URL.to_string());
    tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::debug_write_ndef_template_execute(
            reader_name.as_deref(),
            timeout_ms,
            &base_url,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join execute task: {e}")))?
    .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VerifySdmLayoutRequest {
    ndef_file_hex: String,
    picc_data_offset: Option<u32>,
    mac_input_offset: u32,
    mac_offset: u32,
}

#[tauri::command]
fn debug_verify_sdm_layout(
    request: VerifySdmLayoutRequest,
) -> Result<nfc_debug::VerifySdmLayoutResult, String> {
    Ok(nfc_debug::debug_verify_sdm_layout(
        &request.ndef_file_hex,
        request.picc_data_offset,
        request.mac_input_offset,
        request.mac_offset,
    ))
}

/// Import a PEM private key into encrypted vault keyring storage.
#[tauri::command]
fn import_signing_key(
    app: tauri::AppHandle,
    master_password: String,
    key_file_path: String,
) -> Result<i64, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::import_private_key_from_file(&vault_dir, master_password.as_str(), &key_file_path)
        .map_err(sanitize_error_for_frontend)
}

/// Validate that the provided password can unlock the existing vault.
#[tauri::command]
fn validate_vault_password(app: tauri::AppHandle, master_password: String) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::verify_vault_password(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
struct GenerateIdentityRequest {
    name: String,
    organization: String,
    country: String,
}

#[derive(Debug, Deserialize)]
struct GenerateCsrRequest {
    identity_id: i64,
    name: String,
    organization: String,
    country: String,
    output_path: String,
}

#[derive(Debug, Deserialize)]
struct ImportCaCertificateRequest {
    identity_id: i64,
    certificate_chain_path: String,
}

#[derive(Debug, Deserialize)]
struct ExportPrivateKeyBackupRequest {
    identity_id: i64,
    output_path: String,
}

#[derive(Debug, Deserialize)]
struct ExportPublicCertificateRequest {
    output_path: String,
}

#[derive(Debug, Deserialize)]
struct ImportContactCertificateRequest {
    file_path: String,
}

#[derive(Debug, Deserialize)]
struct DeleteTrustedContactRequest {
    contact_id: String,
}

#[derive(Debug, Deserialize)]
struct ExportContactCertificateRequest {
    contact_id: String,
    output_path: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateContactNotesRequest {
    contact_id: String,
    notes: Option<String>,
}

#[derive(Deserialize)]
struct SignMessageRequest {
    message: String,
}

#[derive(Serialize)]
struct SignMessageResponse {
    pem: String,
}

#[derive(Deserialize)]
struct VerifySignedMessageRequest {
    pem: String,
}

#[derive(Serialize)]
struct SignerInfo {
    common_name: String,
    organization: String,
    fingerprint: String,
}

#[derive(Serialize)]
struct VerifySignedMessageResponse {
    message: String,
    signer: SignerInfo,
    is_trusted_contact: bool,
    certificate_expired: bool,
}

#[derive(Deserialize)]
struct AddContactFromSignedMessageRequest {
    pem: String,
}

#[tauri::command]
fn generate_sovereign_identity(
    app: tauri::AppHandle,
    master_password: String,
    request: GenerateIdentityRequest,
) -> Result<i64, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::generate_sovereign_identity(
        &vault_dir,
        master_password.as_str(),
        &request.name,
        &request.organization,
        &request.country,
    )
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn list_identities(
    app: tauri::AppHandle,
    master_password: String,
) -> Result<Vec<vault::IdentityRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::list_identities(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn set_active_identity(
    app: tauri::AppHandle,
    master_password: String,
    identity_id: i64,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::set_active_identity(&vault_dir, master_password.as_str(), identity_id)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn generate_csr(
    app: tauri::AppHandle,
    master_password: String,
    request: GenerateCsrRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::generate_csr_to_file(
        &vault_dir,
        master_password.as_str(),
        request.identity_id,
        &request.name,
        &request.organization,
        &request.country,
        &request.output_path,
    )
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn import_ca_certificate(
    app: tauri::AppHandle,
    master_password: String,
    request: ImportCaCertificateRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::import_ca_certificate(
        &vault_dir,
        master_password.as_str(),
        request.identity_id,
        &request.certificate_chain_path,
    )
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn export_private_key_backup(
    app: tauri::AppHandle,
    master_password: String,
    request: ExportPrivateKeyBackupRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::export_private_key_backup(
        &vault_dir,
        master_password.as_str(),
        request.identity_id,
        &request.output_path,
    )
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn export_public_certificate(
    app: tauri::AppHandle,
    master_password: String,
    request: ExportPublicCertificateRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::export_public_certificate(&vault_dir, master_password.as_str(), &request.output_path)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn import_contact_certificate(
    app: tauri::AppHandle,
    master_password: String,
    request: ImportContactCertificateRequest,
) -> Result<String, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::import_contact_certificate(&vault_dir, master_password.as_str(), &request.file_path)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn list_trusted_contacts(
    app: tauri::AppHandle,
    master_password: String,
) -> Result<Vec<vault::TrustedContact>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::list_trusted_contacts(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn delete_trusted_contact(
    app: tauri::AppHandle,
    master_password: String,
    request: DeleteTrustedContactRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::delete_trusted_contact(&vault_dir, master_password.as_str(), &request.contact_id)
        .map_err(sanitize_error_for_frontend)
}

/// Export a trusted contact's certificate to a .crt file (same format as imported).
#[tauri::command]
fn export_contact_certificate(
    app: tauri::AppHandle,
    master_password: String,
    request: ExportContactCertificateRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let contact = vault::get_trusted_contact_by_id(
        &vault_dir,
        master_password.as_str(),
        &request.contact_id,
    )
    .map_err(sanitize_error_for_frontend)?;
    fs::write(&request.output_path, &contact.certificate_pem)
        .map_err(|e| sanitize_error_for_frontend(e))
}

#[tauri::command]
fn update_contact_notes(
    app: tauri::AppHandle,
    master_password: String,
    request: UpdateContactNotesRequest,
) -> Result<vault::TrustedContact, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::update_contact_notes(
        &vault_dir,
        master_password.as_str(),
        &request.contact_id,
        request.notes.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn sign_message(
    app: tauri::AppHandle,
    master_password: String,
    request: SignMessageRequest,
) -> Result<SignMessageResponse, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity = vault::get_active_identity_material(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    let pem = signed_messages::sign_message(
        &request.message,
        &identity.private_key_pem,
        &identity.certificate_chain_pem,
    )
    .map_err(sanitize_error_for_frontend)?;
    Ok(SignMessageResponse { pem })
}

#[tauri::command]
fn verify_signed_message(
    app: tauri::AppHandle,
    master_password: String,
    request: VerifySignedMessageRequest,
) -> Result<VerifySignedMessageResponse, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let (message, common_name, organization, fingerprint, certificate_expired) =
        signed_messages::verify_signed_message(&request.pem).map_err(sanitize_error_for_frontend)?;
    let is_trusted_contact = vault::find_trusted_contact_by_fingerprint(
        &vault_dir,
        master_password.as_str(),
        &fingerprint,
    )
    .ok()
    .flatten()
    .is_some();
    Ok(VerifySignedMessageResponse {
        message,
        signer: SignerInfo {
            common_name: common_name.unwrap_or_else(|| "Unknown".to_string()),
            organization: organization.unwrap_or_default(),
            fingerprint,
        },
        is_trusted_contact,
        certificate_expired,
    })
}

#[tauri::command]
fn add_contact_from_signed_message(
    app: tauri::AppHandle,
    master_password: String,
    request: AddContactFromSignedMessageRequest,
) -> Result<vault::TrustedContact, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let cert_pem = signed_messages::extract_signer_certificate_pem(&request.pem)
        .map_err(sanitize_error_for_frontend)?;
    let contact_id = vault::import_contact_certificate_from_pem(
        &vault_dir,
        master_password.as_str(),
        &cert_pem,
    )
    .map_err(sanitize_error_for_frontend)?;
    vault::get_trusted_contact_by_id(&vault_dir, master_password.as_str(), &contact_id)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn pick_open_file_path(title: String, extensions: Vec<String>) -> Result<Option<String>, String> {
    let mut dialog = FileDialog::new().set_title(&title);
    if !extensions.is_empty() {
        let exts: Vec<&str> = extensions.iter().map(String::as_str).collect();
        dialog = dialog.add_filter("files", &exts);
    }
    Ok(dialog.pick_file().map(|p| p.to_string_lossy().to_string()))
}

#[tauri::command]
fn pick_save_file_path(title: String, default_name: Option<String>) -> Result<Option<String>, String> {
    let mut dialog = FileDialog::new().set_title(&title);
    if let Some(name) = default_name {
        if !name.trim().is_empty() {
            dialog = dialog.set_file_name(&name);
        }
    }
    Ok(dialog.save_file().map(|p| p.to_string_lossy().to_string()))
}

/// Step size for QIM embedding. 45 survives JPEG 75% compression.
const WATERMARK_STEP_SIZE: f64 = 45.0;

/// Minimum image dimension for watermark (watermark-core requirement).
const WATERMARK_MIN_DIM: usize = 344;

/// Applies blind provenance watermark to an image and writes to a temp file.
/// Returns the temp path. Caller must use this path for C2PA signing (watermark must precede signing).
fn apply_watermark_to_temp(
    input_path: &str,
    private_key_pem: &[u8],
    asset_id: i64,
) -> Result<String, String> {
    let pem_str = std::str::from_utf8(private_key_pem)
        .map_err(|_| "private key is not valid UTF-8 PEM".to_string())?;
    let signing_key = SigningKey::from_pkcs8_pem(pem_str)
        .map_err(|e| format!("parse private key for watermark: {e}"))?;
    let verifying_key = signing_key.verifying_key();
    let encoded = verifying_key.to_encoded_point(true);
    let compressed_bytes = encoded.as_bytes();
    let mut compressed_pubkey = [0u8; 33];
    if compressed_bytes.len() != 33 {
        return Err("expected 33-byte compressed P-256 public key".to_string());
    }
    compressed_pubkey.copy_from_slice(compressed_bytes);

    let asset_id_u32 = asset_id
        .try_into()
        .map_err(|_| "draft id out of range for watermark asset_id (u32)".to_string())?;

    let provenance = ProvenancePayload {
        version: 1,
        compressed_pubkey,
        asset_id: asset_id_u32,
        checksum: 0,
    };

    let img = ImageReader::open(input_path)
        .map_err(|e| format!("open image for watermark: {e}"))?
        .decode()
        .map_err(|e| format!("decode image for watermark: {e}"))?;
    let mut rgb = img.to_rgb8();
    let (w, h) = rgb.dimensions();
    if (w as usize) < WATERMARK_MIN_DIM || (h as usize) < WATERMARK_MIN_DIM {
        return Err(format!(
            "image too small for invisible watermark (min {}×{} px, got {}×{})",
            WATERMARK_MIN_DIM, WATERMARK_MIN_DIM, w, h
        ));
    }

    apply_watermark(&mut rgb, &provenance, WATERMARK_STEP_SIZE)
        .map_err(|e| format!("apply watermark: {e}"))?;

    // Save as PNG for compatibility with img_hash (image 0.23) perceptual hash decoder.
    // JPEG from image 0.25 can fail to decode in older image crate versions.
    let temp_path = std::env::temp_dir()
        .join(format!("trust-systems-wm-{}.png", uuid::Uuid::new_v4()));
    rgb.save(&temp_path)
        .map_err(|e| format!("save watermarked image: {e}"))?;

    Ok(temp_path.to_string_lossy().to_string())
}

/// Sign image by selecting metadata from a vault draft and using vault keyring private key.
#[tauri::command]
async fn sign_image_from_vault(
    app: tauri::AppHandle,
    master_password: String,
    request: SignImageFromVaultRequest,
) -> Result<SignImageFromVaultResponse, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let draft = vault::get_draft_for_signing(&vault_dir, master_password.as_str(), request.draft_id)
        .map_err(sanitize_error_for_frontend)?;
    let identity =
        vault::get_active_identity_material(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)?;

    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;

    // When invisible watermark is enabled, apply it first (before C2PA) so the content hash binds correctly.
    let effective_input_path = if request.invisible_watermark {
        let private_key_pem = identity.private_key_pem.clone();
        let input_path = request.input_path.clone();
        let draft_id = draft.id;
        tauri::async_runtime::spawn_blocking(move || {
            apply_watermark_to_temp(&input_path, &private_key_pem, draft_id)
        })
        .await
        .map_err(|e| sanitize_error_for_frontend(format!("join watermark task: {e}")))?
        .map_err(sanitize_error_for_frontend)?
    } else {
        request.input_path.clone()
    };

    let input_bytes = fs::read(&effective_input_path).map_err(sanitize_error_for_frontend)?;
    let perceptual_hash = tauri::async_runtime::spawn_blocking(move || {
        fingerprint::generate_phash_checked(&input_bytes)
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join fingerprint task: {e}")))?
    .map_err(sanitize_error_for_frontend)?;
    vault::update_draft_perceptual_hash(&vault_dir, master_password.as_str(), draft.id, &perceptual_hash)
        .map_err(sanitize_error_for_frontend)?;

    let output_path = if request.invisible_watermark {
        sealed_output_path_with_ext(&request.input_path, identity.is_production, Some("png"))?
    } else {
        sealed_output_path(&request.input_path, identity.is_production)?
    };
    let output_path_str = output_path.to_string_lossy().to_string();
    let signing_chain = if identity.is_production {
        c2pa_core::ensure_chain_includes_trust_anchor(&identity.certificate_chain_pem)
    } else {
        identity.certificate_chain_pem.clone()
    };

    let anchor = if let Some(tag_id) = request.tag_id {
        let tag = vault::get_tag_by_id(&vault_dir, master_password.as_str(), tag_id)
            .map_err(sanitize_error_for_frontend)?;
        if tag.identity_id != Some(identity_id) {
            return Err("Selected tag is not controlled by the active identity.".to_string());
        }
        if tag.artwork_id.is_some() {
            return Err("Selected tag is already linked to an artwork.".to_string());
        }
        let chip_family = tag
            .tag_family
            .as_deref()
            .or(tag.chip_subtype.as_deref())
            .unwrap_or("NTAG 424 DNA")
            .to_string();
        Some(c2pa_core::PhysicalAnchorSource::Nfc {
            uid: tag.uid.clone(),
            chip_family,
        })
    } else {
        draft
            .physical_hash
            .as_deref()
            .filter(|h| !h.trim().is_empty())
            .map(c2pa_core::PhysicalAnchorSource::Manual)
    };

    let ipfs_cid = c2pa_core::extract_c2pa_content_bytes(&effective_input_path)
        .ok()
        .and_then(|bytes| c2pa_core::compute_ipfs_cid_from_bytes(&bytes).ok());

    let extras = c2pa_core::CreativeWorkExtras {
        description: draft.description.as_deref(),
        date_created: draft.date_created.as_deref(),
        license: draft.license.as_deref(),
        keywords: draft.keywords.as_deref(),
        genre: draft.genre.as_deref(),
        material: draft.material.as_deref(),
        contributor: draft.contributor.as_deref(),
        abstract_: draft.abstract_.as_deref(),
        ipfs_cid: ipfs_cid.as_deref().filter(|s| !s.trim().is_empty()),
        perceptual_hash: Some(perceptual_hash.as_str()),
    };
    c2pa_core::sign_image_with_materials_and_anchor(
        &effective_input_path,
        &output_path_str,
        &draft.artifact_title,
        &draft.author,
        extras,
        anchor,
        &signing_chain,
        &identity.private_key_pem,
    )
    .map_err(sanitize_error_for_frontend)?;

    let verify = c2pa_core::verify_image(&output_path_str).map_err(sanitize_error_for_frontend)?;
    let manifest_id = verify
        .instance_id
        .unwrap_or_else(|| "unknown-instance-id".to_string());
    let claim_id = verify
        .manifest_history
        .iter()
        .find(|e| e.is_active)
        .and_then(|e| e.label.as_deref())
        .filter(|s| s.starts_with("urn:"));
    vault::mark_draft_certified(
        &vault_dir,
        master_password.as_str(),
        draft.id,
        Some(&manifest_id),
        claim_id,
        ipfs_cid.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)?;
    let _ = vault::update_draft_asset_path_for_draft(
        &vault_dir,
        master_password.as_str(),
        draft.id,
        &request.input_path,
        &output_path_str,
    );

    if let Some(tag_id) = request.tag_id {
        vault::link_tag_to_artwork(
            &vault_dir,
            master_password.as_str(),
            tag_id,
            draft.id,
            identity_id,
        )
        .map_err(sanitize_error_for_frontend)?;
        let tag = vault::get_tag_by_id(&vault_dir, master_password.as_str(), tag_id)
            .map_err(sanitize_error_for_frontend)?;
        let physical_hash = format!("nfc:uid:{}", tag.uid.to_uppercase());
        vault::update_draft_physical_hash(&vault_dir, master_password.as_str(), draft.id, Some(&physical_hash))
            .map_err(sanitize_error_for_frontend)?;
    }

    if request.invisible_watermark && effective_input_path != request.input_path {
        let _ = fs::remove_file(&effective_input_path);
    }

    Ok(SignImageFromVaultResponse {
        manifest_id,
        output_path: output_path_str,
        ipfs_cid: ipfs_cid.filter(|s| !s.trim().is_empty()),
        input_path: Some(request.input_path.clone()),
    })
}

fn sealed_output_path(input_path: &str, is_charmed: bool) -> Result<PathBuf, String> {
    let ext = Path::new(input_path).extension().and_then(|s| s.to_str());
    sealed_output_path_with_ext(input_path, is_charmed, ext)
}

fn sealed_output_path_with_ext(
    input_path: &str,
    is_charmed: bool,
    ext: Option<&str>,
) -> Result<PathBuf, String> {
    let input = Path::new(input_path);
    let parent = input
        .parent()
        .ok_or_else(|| "invalid input path".to_string())?;
    let stem = input
        .file_stem()
        .and_then(|s| s.to_str())
        .ok_or_else(|| "invalid input filename".to_string())?;

    let suffix = if is_charmed { "charmed" } else { "sealed" };
    let with_suffix = |n: usize| -> String {
        if n == 0 {
            format!("{stem}_{suffix}")
        } else {
            format!("{stem}_{suffix}_{n}")
        }
    };

    for n in 0..1000 {
        let filename = match ext {
            Some(ext) if !ext.trim().is_empty() => format!("{}.{}", with_suffix(n), ext),
            _ => with_suffix(n),
        };
        let candidate = parent.join(filename);
        if !candidate.exists() {
            return Ok(candidate);
        }
    }
    Err("unable to allocate a non-conflicting sealed output filename".to_string())
}

fn transfer_output_path(input_path: &str) -> Result<PathBuf, String> {
    let input = Path::new(input_path);
    let parent = input
        .parent()
        .ok_or_else(|| "invalid input path".to_string())?;
    let stem = input
        .file_stem()
        .and_then(|s| s.to_str())
        .ok_or_else(|| "invalid input filename".to_string())?;
    let ext = input.extension().and_then(|s| s.to_str());

    let with_suffix = |n: usize| -> String {
        if n == 0 {
            format!("{stem}_transferred")
        } else {
            format!("{stem}_transferred_{n}")
        }
    };

    for n in 0..1000 {
        let filename = match ext {
            Some(ext) if !ext.trim().is_empty() => format!("{}.{}", with_suffix(n), ext),
            _ => with_suffix(n),
        };
        let candidate = parent.join(filename);
        if !candidate.exists() {
            return Ok(candidate);
        }
    }
    Err("unable to allocate a non-conflicting transfer output filename".to_string())
}

#[tauri::command]
async fn transfer_asset(
    app: tauri::AppHandle,
    master_password: String,
    request: TransferAssetRequest,
) -> Result<TransferAssetResponse, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let contact = vault::get_trusted_contact_by_id(
        &vault_dir,
        master_password.as_str(),
        &request.recipient_contact_id,
    )
    .map_err(sanitize_error_for_frontend)?;
    let fingerprint = vault::certificate_fingerprint_sha256(&contact.certificate_pem)
        .map_err(sanitize_error_for_frontend)?;
    let identity =
        vault::get_active_identity_material(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)?;
    let output_path = transfer_output_path(&request.input_path)?;
    let output_path_str = output_path.to_string_lossy().to_string();
    let signing_chain = if identity.is_production {
        c2pa_core::ensure_chain_includes_trust_anchor(&identity.certificate_chain_pem)
    } else {
        identity.certificate_chain_pem.clone()
    };
    c2pa_core::transfer_asset_with_materials(
        &request.input_path,
        &output_path_str,
        &contact.common_name,
        &signing_chain,
        &identity.private_key_pem,
        Some(&fingerprint),
    )
    .map_err(sanitize_error_for_frontend)?;

    if let Ok(Some(draft_id)) =
        vault::find_draft_id_by_asset_path(&vault_dir, master_password.as_str(), &request.input_path)
    {
        let _ = vault::update_draft_asset_path_for_draft(
            &vault_dir,
            master_password.as_str(),
            draft_id,
            &request.input_path,
            &output_path_str,
        );
    }

    let verify = c2pa_core::verify_image(&output_path_str).map_err(sanitize_error_for_frontend)?;
    let manifest_id = verify
        .instance_id
        .unwrap_or_else(|| "unknown-instance-id".to_string());
    Ok(TransferAssetResponse {
        output_path: output_path_str,
        manifest_id,
    })
}

#[derive(Debug, Serialize)]
struct VerifyWithVaultResult {
    verification: VerificationResult,
    vault_status: String,
    correlation_method: String,
    draft_id: Option<i64>,
    artifact_title: Option<String>,
    artifact_author: Option<String>,
    /// IPFS CID for display (from manifest, or vault when manifest lacks it).
    ipfs_cid: Option<String>,
    perceptual_threshold: u32,
    similar_matches: Vec<vault::SimilarAssetMatch>,
    perceptual_error: Option<String>,
    /// Matched trusted contact (by signer certificate fingerprint) if found.
    publisher_verified_contact: Option<String>,
    /// True when the signer certificate matches a vault identity (i.e. signed by this app).
    is_signed_by_vault: bool,
}

#[derive(Debug, Serialize)]
struct WatermarkVerification {
    detected: bool,
    asset_id: Option<i64>,
    step_size: Option<f64>,
    matched_identity_id: Option<i64>,
    matched_identity_name: Option<String>,
}

fn detect_watermark_for_vault(
    input_bytes: &[u8],
    vault_dir: &Path,
    master_password: &str,
) -> WatermarkVerification {
    let dynamic = match image::load_from_memory(input_bytes) {
        Ok(img) => img,
        Err(_) => {
            return WatermarkVerification {
                detected: false,
                asset_id: None,
                step_size: None,
                matched_identity_id: None,
                matched_identity_name: None,
            }
        }
    };
    let rgb = dynamic.to_rgb8();
    let (payload, step_size) = match extract_watermark(&rgb) {
        Ok(v) => v,
        Err(_) => {
            return WatermarkVerification {
                detected: false,
                asset_id: None,
                step_size: None,
                matched_identity_id: None,
                matched_identity_name: None,
            }
        }
    };

    let identity_match =
        vault::find_identity_by_watermark_pubkey(vault_dir, master_password, &payload.compressed_pubkey).ok().flatten();

    WatermarkVerification {
        detected: true,
        asset_id: Some(i64::from(payload.asset_id)),
        step_size: Some(step_size),
        matched_identity_id: identity_match.as_ref().map(|m| m.identity_id),
        matched_identity_name: identity_match.map(|m| m.display_name),
    }
}

/// Verify image and correlate extracted physical hash against local vault records.
#[tauri::command]
async fn verify_image_with_vault(
    app: tauri::AppHandle,
    master_password: String,
    input_path: String,
    threshold: Option<u32>,
) -> Result<VerifyWithVaultResult, String> {
    let master_password = Zeroizing::new(master_password);
    let verification = c2pa_core::verify_image(&input_path).map_err(sanitize_error_for_frontend)?;
    let vault_dir = app_vault_dir(&app)?;
    let threshold = threshold.unwrap_or(10);
    let correlation = if verification
        .physical_hash
        .as_deref()
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .is_some()
    {
        vault::correlate_by_physical_hash(&vault_dir, master_password.as_str(), verification.physical_hash.as_deref())
            .map_err(sanitize_error_for_frontend)?
    } else {
        vault::VaultCorrelation {
            status: "Orphan".to_string(),
            draft_id: None,
            artifact_title: None,
            author: None,
        }
    };

    let mut vault_status = correlation.status.clone();
    let mut correlation_method = if correlation.status == "Match" {
        "physical_hash".to_string()
    } else {
        "none".to_string()
    };
    let mut draft_id = correlation.draft_id;
    let mut artifact_title = correlation.artifact_title;
    let mut artifact_author = correlation.author;
    let mut similar_matches = Vec::new();
    let mut perceptual_error = None;
    let mut publisher_verified_contact = None;
    let mut is_signed_by_vault = false;

    if let Some(ref fp) = verification.signer_certificate_fingerprint {
        if let Ok(Some(contact)) =
            vault::find_trusted_contact_by_fingerprint(&vault_dir, master_password.as_str(), fp)
        {
            publisher_verified_contact = Some(contact.common_name);
        }
        is_signed_by_vault = vault::signer_matches_vault_identity(
            &vault_dir,
            master_password.as_str(),
            fp,
        )
        .unwrap_or(false);
    }

    if vault_status != "Match" {
        match fs::read(&input_path) {
            Ok(input_bytes) => {
                let vault_dir_bg = vault_dir.clone();
                let password_bg = master_password.clone();
                match tauri::async_runtime::spawn_blocking(move || {
                    vault::find_similar_assets(&vault_dir_bg, password_bg.as_str(), &input_bytes, threshold)
                })
                .await
                {
                    Ok(Ok(matches)) => {
                        similar_matches = matches;
                    }
                    Ok(Err(e)) => {
                        perceptual_error = Some(sanitize_error_for_frontend(&e));
                    }
                    Err(e) => {
                        perceptual_error = Some(sanitize_error_for_frontend(format!(
                            "join similar-assets task: {e}"
                        )));
                    }
                }
            }
            Err(e) => {
                perceptual_error = Some(sanitize_error_for_frontend(format!(
                    "read image for perceptual hash: {e}"
                )));
            }
        }

        if let Some(best) = similar_matches.first() {
            vault_status = "PerceptualMatch".to_string();
            correlation_method = "perceptual_hash".to_string();
            draft_id = Some(best.draft_id);
            artifact_title = Some(best.artifact_title.clone());
            artifact_author = Some(best.author.clone());
        } else if perceptual_error.is_some() && verification.physical_hash.is_some() {
            correlation_method = "physical_then_perceptual_unavailable".to_string();
        } else if perceptual_error.is_some() {
            correlation_method = "perceptual_unavailable".to_string();
        } else if verification.physical_hash.is_some() {
            correlation_method = "physical_then_perceptual".to_string();
        } else {
            correlation_method = "perceptual_hash".to_string();
        }
    }

    let vault_draft = draft_id
        .and_then(|id| vault::get_draft_by_id(&vault_dir, master_password.as_str(), id).ok())
        .flatten();

    let ipfs_cid = verification.ipfs_cid.clone().or_else(|| {
        vault_draft.as_ref().and_then(|d| d.ipfs_cid.as_ref().map(|c| format!("ipfs://{c}")))
    });

    let mut verification = verification;
    if verification.physical_hash.is_none() {
        if let Some(ref d) = vault_draft {
            if let Some(ref ph) = d.physical_hash {
                if !ph.trim().is_empty() {
                    verification.physical_hash = Some(ph.clone());
                }
            }
        }
    }

    Ok(VerifyWithVaultResult {
        verification,
        vault_status,
        correlation_method,
        draft_id,
        artifact_title,
        artifact_author,
        ipfs_cid,
        publisher_verified_contact,
        is_signed_by_vault,
        perceptual_threshold: threshold,
        similar_matches,
        perceptual_error,
    })
}

/// Detect invisible watermark and correlate watermark public key against vault identities.
#[tauri::command]
async fn check_image_watermark_with_vault(
    app: tauri::AppHandle,
    master_password: String,
    input_path: String,
) -> Result<WatermarkVerification, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let input_bytes = fs::read(&input_path).map_err(sanitize_error_for_frontend)?;
    tauri::async_runtime::spawn_blocking(move || {
        detect_watermark_for_vault(&input_bytes, &vault_dir, master_password.as_str())
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join watermark check task: {e}")))
}

#[derive(Debug, Deserialize)]
struct SaveDraftRequest {
    artifact_title: String,
    author: String,
    description: Option<String>,
    date_created: Option<String>,
    license: Option<String>,
    keywords: Option<String>,
    genre: Option<String>,
    material: Option<String>,
    contributor: Option<String>,
    #[serde(alias = "abstract")]
    abstract_: Option<String>,
    physical_hash: Option<String>,
    draft_status: String,
    asset_path: Option<String>,
}

#[derive(Debug, Deserialize)]
struct DeleteDraftRequest {
    #[serde(alias = "draftId")]
    draft_id: i64,
}

#[derive(Debug, Deserialize)]
struct DeleteIdentityRequest {
    identity_id: i64,
}

fn app_vault_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
    let mut dir = app
        .path()
        .app_data_dir()
        .map_err(|e| format!("resolve app data directory: {e}"))?;
    dir.push("vault");
    Ok(dir)
}

#[tauri::command]
fn get_vault_db_path(app: tauri::AppHandle) -> Result<String, String> {
    let mut db_path = app_vault_dir(&app)?;
    db_path.push("vault.db");
    Ok(db_path.to_string_lossy().to_string())
}

/// Initialize encrypted local vault using a password-derived key.
#[tauri::command]
fn init_vault(app: tauri::AppHandle, master_password: String) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::init_vault(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
struct ChangeVaultPasswordRequest {
    current_password: String,
    new_password: String,
}

/// Change the vault master password. Rekeys the database; caller should lock the app after success.
#[tauri::command]
fn change_vault_password(
    app: tauri::AppHandle,
    request: ChangeVaultPasswordRequest,
) -> Result<(), String> {
    let current = Zeroizing::new(request.current_password);
    let new_pw = Zeroizing::new(request.new_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::change_vault_password(&vault_dir, current.as_str(), new_pw.as_str())
        .map_err(sanitize_error_for_frontend)
}

/// Export vault to a zip file: vault.db, vault-meta.json, images/ (copies of referenced assets),
/// images/original_paths.txt, and README.md.
#[tauri::command]
fn export_vault(
    app: tauri::AppHandle,
    master_password: String,
    output_path: String,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::export_vault_to_zip(&vault_dir, master_password.as_str(), Path::new(&output_path))
        .map_err(sanitize_error_for_frontend)
}

/// Import vault from a Trust Systems export zip. Extracts vault.db and vault-meta.json to the
/// vault directory. Safe to call when vault is locked. Overwrites existing vault if present.
#[tauri::command]
fn import_vault(app: tauri::AppHandle, zip_path: String) -> Result<(), String> {
    let vault_dir = app_vault_dir(&app)?;
    vault::import_vault_from_zip(Path::new(&zip_path), &vault_dir)
        .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
struct UpdateDraftRequest {
    draft_id: i64,
    artifact_title: String,
    author: String,
    description: Option<String>,
    date_created: Option<String>,
    license: Option<String>,
    keywords: Option<String>,
    genre: Option<String>,
    material: Option<String>,
    contributor: Option<String>,
    #[serde(alias = "abstract")]
    abstract_: Option<String>,
    physical_hash: Option<String>,
    asset_path: Option<String>,
}

/// Update an existing unsealed draft's metadata.
#[tauri::command]
async fn update_draft(
    app: tauri::AppHandle,
    master_password: String,
    draft: UpdateDraftRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::update_draft(
        &vault_dir,
        master_password.as_str(),
        draft.draft_id,
        &draft.artifact_title,
        &draft.author,
        draft.description.as_deref(),
        draft.date_created.as_deref(),
        draft.license.as_deref(),
        draft.keywords.as_deref(),
        draft.genre.as_deref(),
        draft.material.as_deref(),
        draft.contributor.as_deref(),
        draft.abstract_.as_deref(),
        draft.physical_hash.as_deref(),
        draft.asset_path.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)
}

fn relink_draft_from_watermark(
    vault_dir: &Path,
    master_password: &str,
    asset_path: &str,
    input_bytes: &[u8],
) -> Result<Option<i64>, String> {
    let watermark = detect_watermark_for_vault(input_bytes, vault_dir, master_password);
    if !watermark.detected || watermark.matched_identity_id.is_none() {
        return Ok(None);
    }
    let Some(draft_id) = watermark.asset_id else {
        return Ok(None);
    };
    let Some((draft_id, was_deleted)) =
        vault::find_draft_by_id_with_deleted_state(vault_dir, master_password, draft_id)?
    else {
        return Ok(None);
    };
    if was_deleted {
        vault::restore_draft(vault_dir, master_password, draft_id, asset_path)?;
    } else {
        vault::update_draft_asset_path_by_id(vault_dir, master_password, draft_id, asset_path)?;
    }
    Ok(Some(draft_id))
}

/// Store a draft C2PA metadata record in the encrypted local vault.
/// If the file has C2PA manifest and matches an existing draft (including soft-deleted),
/// restores or relinks to that draft instead of creating a new one.
/// If there is no manifest, tries watermark-based draft relink for images that were
/// sealed with invisible watermark by a vault identity.
#[tauri::command]
async fn save_draft(
    app: tauri::AppHandle,
    master_password: String,
    draft: SaveDraftRequest,
) -> Result<i64, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;

    // If file path provided, check for C2PA manifest to restore/relink existing draft
    if let Some(path) = draft
        .asset_path
        .as_deref()
        .map(str::trim)
        .filter(|p| !p.is_empty())
    {
        if let Ok(verification) = c2pa_core::verify_image(path) {
            if let Some(manifest_id) = verification
                .instance_id
                .as_deref()
                .map(str::trim)
                .filter(|s| !s.is_empty())
            {
                if let Some((draft_id, was_deleted)) = vault::find_draft_by_manifest_id(
                    &vault_dir,
                    master_password.as_str(),
                    manifest_id,
                )
                .map_err(sanitize_error_for_frontend)?
                {
                    if was_deleted {
                        vault::restore_draft(&vault_dir, master_password.as_str(), draft_id, path)
                            .map_err(sanitize_error_for_frontend)?;
                    } else {
                        vault::update_draft_asset_path_by_id(
                            &vault_dir,
                            master_password.as_str(),
                            draft_id,
                            path,
                        )
                        .map_err(sanitize_error_for_frontend)?;
                    }
                    return Ok(draft_id);
                }
            }
        }

        // No manifest match: attempt watermark-based relink to an existing vault draft row.
        let image_bytes = fs::read(path).map_err(sanitize_error_for_frontend)?;
        let vault_dir_bg = vault_dir.clone();
        let password_bg = master_password.clone();
        let path_bg = path.to_string();
        let image_bytes_bg = image_bytes.clone();
        if let Some(draft_id) = tauri::async_runtime::spawn_blocking(move || {
            relink_draft_from_watermark(&vault_dir_bg, password_bg.as_str(), &path_bg, &image_bytes_bg)
        })
        .await
        .map_err(|e| sanitize_error_for_frontend(format!("join watermark relink task: {e}")))?
        .map_err(sanitize_error_for_frontend)?
        {
            return Ok(draft_id);
        }
    }

    // No manifest match or unsealed file: create new draft
    let perceptual_hash = if let Some(path) = draft
        .asset_path
        .as_deref()
        .map(str::trim)
        .filter(|p| !p.is_empty())
    {
        let image_bytes = fs::read(path).map_err(sanitize_error_for_frontend)?;
        Some(
            tauri::async_runtime::spawn_blocking(move || {
                fingerprint::generate_phash_checked(&image_bytes)
            })
            .await
            .map_err(|e| sanitize_error_for_frontend(format!("join fingerprint task: {e}")))?
            .map_err(sanitize_error_for_frontend)?,
        )
    } else {
        None
    };
    vault::save_draft(
        &vault_dir,
        master_password.as_str(),
        &draft.artifact_title,
        &draft.author,
        draft.description.as_deref(),
        draft.date_created.as_deref(),
        draft.license.as_deref(),
        draft.keywords.as_deref(),
        draft.genre.as_deref(),
        draft.material.as_deref(),
        draft.contributor.as_deref(),
        draft.abstract_.as_deref(),
        draft.physical_hash.as_deref(),
        &draft.draft_status,
        perceptual_hash.as_deref(),
        draft.asset_path.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
struct FindSimilarAssetsRequest {
    input_path: String,
    threshold: Option<u32>,
}

/// Compute perceptual hash and find near-duplicate vault assets by Hamming distance.
#[tauri::command]
async fn find_similar_assets(
    app: tauri::AppHandle,
    master_password: String,
    request: FindSimilarAssetsRequest,
) -> Result<Vec<vault::SimilarAssetMatch>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let input_bytes = fs::read(&request.input_path).map_err(sanitize_error_for_frontend)?;
    let threshold = request.threshold.unwrap_or(10);
    tauri::async_runtime::spawn_blocking(move || {
        vault::find_similar_assets(&vault_dir, master_password.as_str(), &input_bytes, threshold)
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join similar-assets task: {e}")))?
    .map_err(sanitize_error_for_frontend)
}

/// Read draft C2PA metadata records from the encrypted local vault.
#[tauri::command]
fn list_drafts(app: tauri::AppHandle, master_password: String) -> Result<Vec<vault::DraftRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::list_drafts(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GetDraftByIdRequest {
    draft_id: i64,
}

#[tauri::command]
fn get_draft_by_id(
    app: tauri::AppHandle,
    master_password: String,
    request: GetDraftByIdRequest,
) -> Result<Option<vault::DraftRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::get_draft_by_id(&vault_dir, master_password.as_str(), request.draft_id)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn list_tags(app: tauri::AppHandle, master_password: String) -> Result<Vec<vault::TagRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::list_tags(&vault_dir, master_password.as_str()).map_err(sanitize_error_for_frontend)
}

/// Sealed artworks not yet linked to any tag. For "Link Artwork" dropdown on Tag page.
#[tauri::command]
fn list_sealed_artworks_for_linking(
    app: tauri::AppHandle,
    master_password: String,
) -> Result<Vec<vault::DraftRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    vault::list_sealed_artworks_for_linking(&vault_dir, master_password.as_str(), identity_id)
        .map_err(sanitize_error_for_frontend)
}

/// Remove artwork linkage from a tag (rollback helper for failed NFC writes).
#[tauri::command]
fn unlink_tag_artwork(
    app: tauri::AppHandle,
    master_password: String,
    request: UnlinkTagArtworkRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;

    let tag = vault::get_tag_by_id(&vault_dir, master_password.as_str(), request.tag_id)
        .map_err(sanitize_error_for_frontend)?;
    vault::unlink_tag_artwork(&vault_dir, master_password.as_str(), request.tag_id)
        .map_err(sanitize_error_for_frontend)?;
    if let Some(artwork_id) = tag.artwork_id {
        let _ = vault::update_draft_physical_hash(
            &vault_dir,
            master_password.as_str(),
            artwork_id,
            None,
        );
    }
    Ok(())
}

/// Tags enrolled by the active identity with no linked artwork. For Seal tag dropdown.
#[tauri::command]
fn list_tags_available_for_seal(
    app: tauri::AppHandle,
    master_password: String,
) -> Result<Vec<vault::TagRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    vault::list_tags_available_for_seal(&vault_dir, master_password.as_str(), identity_id)
        .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkTagToArtworkRequest {
    tag_id: i64,
    artwork_id: i64,
    identity_id: i64,
}

#[tauri::command]
fn link_tag_to_artwork(
    app: tauri::AppHandle,
    master_password: String,
    request: LinkTagToArtworkRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::link_tag_to_artwork(
        &vault_dir,
        master_password.as_str(),
        request.tag_id,
        request.artwork_id,
        request.identity_id,
    )
    .map_err(sanitize_error_for_frontend)?;

    let tag = vault::get_tag_by_id(&vault_dir, master_password.as_str(), request.tag_id)
        .map_err(sanitize_error_for_frontend)?;
    let physical_hash = format!("nfc:uid:{}", tag.uid.to_uppercase());
    vault::update_draft_physical_hash(
        &vault_dir,
        master_password.as_str(),
        request.artwork_id,
        Some(&physical_hash),
    )
    .map_err(sanitize_error_for_frontend)?;

    Ok(())
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UnlinkTagArtworkRequest {
    tag_id: i64,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GetTagByUidRequest {
    uid: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResetTagSdmRequest {
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
}

#[tauri::command]
async fn reset_tag_sdm(
    app: tauri::AppHandle,
    master_password: String,
    request: Option<ResetTagSdmRequest>,
) -> Result<nfc_debug::ResetTagSdmResult, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    let request = request.unwrap_or(ResetTagSdmRequest {
        reader_name: None,
        timeout_ms: None,
    });
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(12_000);
    let vault_dir_clone = vault_dir.clone();
    let password_clone = master_password.clone();
    let result = tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::reset_tag_sdm_execute(
            reader_name.as_deref(),
            timeout_ms,
            &vault_dir_clone,
            password_clone.as_str(),
            identity_id,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join reset task: {e}")))?
    .map_err(sanitize_error_for_frontend)?;

    if !result.success {
        return Err("Reset did not complete successfully.".to_string());
    }

    vault::mark_tag_reset_by_uid(
        &vault_dir,
        master_password.as_str(),
        &result.tag_uid,
        identity_id,
    )
    .map_err(sanitize_error_for_frontend)?;

    Ok(result)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnrollTagRequest {
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
    verification_url: String,
}

#[tauri::command]
async fn enroll_tag_with_identity(
    app: tauri::AppHandle,
    master_password: String,
    request: EnrollTagRequest,
) -> Result<vault::TagRecord, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let verification_url = request.verification_url.trim().to_string();
    if verification_url.is_empty() {
        return Err("Verification URL is required.".to_string());
    }
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(12_000);
    let verification_url_for_enroll = verification_url.clone();

    let vault_dir_clone = vault_dir.clone();
    let password_clone = master_password.clone();
    let result = tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::enroll_tag_execute(
            reader_name.as_deref(),
            timeout_ms,
            &verification_url_for_enroll,
            &vault_dir_clone,
            password_clone.as_str(),
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join enroll task: {e}")))?
    .map_err(sanitize_error_for_frontend)?;

    if !result.success {
        return Err("Enrollment did not complete successfully.".to_string());
    }

    vault::update_tag_enrolled_by_uid(
        &vault_dir,
        master_password.as_str(),
        &result.tag_uid,
        identity_id,
        Some(result.key_version as i32),
        Some(&verification_url),
    )
    .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpsertTagFromPreflightRequest {
    uid: String,
    classification: String,
    reader_name: Option<String>,
    tag_family: Option<String>,
    chip_subtype: Option<String>,
}

#[tauri::command]
fn upsert_tag_from_preflight(
    app: tauri::AppHandle,
    master_password: String,
    request: UpsertTagFromPreflightRequest,
) -> Result<vault::TagRecord, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::upsert_tag_from_preflight(
        &vault_dir,
        master_password.as_str(),
        &request.uid,
        &request.classification,
        request.reader_name.as_deref(),
        request.tag_family.as_deref(),
        request.chip_subtype.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn get_tag_by_uid(
    app: tauri::AppHandle,
    master_password: String,
    request: GetTagByUidRequest,
) -> Result<Option<vault::TagRecord>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::get_tag_by_uid(&vault_dir, master_password.as_str(), &request.uid)
        .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GetTagLastVerificationUrlRequest {
    tag_id: i64,
}

#[tauri::command]
fn get_tag_last_verification_url(
    app: tauri::AppHandle,
    master_password: String,
    request: GetTagLastVerificationUrlRequest,
) -> Result<Option<String>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::get_tag_last_verification_url(&vault_dir, master_password.as_str(), request.tag_id)
        .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateTagNotesRequest {
    tag_id: i64,
    notes: Option<String>,
}

#[tauri::command]
fn update_tag_notes(
    app: tauri::AppHandle,
    master_password: String,
    request: UpdateTagNotesRequest,
) -> Result<vault::TagRecord, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::update_tag_notes(
        &vault_dir,
        master_password.as_str(),
        request.tag_id,
        request.notes.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RotateTagKeysRequest {
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
}

#[tauri::command]
async fn rotate_tag_keys(
    app: tauri::AppHandle,
    master_password: String,
    request: RotateTagKeysRequest,
) -> Result<vault::TagRecord, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(12_000);
    let vault_dir_clone = vault_dir.clone();
    let password_clone = master_password.clone();
    let result = tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::rotate_tag_keys_execute(
            reader_name.as_deref(),
            timeout_ms,
            &vault_dir_clone,
            password_clone.as_str(),
            identity_id,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join rotate task: {e}")))?
    .map_err(sanitize_error_for_frontend)?;

    if !result.success {
        return Err("Key rotation did not complete successfully.".to_string());
    }

    vault::update_tag_key_version(
        &vault_dir,
        master_password.as_str(),
        &result.tag_uid,
        identity_id,
        result.key_version as i32,
    )
    .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RewriteTagRequest {
    base_url: String,
    reader_name: Option<String>,
    timeout_ms: Option<u64>,
}

#[tauri::command]
async fn rewrite_tag_ndef(
    app: tauri::AppHandle,
    master_password: String,
    request: RewriteTagRequest,
) -> Result<nfc_debug::WriteNdefExecuteResult, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let base_url = request.base_url.trim();
    if base_url.is_empty() {
        return Err("Verification URL is required.".to_string());
    }
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    let reader_name = request.reader_name;
    let timeout_ms = request.timeout_ms.unwrap_or(12_000);
    let vault_dir_clone = vault_dir.clone();
    let password_clone = master_password.clone();
    let base_url_clone = base_url.to_string();
    let result = tauri::async_runtime::spawn_blocking(move || {
        nfc_debug::rewrite_tag_ndef_execute(
            reader_name.as_deref(),
            timeout_ms,
            &base_url_clone,
            &vault_dir_clone,
            password_clone.as_str(),
            identity_id,
        )
    })
    .await
    .map_err(|e| sanitize_error_for_frontend(format!("join rewrite task: {e}")))?
    .map_err(sanitize_error_for_frontend)?;

    if result.success {
        if let Some(ref uid) = result.tag_uid {
            let _ = vault::update_tag_last_ndef_write(&vault_dir, master_password.as_str(), uid);
            let _ = vault::record_tag_verification_url_rewrite(
                &vault_dir,
                master_password.as_str(),
                uid,
                base_url,
            );
        }
    }
    Ok(result)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TakeTagControlRequest {
    uid: String,
    verification_url: Option<String>,
}

#[tauri::command]
fn take_tag_control(
    app: tauri::AppHandle,
    master_password: String,
    request: TakeTagControlRequest,
) -> Result<vault::TagRecord, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    vault::update_tag_enrolled_by_uid(
        &vault_dir,
        master_password.as_str(),
        &request.uid,
        identity_id,
        None, // preserve existing key_version when taking control
        request.verification_url.as_deref(),
    )
    .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgetTagRequest {
    tag_id: i64,
}

#[tauri::command]
fn forget_tag(
    app: tauri::AppHandle,
    master_password: String,
    request: ForgetTagRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::forget_tag(&vault_dir, master_password.as_str(), request.tag_id)
        .map_err(sanitize_error_for_frontend)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReleaseTagControlRequest {
    tag_id: i64,
}

#[tauri::command]
fn release_tag_control(
    app: tauri::AppHandle,
    master_password: String,
    request: ReleaseTagControlRequest,
) -> Result<vault::TagRecord, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    let identity_id = vault::get_active_identity_id(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)?;
    vault::release_tag_control(&vault_dir, master_password.as_str(), request.tag_id, identity_id)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn delete_draft(
    app: tauri::AppHandle,
    master_password: String,
    request: DeleteDraftRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::delete_draft(&vault_dir, master_password.as_str(), request.draft_id)
        .map_err(sanitize_error_for_frontend)
}

#[tauri::command]
fn delete_identity(
    app: tauri::AppHandle,
    master_password: String,
    request: DeleteIdentityRequest,
) -> Result<(), String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::delete_identity(&vault_dir, master_password.as_str(), request.identity_id)
        .map_err(sanitize_error_for_frontend)
}

/// Returns leaf certificate fingerprints for all vault identities.
#[tauri::command]
fn list_vault_identity_fingerprints(
    app: tauri::AppHandle,
    master_password: String,
) -> Result<Vec<String>, String> {
    let master_password = Zeroizing::new(master_password);
    let vault_dir = app_vault_dir(&app)?;
    vault::list_vault_identity_fingerprints(&vault_dir, master_password.as_str())
        .map_err(sanitize_error_for_frontend)
}

/// Indicates whether vault metadata and database files exist.
#[tauri::command]
fn is_vault_initialized(app: tauri::AppHandle) -> Result<bool, String> {
    let vault_dir = app_vault_dir(&app)?;
    Ok(vault::vault_initialized(&vault_dir))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            sign_image_from_vault,
            transfer_asset,
            verify_image,
            load_image_data_url,
            verify_image_with_vault,
            check_image_watermark_with_vault,
            init_vault,
            validate_vault_password,
            change_vault_password,
            import_signing_key,
            generate_sovereign_identity,
            list_identities,
            set_active_identity,
            generate_csr,
            import_ca_certificate,
            export_private_key_backup,
            export_vault,
            export_public_certificate,
            import_contact_certificate,
            sign_message,
            verify_signed_message,
            add_contact_from_signed_message,
            list_trusted_contacts,
            delete_trusted_contact,
            export_contact_certificate,
            update_contact_notes,
            pick_open_file_path,
            pick_save_file_path,
            list_nfc_readers,
            preflight_tag_state,
            read_back_enrollment_state,
            persist_enrollment_baseline,
            get_enrollment_baseline,
            debug_auth_ev2_first,
            debug_build_sdm_template,
            debug_change_file_settings_dry_run,
            debug_change_file_settings_execute,
            debug_write_ndef_template_execute,
            debug_verify_sdm_layout,
            import_vault,
            save_draft,
            update_draft,
            list_drafts,
            get_draft_by_id,
            list_tags,
            list_tags_available_for_seal,
            list_sealed_artworks_for_linking,
            link_tag_to_artwork,
            unlink_tag_artwork,
            get_tag_by_uid,
            get_tag_last_verification_url,
            update_tag_notes,
            upsert_tag_from_preflight,
            reset_tag_sdm,
            enroll_tag_with_identity,
            rotate_tag_keys,
            rewrite_tag_ndef,
            release_tag_control,
            forget_tag,
            take_tag_control,
            delete_draft,
            delete_identity,
            find_similar_assets,
            list_vault_identity_fingerprints,
            is_vault_initialized,
            get_vault_db_path
        ])
        .run(tauri::generate_context!())
        .expect("error while running Trust Systems");
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::{Path, PathBuf};

    #[test]
    fn sign_with_vault_generated_identity() {
        let base = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("..")
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        if !source.exists() {
            eprintln!("Skipping: missing sample.jpg");
            return;
        }

        let vault_dir = std::env::temp_dir().join(format!(
            "trust_systems_sign_gen_test_{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&vault_dir);
        vault::init_vault(&vault_dir, "test-password").expect("init vault");
        vault::generate_sovereign_identity(
            &vault_dir,
            "test-password",
            "Test User",
            "Test Org",
            "US",
        )
        .expect("generate identity");

        let identity = vault::get_active_identity_material(&vault_dir, "test-password")
            .expect("get identity material");

        let out = std::env::temp_dir().join(format!(
            "trust_systems_sign_gen_test_out_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        let result = c2pa_core::sign_image_with_materials(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            "Test Title",
            "Test Author",
            None,
            &identity.certificate_chain_pem,
            &identity.private_key_pem,
        );

        assert!(
            result.is_ok(),
            "signing with vault-generated identity failed: {:?}",
            result.err()
        );

        let _ = std::fs::remove_file(&out);
        let _ = std::fs::remove_dir_all(&vault_dir);
    }

    /// Tauri app crate can use trust_systems and verify_image returns the expected shape.
    /// Signs an image then verifies via the same API the command uses.
    #[test]
    fn verify_image_command_contract_signed_image() {
        let base = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("..")
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            eprintln!("Skipping: missing CAI test assets in assets/test_data");
            return;
        }
        let out = std::env::temp_dir().join(format!(
            "trust_systems_app_verify_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        c2pa_core::sign_image(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Tauri Test Author",
        )
        .expect("sign for test");

        let result = c2pa_core::verify_image(out.to_str().unwrap()).expect("verify for test");
        assert_eq!(result.author.as_deref(), Some("Tauri Test Author"));
        assert!(!result.status_explanation.is_empty());
        assert!(!result.assertion_labels.is_empty());

        let _ = std::fs::remove_file(&out);
    }

    /// verify_image returns an error for a missing file (command would map to Err(String)).
    #[test]
    fn verify_image_command_contract_missing_file() {
        let err = c2pa_core::verify_image("/nonexistent/path.jpg").unwrap_err();
        assert!(err.to_string().contains("not found"));
    }

    #[test]
    fn sign_image_with_physical_hash_writes_custom_assertion() {
        let base = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("..")
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            eprintln!("Skipping: missing CAI test assets in assets/test_data");
            return;
        }

        let out = std::env::temp_dir().join(format!(
            "trust_systems_app_sign_anchor_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        c2pa_core::sign_image_with_manual_anchor(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Tauri Anchor Author",
            Some("tauri-manual-hash-xyz"),
        )
        .expect("sign with manual anchor");

        let result = c2pa_core::verify_image(out.to_str().unwrap()).expect("verify signed image");
        assert!(
            result
                .assertion_labels
                .iter()
                .any(|label| label.contains("physical-anchor")),
            "expected custom physical anchor assertion label in manifest, got: {:?}",
            result.assertion_labels
        );

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn transfer_output_path_appends_transferred_suffix() {
        let base = std::env::temp_dir().join(format!(
            "trust_systems_transfer_path_test_{}",
            std::process::id()
        ));
        let _ = std::fs::create_dir_all(&base);
        let input = base.join("artifact.jpg");
        std::fs::write(&input, b"fixture").expect("write input fixture");

        let out = transfer_output_path(input.to_str().unwrap()).expect("compute transfer output");
        let expected = base.join("artifact_transferred.jpg");
        assert_eq!(out, expected);

        let _ = std::fs::remove_file(&input);
        let _ = std::fs::remove_dir_all(&base);
    }

    #[test]
    fn transfer_output_path_avoids_existing_file_collision() {
        let base = std::env::temp_dir().join(format!(
            "trust_systems_transfer_collision_test_{}",
            std::process::id()
        ));
        let _ = std::fs::create_dir_all(&base);
        let input = base.join("artifact.jpg");
        let first_taken = base.join("artifact_transferred.jpg");
        std::fs::write(&input, b"fixture").expect("write input fixture");
        std::fs::write(&first_taken, b"already exists").expect("write collision fixture");

        let out = transfer_output_path(input.to_str().unwrap()).expect("compute transfer output");
        let expected = PathBuf::from(base.join("artifact_transferred_1.jpg"));
        assert_eq!(out, expected);

        let _ = std::fs::remove_file(&input);
        let _ = std::fs::remove_file(&first_taken);
        let _ = std::fs::remove_dir_all(&base);
    }

    #[test]
    fn legacy_sign_command_not_exposed() {
        let src = include_str!("lib.rs");
        let has_legacy_fn = src
            .lines()
            .any(|line| line.trim_start().starts_with("fn sign_image("));
        let legacy_struct = ["struct ", "Sign", "Image", "Request", " {"].concat();
        let legacy_handler = ["sign", "_image,"].concat();
        assert!(
            !has_legacy_fn,
            "legacy sign_image command should not be present"
        );
        assert!(
            !src.contains(&legacy_struct),
            "legacy SignImageRequest should not be present"
        );
        assert!(
            !src.contains(&legacy_handler),
            "legacy sign_image should not be registered in generate_handler"
        );
        assert!(
            src.contains("sign_image_from_vault"),
            "vault-backed sign command should remain available"
        );
    }
}
