using EntKube.Web.Data;
using Microsoft.EntityFrameworkCore;

namespace EntKube.Web.Services;

/// <summary>
/// Manages the per-tenant secrets vault: initializing vaults, storing/retrieving
/// encrypted secrets for apps and cluster components, and configuring Kubernetes
/// sync. Each operation unseals the vault transparently (auto-unseal via root key).
/// </summary>
public class VaultService(IDbContextFactory<ApplicationDbContext> dbFactory, VaultEncryptionService encryption)
{
    // --- Vault Lifecycle ---

    /// <summary>
    /// Creates a new vault for the tenant if one doesn't already exist.
    /// Generates a fresh DEK and seals it with the platform root key.
    /// Idempotent — returns the existing vault if already initialized.
    /// </summary>
    public async Task<SecretVault> InitializeVaultAsync(Guid tenantId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        // If the tenant already has a vault, just return it.
        SecretVault? existing = await db.Set<SecretVault>()
            .FirstOrDefaultAsync(v => v.TenantId == tenantId, ct);

        if (existing is not null)
        {
            return existing;
        }

        // Generate a brand new data encryption key and seal it with the root key.
        byte[] dataKey = encryption.GenerateDataKey();
        (byte[] encryptedKey, byte[] nonce) = encryption.SealDataKey(dataKey);

        SecretVault vault = new()
        {
            Id = Guid.NewGuid(),
            TenantId = tenantId,
            EncryptedDataKey = encryptedKey,
            Nonce = nonce
        };

        db.Set<SecretVault>().Add(vault);
        await db.SaveChangesAsync(ct);
        return vault;
    }

    /// <summary>
    /// Retrieves the vault for a tenant, or null if not yet initialized.
    /// </summary>
    public async Task<SecretVault?> GetVaultAsync(Guid tenantId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<SecretVault>()
            .FirstOrDefaultAsync(v => v.TenantId == tenantId, ct);
    }

    // --- App Secrets ---

    /// <summary>
    /// Stores or updates a secret for a customer app. If a secret with the same
    /// name already exists for this app, its value is updated in place.
    /// </summary>
    public async Task<VaultSecret> SetAppSecretAsync(
        Guid tenantId, Guid appId, string name, string value, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        // Unseal the vault to get the tenant's DEK.
        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        // Check if a secret with this name already exists for this app.
        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.AppId == appId && s.Name == name, ct);

        // Encrypt the value with the tenant's DEK.
        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            // Update the existing secret's encrypted value.
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        // Create a new secret entry.
        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            AppId = appId
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Decrypts and returns a specific app secret value.
    /// </summary>
    public async Task<string?> GetAppSecretValueAsync(
        Guid tenantId, Guid appId, string name, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.AppId == appId && s.Name == name, ct);

        if (secret is null)
        {
            return null;
        }

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    /// <summary>
    /// Lists all secrets for an app (metadata only — values remain encrypted).
    /// </summary>
    public async Task<List<VaultSecret>> GetAppSecretsAsync(
        Guid tenantId, Guid appId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<VaultSecret>()
            .Where(s => s.Vault.TenantId == tenantId && s.AppId == appId)
            .OrderBy(s => s.Name)
            .ToListAsync(ct);
    }

    // --- Component Secrets ---

    /// <summary>
    /// Stores or updates a secret for a cluster component. When k8sSecretName is
    /// provided, the vault sync service will mirror the value into that Kubernetes Secret.
    /// </summary>
    public async Task<VaultSecret> SetComponentSecretAsync(
        Guid tenantId, Guid componentId, string name, string value,
        CancellationToken ct = default,
        string? k8sSecretName = null, string? k8sNamespace = null)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.ComponentId == componentId && s.Name == name, ct);

        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            if (k8sSecretName is not null)
            {
                existing.SyncToKubernetes = true;
                existing.KubernetesSecretName = k8sSecretName;
                existing.KubernetesNamespace = k8sNamespace;
            }
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            ComponentId = componentId,
            SyncToKubernetes = k8sSecretName is not null,
            KubernetesSecretName = k8sSecretName,
            KubernetesNamespace = k8sNamespace
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Decrypts and returns a specific component secret value.
    /// </summary>
    public async Task<string?> GetComponentSecretValueAsync(
        Guid tenantId, Guid componentId, string name, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.ComponentId == componentId && s.Name == name, ct);

        if (secret is null)
        {
            return null;
        }

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    /// <summary>
    /// Lists all secrets for a component (metadata only).
    /// </summary>
    public async Task<List<VaultSecret>> GetComponentSecretsAsync(
        Guid tenantId, Guid componentId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<VaultSecret>()
            .Where(s => s.Vault.TenantId == tenantId && s.ComponentId == componentId)
            .OrderBy(s => s.Name)
            .ToListAsync(ct);
    }

    // --- Storage Link Secrets ---

    /// <summary>
    /// Creates or updates a secret scoped to a storage link.
    /// </summary>
    public async Task<VaultSecret> SetStorageLinkSecretAsync(
        Guid tenantId, Guid storageLinkId, string name, string value, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.StorageLinkId == storageLinkId && s.Name == name, ct);

        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            StorageLinkId = storageLinkId
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Returns all secrets scoped to a storage link.
    /// </summary>
    public async Task<List<VaultSecret>> GetStorageLinkSecretsAsync(
        Guid tenantId, Guid storageLinkId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<VaultSecret>()
            .Where(s => s.Vault.TenantId == tenantId && s.StorageLinkId == storageLinkId)
            .OrderBy(s => s.Name)
            .ToListAsync(ct);
    }

    /// <summary>
    /// Gets a single decrypted secret value for a storage link by name.
    /// Returns null if the secret doesn't exist.
    /// </summary>
    public async Task<string?> GetStorageLinkSecretValueAsync(
        Guid tenantId, Guid storageLinkId, string name, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.StorageLinkId == storageLinkId && s.Name == name, ct);

        if (secret is null)
        {
            return null;
        }

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    // --- OpenStack Connection Secrets ---

    /// <summary>
    /// Creates or updates a secret scoped to an OpenStack connection.
    /// </summary>
    public async Task<VaultSecret> SetOpenStackSecretAsync(
        Guid tenantId, Guid openStackConnectionId, string name, string value, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.OpenStackConnectionId == openStackConnectionId && s.Name == name, ct);

        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            OpenStackConnectionId = openStackConnectionId
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Returns all secrets scoped to an OpenStack connection.
    /// </summary>
    public async Task<List<VaultSecret>> GetOpenStackSecretsAsync(
        Guid tenantId, Guid openStackConnectionId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<VaultSecret>()
            .Where(s => s.Vault.TenantId == tenantId && s.OpenStackConnectionId == openStackConnectionId)
            .OrderBy(s => s.Name)
            .ToListAsync(ct);
    }

    /// <summary>
    /// Decrypts and returns a specific OpenStack connection secret by name.
    /// </summary>
    public async Task<string?> GetOpenStackSecretValueAsync(
        Guid tenantId, Guid openStackConnectionId, string name, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.OpenStackConnectionId == openStackConnectionId && s.Name == name, ct);

        if (secret is null)
        {
            return null;
        }

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    // --- CNPG Database Secrets ---

    /// <summary>
    /// Creates or updates a secret scoped to a CNPG database, pre-configured
    /// for Kubernetes sync so applications can consume connection credentials
    /// as a standard K8s Secret.
    /// </summary>
    public async Task<VaultSecret> SetCnpgDatabaseSecretAsync(
        Guid tenantId, Guid cnpgDatabaseId, string name, string value,
        string k8sSecretName, string k8sNamespace, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.CnpgDatabaseId == cnpgDatabaseId && s.Name == name, ct);

        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            existing.SyncToKubernetes = true;
            existing.KubernetesSecretName = k8sSecretName;
            existing.KubernetesNamespace = k8sNamespace;
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            CnpgDatabaseId = cnpgDatabaseId,
            SyncToKubernetes = true,
            KubernetesSecretName = k8sSecretName,
            KubernetesNamespace = k8sNamespace
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Returns all secrets scoped to a CNPG database.
    /// </summary>
    public async Task<List<VaultSecret>> GetCnpgDatabaseSecretsAsync(
        Guid tenantId, Guid cnpgDatabaseId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<VaultSecret>()
            .Where(s => s.Vault.TenantId == tenantId && s.CnpgDatabaseId == cnpgDatabaseId)
            .OrderBy(s => s.Name)
            .ToListAsync(ct);
    }

    /// <summary>
    /// Returns the decrypted password for a CNPG database. Used when another
    /// service (e.g. Keycloak) needs to read the DB password to propagate it.
    /// </summary>
    public async Task<string?> GetCnpgDatabasePasswordAsync(
        Guid tenantId, Guid cnpgDatabaseId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId
                && s.CnpgDatabaseId == cnpgDatabaseId
                && s.Name == "PASSWORD", ct);

        if (secret is null) return null;

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    /// <summary>
    /// Creates or updates a vault secret scoped to a MongoDB database.
    /// Mirrors SetCnpgDatabaseSecretAsync but uses MongoDatabaseId.
    /// </summary>
    public async Task<VaultSecret> SetMongoDatabaseSecretAsync(
        Guid tenantId, Guid mongoDatabaseId, string name, string value,
        string k8sSecretName, string k8sNamespace, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.MongoDatabaseId == mongoDatabaseId && s.Name == name, ct);

        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            existing.SyncToKubernetes = true;
            existing.KubernetesSecretName = k8sSecretName;
            existing.KubernetesNamespace = k8sNamespace;
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            MongoDatabaseId = mongoDatabaseId,
            SyncToKubernetes = true,
            KubernetesSecretName = k8sSecretName,
            KubernetesNamespace = k8sNamespace
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Creates or updates a vault secret scoped to a managed MongoDB cluster.
    /// When SyncToKubernetes is true the vault sync service will keep the K8s Secret
    /// up to date, but callers that need the Secret immediately should also apply
    /// the manifest directly via IKubernetesClientFactory.
    /// </summary>
    public async Task<VaultSecret> SetMongoClusterSecretAsync(
        Guid tenantId, Guid mongoClusterId, string name, string value,
        string k8sSecretName, string k8sNamespace, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);

        VaultSecret? existing = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.MongoClusterId == mongoClusterId && s.Name == name, ct);

        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);

        if (existing is not null)
        {
            existing.EncryptedValue = ciphertext;
            existing.Nonce = nonce;
            existing.SyncToKubernetes = true;
            existing.KubernetesSecretName = k8sSecretName;
            existing.KubernetesNamespace = k8sNamespace;
            existing.UpdatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(ct);
            return existing;
        }

        SecretVault vault = (await GetVaultAsync(tenantId, ct))!;

        VaultSecret secret = new()
        {
            Id = Guid.NewGuid(),
            VaultId = vault.Id,
            Name = name,
            EncryptedValue = ciphertext,
            Nonce = nonce,
            MongoClusterId = mongoClusterId,
            SyncToKubernetes = true,
            KubernetesSecretName = k8sSecretName,
            KubernetesNamespace = k8sNamespace
        };

        db.Set<VaultSecret>().Add(secret);
        await db.SaveChangesAsync(ct);
        return secret;
    }

    /// <summary>
    /// Decrypts and returns the value of a secret scoped to a managed MongoDB cluster.
    /// Returns null if no matching secret is found.
    /// </summary>
    public async Task<string?> GetMongoClusterSecretValueAsync(
        Guid tenantId, Guid mongoClusterId, string name, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.MongoClusterId == mongoClusterId && s.Name == name, ct);

        if (secret is null) return null;

        byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    // --- Secret Management ---

    /// <summary>
    /// Decrypts and returns the value of any secret by its ID, regardless of scope.
    /// Used by the UI to reveal a secret value on demand.
    /// </summary>
    public async Task<string?> GetSecretValueByIdAsync(Guid secretId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .Include(s => s.Vault)
            .FirstOrDefaultAsync(s => s.Id == secretId, ct);

        if (secret is null)
        {
            return null;
        }

        byte[] dataKey = await UnsealVaultAsync(secret.Vault.TenantId, ct);
        return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
    }

    /// <summary>
    /// Updates the encrypted value of an existing secret by its ID.
    /// </summary>
    public async Task<bool> UpdateSecretValueAsync(Guid secretId, string newValue, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>()
            .Include(s => s.Vault)
            .FirstOrDefaultAsync(s => s.Id == secretId, ct);

        if (secret is null)
        {
            return false;
        }

        byte[] dataKey = await UnsealVaultAsync(secret.Vault.TenantId, ct);
        (byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, newValue);

        secret.EncryptedValue = ciphertext;
        secret.Nonce = nonce;
        secret.UpdatedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(ct);
        return true;
    }

    /// <summary>
    /// Checks whether a secret can be safely deleted. A secret is protected from
    /// deletion when it belongs to a StorageLink that has active StorageBindings
    /// (i.e. a workload depends on those credentials being available).
    /// </summary>
    public async Task<(bool CanDelete, string? Reason)> CanDeleteSecretAsync(
        Guid secretId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>().FindAsync([secretId], ct);

        if (secret is null)
        {
            return (true, null);
        }

        // If the secret belongs to a storage link, check for active bindings.

        if (secret.StorageLinkId is not null)
        {
            bool hasBindings = await db.Set<StorageBinding>()
                .AnyAsync(b => b.StorageLinkId == secret.StorageLinkId && b.SyncEnabled, ct);

            if (hasBindings)
            {
                return (false, "This secret is linked to a storage binding that syncs to Kubernetes. Unbind the storage first.");
            }
        }

        // If the secret is synced to K8s, warn but still allow deletion.

        return (true, null);
    }

    /// <summary>
    /// Deletes a secret by ID regardless of its scope.
    /// </summary>
    public async Task<bool> DeleteSecretAsync(Guid secretId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>().FindAsync([secretId], ct);

        if (secret is null)
        {
            return false;
        }

        db.Set<VaultSecret>().Remove(secret);
        await db.SaveChangesAsync(ct);
        return true;
    }

    /// <summary>
    /// Configures Kubernetes sync for a secret — enabling or disabling sync
    /// and setting the target Secret name and namespace.
    /// </summary>
    public async Task ConfigureKubernetesSyncAsync(
        Guid secretId, bool syncEnabled, string? secretName, string? ns, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        VaultSecret? secret = await db.Set<VaultSecret>().FindAsync([secretId], ct);

        if (secret is null)
        {
            return;
        }

        secret.SyncToKubernetes = syncEnabled;
        secret.KubernetesSecretName = secretName;
        secret.KubernetesNamespace = ns;
        secret.UpdatedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(ct);
    }

    // --- Cluster Components ---

    /// <summary>
    /// Adds a new component (helm chart, deployment, etc.) to a cluster.
    /// </summary>
    public async Task<ClusterComponent> CreateComponentAsync(
        Guid clusterId, string name, string componentType, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        ClusterComponent component = new()
        {
            Id = Guid.NewGuid(),
            ClusterId = clusterId,
            Name = name,
            ComponentType = componentType
        };

        db.Set<ClusterComponent>().Add(component);
        await db.SaveChangesAsync(ct);
        return component;
    }

    /// <summary>
    /// Lists all components for a cluster.
    /// </summary>
    public async Task<List<ClusterComponent>> GetComponentsAsync(Guid clusterId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.Set<ClusterComponent>()
            .Where(c => c.ClusterId == clusterId)
            .OrderBy(c => c.Name)
            .ToListAsync(ct);
    }

    /// <summary>
    /// Deletes a component by ID. Cascade will also remove its secrets.
    /// </summary>
    public async Task<bool> DeleteComponentAsync(Guid componentId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        ClusterComponent? component = await db.Set<ClusterComponent>().FindAsync([componentId], ct);

        if (component is null)
        {
            return false;
        }

        db.Set<ClusterComponent>().Remove(component);
        await db.SaveChangesAsync(ct);
        return true;
    }

    // --- Private Helpers ---

    /// <summary>
    /// Unseals the tenant's vault and returns the decrypted DEK.
    /// This is the "auto-unseal" — we just decrypt using the root key from config.
    /// </summary>
    private async Task<byte[]> UnsealVaultAsync(Guid tenantId, CancellationToken ct)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        SecretVault vault = await db.Set<SecretVault>()
            .FirstAsync(v => v.TenantId == tenantId, ct);

        return encryption.UnsealDataKey(vault.EncryptedDataKey, vault.Nonce);
    }
}
