//! Unified error type for the Relicario core crate.
//!
//! Every fallible function in this crate returns [`Result<T>`], which is an alias
//! for `std::result::Result<T, RelicarioError>`. Using a single error enum keeps the
//! public API surface predictable and makes error handling in callers (CLI, WASM
//! bindings, mobile FFI) straightforward.

use thiserror::Error;

/// All errors that can originate from Relicario core operations.
///
/// Variants are ordered roughly by the pipeline stage where they occur:
/// KDF -> encryption -> decryption -> format parsing -> item lookup -> image
/// steganography -> serialization -> device keys.
#[derive(Debug, Error)]
pub enum RelicarioError {
    /// The Argon2id key derivation failed. This typically means invalid KDF
    /// parameters were supplied (e.g., memory cost below Argon2's minimum).
    #[error("key derivation failed: {0}")]
    Kdf(String),

    /// XChaCha20-Poly1305 encryption failed. In practice this is extremely rare
    /// -- the only realistic cause is an internal library error, since the cipher
    /// accepts arbitrary-length plaintext.
    #[error("encryption failed: {0}")]
    Encrypt(String),

    /// Authenticated decryption failed. Message intentionally opaque (audit M4).
    #[error("decryption failed")]
    Decrypt,

    /// The binary ciphertext blob does not match the expected format (e.g.,
    /// too short to contain the version byte + nonce + tag, or an unrecognized
    /// version byte). This usually indicates file corruption or a version
    /// mismatch between the writer and reader.
    #[error("invalid vault format: {0}")]
    Format(String),

    #[error("unsupported vault format version: found 0x{found:02x}, expected 0x{expected:02x}")]
    UnsupportedFormatVersion { found: u8, expected: u8 },

    /// Backup file's first 4 bytes don't match the "RBAK" magic.
    #[error("not a Relicario backup file")]
    BackupBadMagic,

    /// Backup format version is newer than this binary supports.
    #[error("backup created by a newer Relicario; upgrade required")]
    BackupUnsupportedVersion { found: u8, expected: u8 },

    /// Backup envelope schema version doesn't match.
    #[error("backup envelope schema v{found}; this Relicario reads v{expected}")]
    BackupSchemaMismatch { found: u32, expected: u32 },

    /// An error during backup restore (e.g., tar safety validation failure).
    #[error("backup restore: {0}")]
    BackupRestore(String),

    /// CSV header doesn't match the LastPass column layout.
    #[error("unrecognized CSV header — expected LastPass export format ({0})")]
    ImportCsvHeader(String),

    /// CSV body could not be parsed (mismatched quoting, encoding, etc.).
    /// Per-row record errors that the importer recovers from become
    /// `ImportWarning` entries — this variant is reserved for failures
    /// that abort the whole import.
    #[error("CSV parse failed: {0}")]
    ImportCsvFormat(String),

    /// An item was looked up by ID but does not exist in the manifest.
    #[error("item not found: {0}")]
    ItemNotFound(String),

    /// A passphrase failed the strength gate at vault creation (audit H3).
    #[error("passphrase strength insufficient (score {score}/4)")]
    WeakPassphrase { score: u8 },

    /// An attachment exceeded the per-attachment cap from VaultSettings.
    #[error("attachment too large: {size} bytes > {max} bytes max")]
    AttachmentTooLarge { size: u64, max: u64 },

    /// A general error from the image steganography subsystem (imgsecret).
    /// Covers issues like failing to decode the carrier JPEG or failing to
    /// encode the output JPEG after modification.
    #[error("imgsecret: {0}")]
    ImgSecret(String),

    /// The carrier image is too small to hold the embedded secret with
    /// sufficient redundancy. The embed region (central 70% of the image)
    /// must contain at least `BLOCKS_PER_COPY * MIN_COPIES` 8x8 blocks.
    #[error("image too small: need at least {min_width}x{min_height}, got {actual_width}x{actual_height}")]
    ImageTooSmall {
        min_width: u32,
        min_height: u32,
        actual_width: u32,
        actual_height: u32,
    },

    /// Secret extraction from a JPEG failed. This can mean:
    /// - The image never had a secret embedded in it.
    /// - The image was recompressed below Q85, destroying the QIM watermarks.
    /// - The image was cropped beyond the 15% crumple zone.
    /// - Majority-vote confidence fell below the 60% threshold on one or more bits.
    #[error("extraction failed: no valid secret found in image")]
    ExtractionFailed,

    /// JSON serialization or deserialization of an entry or manifest failed.
    /// Wraps [`serde_json::Error`] transparently via `#[from]`.
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),

    /// An error related to device ed25519 key operations. Device keys are
    /// separate from the vault KDF -- revoking a device does not require
    /// rotating the passphrase or reference image.
    #[error("device key error: {0}")]
    DeviceKey(String),

    /// HOTP requires incrementing and persisting the counter after each use.
    /// Without vault-save machinery in compute_totp_code, HOTP would desync
    /// immediately. Use TOTP instead.
    #[error("HOTP is not supported: counter persistence requires vault save after each use")]
    HotpNotSupported,

    /// Recovery QR generation or parsing failed.
    #[error("recovery QR: {0}")]
    RecoveryQr(String),

    /// Base32 decoding failed (non-alphabet character or other malformed
    /// input). Emitted by [`crate::base32::decode_rfc4648_lenient`] and any
    /// typed wrappers that delegate to it.
    #[error("invalid base32: {0}")]
    InvalidBase32(String),

    /// Card-expiry month/year string failed to parse. Emitted by
    /// [`crate::time::MonthYear::parse`].
    #[error("invalid month/year: {0}")]
    InvalidMonthYear(String),
}

/// Crate-wide result alias, reducing boilerplate in function signatures.
pub type Result<T> = std::result::Result<T, RelicarioError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decrypt_error_message_is_opaque() {
        let err = RelicarioError::Decrypt;
        assert_eq!(format!("{}", err), "decryption failed");
    }

    #[test]
    fn weak_passphrase_carries_score() {
        let err = RelicarioError::WeakPassphrase { score: 1 };
        let s = format!("{}", err);
        assert!(s.contains("passphrase"));
        assert!(s.contains("strength"));
    }

    #[test]
    fn attachment_too_large_reports_sizes() {
        let err = RelicarioError::AttachmentTooLarge { size: 11_000_000, max: 10_485_760 };
        let s = format!("{}", err);
        assert!(s.contains("11000000"));
        assert!(s.contains("10485760"));
    }

    #[test]
    fn item_not_found_carries_id() {
        let err = RelicarioError::ItemNotFound("abc123".to_string());
        assert!(format!("{}", err).contains("abc123"));
    }

    #[test]
    fn unsupported_format_version_reports_byte() {
        let err = RelicarioError::UnsupportedFormatVersion { found: 0x01, expected: 0x02 };
        let s = format!("{}", err);
        assert!(s.contains("01") || s.contains("1"));
        assert!(s.contains("02") || s.contains("2"));
    }

    #[test]
    fn backup_errors_carry_useful_messages() {
        let bad = RelicarioError::BackupBadMagic;
        assert!(format!("{}", bad).contains("not a Relicario backup file"));

        let ver = RelicarioError::BackupUnsupportedVersion { found: 0x02, expected: 0x01 };
        let s = format!("{}", ver);
        assert!(s.contains("newer"));

        let schema = RelicarioError::BackupSchemaMismatch { found: 2, expected: 1 };
        let s = format!("{}", schema);
        assert!(s.contains("v2") && s.contains("v1"));
    }

    #[test]
    fn import_errors_carry_useful_messages() {
        let h = RelicarioError::ImportCsvHeader("missing 'name' column".into());
        assert!(format!("{}", h).contains("LastPass"));
        assert!(format!("{}", h).contains("missing 'name'"));

        let f = RelicarioError::ImportCsvFormat("unterminated quote at line 12".into());
        assert!(format!("{}", f).contains("CSV parse failed"));
        assert!(format!("{}", f).contains("unterminated quote"));
    }
}
