// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Backup responsibility matrix: what a RustFS backup bundle owns per backend.
//!
//! The matrix is two-dimensional on purpose: responsibility is a function of
//! the backend *and* its at-rest protection state, not of the backend alone.
//! This keeps the schema stable when a backend changes protection direction —
//! switching Vault KV2 between storage-only and Transit-wrapped operation
//! selects a different existing row instead of changing the contract.
//!
//! These enums are backup-domain contract types. Once the backlog#1571
//! capability-discovery contract lands, the discovery surface is expected to
//! converge on (or map onto) the states defined here.

use serde::{Deserialize, Serialize};

/// Backend discriminant recorded in a backup manifest.
///
/// Wire names are aligned with [`crate::config::BackendConfig`] and
/// [`crate::config::KmsBackend`] (including the legacy `Vault` alias) so that
/// a manifest and a persisted KMS configuration never disagree about how the
/// same backend is spelled.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackupBackendKind {
    /// Local file-based backend.
    Local,
    /// Vault KV v2 storage backend.
    #[serde(rename = "VaultKV2", alias = "Vault")]
    VaultKv2,
    /// Vault Transit backend.
    VaultTransit,
    /// Static single-key backend.
    Static,
}

/// At-rest protection state of master key material, as observed at snapshot
/// time.
///
/// The first three states mirror the Local backend's on-disk protection
/// marker (`StoredKeyProtection` in `backends/local.rs`, kebab-case wire
/// names). The remaining states describe the non-local backends.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AtRestProtection {
    /// Local key files AEAD-encrypted under the Argon2id-derived master key.
    EncryptedMasterKey,
    /// Local development-only plaintext key files. Such files must never
    /// enter a bundle as-is; bundle artifacts are always re-wrapped under the
    /// backup KEK.
    PlaintextDevOnly,
    /// Pre-beta.9 local key files without a protection marker; the effective
    /// mode is resolved at read time. Treated like [`Self::PlaintextDevOnly`]
    /// for bundling purposes: re-wrap is mandatory.
    LegacyUnspecified,
    /// Vault KV2 as currently shipped: material confidentiality relies on
    /// Vault ACLs, KV2 at-rest encryption, and TLS only; RustFS applies no
    /// cryptographic wrapping of its own. The backend reports nothing about
    /// that boundary at runtime, so this value is a backup-manifest
    /// declaration, not a backend self-report.
    StorageOnly,
    /// Vault KV2 with material wrapped by Vault Transit before storage. Not
    /// produced by any current backend; the row exists so a future direction
    /// change selects a state instead of changing the schema.
    TransitWrapped,
    /// Vault Transit: the cryptographic root lives in Vault and is not
    /// exportable. RustFS can only ever own metadata and references.
    ExternalNonExportable,
    /// Static backend: the secret is delivered externally at startup and
    /// RustFS persists no key material at all.
    ExternalSecretDelivery,
}

/// What a RustFS backup bundle is responsible for, per (backend, protection)
/// combination.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackupResponsibility {
    /// The bundle carries the complete recoverable state: encrypted key
    /// material for every version, salt, metadata, and configuration.
    ///
    /// Restore precondition for the Local backend: the operator re-supplies
    /// the master key out of band. The master key itself is outside the
    /// backup domain; the manifest stores at most an opaque verifier.
    FullMaterial,
    /// The bundle carries only non-sensitive references and verification
    /// information. The source of truth is external secret delivery and the
    /// operator re-provides the secret during restore. Embedding the secret
    /// itself in a bundle is forbidden.
    ReferenceOnly,
    /// The bundle carries RustFS-side metadata, configuration references and
    /// verification data, while the cryptographic root is protected by the
    /// external system's native snapshot/disaster-recovery flow (Vault/HSM).
    /// Restore must re-establish the external trust root first.
    MetadataPlusExternalRoot,
}

impl BackupResponsibility {
    /// Resolve the responsibility matrix for one (backend, protection) cell.
    ///
    /// Returns `None` for combinations that no supported deployment can
    /// produce; manifests declaring such a combination are rejected as
    /// corrupted. This function is total and the unit tests anchor every
    /// cell, so any change to the matrix is a deliberate contract change.
    pub fn for_backend(backend: BackupBackendKind, protection: AtRestProtection) -> Option<Self> {
        use AtRestProtection::*;
        use BackupBackendKind::*;

        match (backend, protection) {
            (Local, EncryptedMasterKey | PlaintextDevOnly | LegacyUnspecified) => Some(Self::FullMaterial),
            (Local, _) => None,
            // Storage-only KV2 offers no external cryptographic root, so the
            // bundle must own the material (re-wrapped under the backup KEK).
            (VaultKv2, StorageOnly) => Some(Self::FullMaterial),
            (VaultKv2, TransitWrapped) => Some(Self::MetadataPlusExternalRoot),
            (VaultKv2, _) => None,
            (VaultTransit, ExternalNonExportable) => Some(Self::MetadataPlusExternalRoot),
            (VaultTransit, _) => None,
            (Static, ExternalSecretDelivery) => Some(Self::ReferenceOnly),
            (Static, _) => None,
        }
    }
}

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

    fn json<T: Serialize>(value: &T) -> String {
        serde_json::to_string(value).expect("serialization should succeed")
    }

    #[test]
    fn backend_kind_wire_names_match_kms_backend() {
        let pairs = [
            (BackupBackendKind::Local, KmsBackend::Local),
            (BackupBackendKind::VaultKv2, KmsBackend::VaultKv2),
            (BackupBackendKind::VaultTransit, KmsBackend::VaultTransit),
            (BackupBackendKind::Static, KmsBackend::Static),
        ];
        for (backup_kind, config_kind) in pairs {
            assert_eq!(json(&backup_kind), json(&config_kind), "wire name drifted for {backup_kind:?}");
        }
    }

    #[test]
    fn backend_kind_accepts_legacy_vault_alias() {
        let decoded: BackupBackendKind = serde_json::from_str("\"Vault\"").expect("legacy alias should decode");
        assert_eq!(decoded, BackupBackendKind::VaultKv2);
    }

    #[test]
    fn responsibility_matrix_is_anchored_cell_by_cell() {
        use AtRestProtection::*;
        use BackupBackendKind::*;
        use BackupResponsibility::*;

        // Every (backend, protection) cell, exhaustively. Changing any row is
        // a contract change and must be made here consciously.
        let matrix = [
            (Local, EncryptedMasterKey, Some(FullMaterial)),
            (Local, PlaintextDevOnly, Some(FullMaterial)),
            (Local, LegacyUnspecified, Some(FullMaterial)),
            (Local, StorageOnly, None),
            (Local, TransitWrapped, None),
            (Local, ExternalNonExportable, None),
            (Local, ExternalSecretDelivery, None),
            (VaultKv2, EncryptedMasterKey, None),
            (VaultKv2, PlaintextDevOnly, None),
            (VaultKv2, LegacyUnspecified, None),
            (VaultKv2, StorageOnly, Some(FullMaterial)),
            (VaultKv2, TransitWrapped, Some(MetadataPlusExternalRoot)),
            (VaultKv2, ExternalNonExportable, None),
            (VaultKv2, ExternalSecretDelivery, None),
            (VaultTransit, EncryptedMasterKey, None),
            (VaultTransit, PlaintextDevOnly, None),
            (VaultTransit, LegacyUnspecified, None),
            (VaultTransit, StorageOnly, None),
            (VaultTransit, TransitWrapped, None),
            (VaultTransit, ExternalNonExportable, Some(MetadataPlusExternalRoot)),
            (VaultTransit, ExternalSecretDelivery, None),
            (Static, EncryptedMasterKey, None),
            (Static, PlaintextDevOnly, None),
            (Static, LegacyUnspecified, None),
            (Static, StorageOnly, None),
            (Static, TransitWrapped, None),
            (Static, ExternalNonExportable, None),
            (Static, ExternalSecretDelivery, Some(ReferenceOnly)),
        ];
        assert_eq!(matrix.len(), 28, "matrix must stay exhaustive: 4 backends x 7 protection states");
        for (backend, protection, expected) in matrix {
            assert_eq!(
                BackupResponsibility::for_backend(backend, protection),
                expected,
                "matrix cell drifted for ({backend:?}, {protection:?})"
            );
        }
    }

    #[test]
    fn local_protection_wire_names_match_stored_key_protection() {
        use crate::backends::local::StoredKeyProtection;

        // The manifest must record exactly the marker values the Local
        // backend writes to disk, or a restore could misread protection.
        let pairs = [
            (AtRestProtection::EncryptedMasterKey, StoredKeyProtection::EncryptedMasterKey),
            (AtRestProtection::PlaintextDevOnly, StoredKeyProtection::PlaintextDevOnly),
            (AtRestProtection::LegacyUnspecified, StoredKeyProtection::LegacyUnspecified),
        ];
        for (backup_state, stored_state) in pairs {
            assert_eq!(json(&backup_state), json(&stored_state), "wire name drifted for {backup_state:?}");
        }
    }

    #[test]
    fn responsibility_wire_names_are_frozen() {
        assert_eq!(json(&BackupResponsibility::FullMaterial), "\"full-material\"");
        assert_eq!(json(&BackupResponsibility::ReferenceOnly), "\"reference-only\"");
        assert_eq!(json(&BackupResponsibility::MetadataPlusExternalRoot), "\"metadata-plus-external-root\"");
    }
}
