using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Serialization;
using EntKube.Web.Data;
using EntKube.Web.Data.Backup;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

namespace EntKube.Web.Services;

/// <summary>
/// Exports the full EntKube app state to a gzip-compressed JSON bundle and
/// restores it on a target installation. Secrets are decrypted during export
/// and re-encrypted with fresh DEKs on restore — the bundle does not carry
/// the root key, but it does carry plaintext secret values, so treat it as
/// sensitive material (encrypt at rest, restrict access).
/// </summary>
public class BackupService(
    IDbContextFactory<ApplicationDbContext> dbFactory,
    VaultEncryptionService encryption,
    ILogger<BackupService> logger)
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        ReferenceHandler = ReferenceHandler.IgnoreCycles,
    };

    // ── Export ──────────────────────────────────────────────────────────────

    public async Task<byte[]> ExportAsync(string performedBy)
    {
        await using ApplicationDbContext db = dbFactory.CreateDbContext();

        // Unseal every tenant's DEK so we can decrypt individual secrets.
        Dictionary<Guid, byte[]> dekByVaultId = [];
        List<VaultRecord> vaultRecords = [];

        foreach (SecretVault vault in await db.SecretVaults.AsNoTracking().ToListAsync())
        {
            byte[] dek = encryption.UnsealDataKey(vault.EncryptedDataKey, vault.Nonce);
            dekByVaultId[vault.Id] = dek;
            vaultRecords.Add(new VaultRecord { Id = vault.Id, TenantId = vault.TenantId, CreatedAt = vault.CreatedAt });
        }

        List<VaultSecretRecord> secretRecords = [];
        foreach (VaultSecret s in await db.VaultSecrets.AsNoTracking().ToListAsync())
        {
            if (!dekByVaultId.TryGetValue(s.VaultId, out byte[]? dek))
            {
                logger.LogWarning("VaultSecret {Id} references unknown vault {VaultId} — skipping.", s.Id, s.VaultId);
                continue;
            }
            string plaintext = encryption.Decrypt(dek, s.EncryptedValue, s.Nonce);
            secretRecords.Add(new VaultSecretRecord(
                s.Id, s.VaultId, s.Name, plaintext,
                s.AppId, s.ComponentId, s.StorageLinkId, s.OpenStackConnectionId,
                s.CnpgClusterId, s.CnpgDatabaseId, s.MongoDatabaseId, s.MongoClusterId,
                s.RegisteredPostgresDatabaseId, s.RabbitMQClusterId,
                s.SyncToKubernetes, s.KubernetesClusterId, s.KubernetesSecretName, s.KubernetesNamespace,
                s.CreatedAt, s.UpdatedAt));
        }

        List<DockerCredentialRecord> credRecords = [];
        foreach (DockerRegistryCredential c in await db.DockerRegistryCredentials.AsNoTracking().ToListAsync())
        {
            if (!dekByVaultId.TryGetValue(c.VaultId, out byte[]? dek))
            {
                logger.LogWarning("DockerCredential {Id} references unknown vault {VaultId} — skipping.", c.Id, c.VaultId);
                continue;
            }
            string plainPassword = encryption.Decrypt(dek, c.EncryptedPassword, c.PasswordNonce);
            credRecords.Add(new DockerCredentialRecord(
                c.Id, c.VaultId, c.Name, c.RegistryType, c.Server, c.Username, plainPassword,
                c.Email, c.AppId, c.KubernetesClusterId, c.KubernetesSecretName, c.KubernetesNamespace,
                c.CreatedAt, c.UpdatedAt));
        }

        List<UserRecord> userRecords = (await db.Users.AsNoTracking().ToListAsync())
            .Select(u => new UserRecord(
                u.Id, u.UserName, u.Email, u.PasswordHash, u.EmailConfirmed,
                u.NormalizedUserName, u.NormalizedEmail, u.SecurityStamp, u.ConcurrencyStamp,
                u.PhoneNumber, u.PhoneNumberConfirmed, u.TwoFactorEnabled,
                u.LockoutEnd, u.LockoutEnabled, u.AccessFailedCount))
            .ToList();

        List<RoleRecord> roleRecords = (await db.Roles.AsNoTracking().ToListAsync())
            .Select(r => new RoleRecord(r.Id, r.Name, r.NormalizedName, r.ConcurrencyStamp))
            .ToList();

        List<UserRoleRecord> userRoleRecords = (await db.Set<IdentityUserRole<string>>().AsNoTracking().ToListAsync())
            .Select(ur => new UserRoleRecord(ur.UserId, ur.RoleId))
            .ToList();

        BackupBundle bundle = new()
        {
            CreatedBy = performedBy,
            CreatedAt = DateTime.UtcNow,
            Users = userRecords,
            Roles = roleRecords,
            UserRoles = userRoleRecords,
            Tenants = await db.Tenants.AsNoTracking().ToListAsync(),
            TenantRoles = await db.TenantRoles.AsNoTracking().ToListAsync(),
            TenantMemberships = await db.TenantMemberships.AsNoTracking().ToListAsync(),
            Groups = await db.Groups.AsNoTracking().ToListAsync(),
            GroupMemberships = await db.GroupMemberships.AsNoTracking().ToListAsync(),
            Environments = await db.Environments.AsNoTracking().ToListAsync(),
            Customers = await db.Customers.AsNoTracking().ToListAsync(),
            CustomerAccesses = await db.CustomerAccesses.AsNoTracking().ToListAsync(),
            Apps = await db.Apps.AsNoTracking().ToListAsync(),
            AppEnvironments = await db.AppEnvironments.AsNoTracking().ToListAsync(),
            KubernetesClusters = await db.KubernetesClusters.AsNoTracking().ToListAsync(),
            ClusterComponents = await db.ClusterComponents.AsNoTracking().ToListAsync(),
            ExternalRoutes = await db.ExternalRoutes.AsNoTracking().ToListAsync(),
            OpenStackConnections = await db.OpenStackConnections.AsNoTracking().ToListAsync(),
            StorageLinks = await db.StorageLinks.AsNoTracking().ToListAsync(),
            AppDeployments = await db.AppDeployments.AsNoTracking().ToListAsync(),
            DeploymentManifests = await db.DeploymentManifests.AsNoTracking().ToListAsync(),
            StorageBindings = await db.StorageBindings.AsNoTracking().ToListAsync(),
            CnpgClusters = await db.CnpgClusters.AsNoTracking().ToListAsync(),
            CnpgDatabases = await db.CnpgDatabases.AsNoTracking().ToListAsync(),
            MongoClusters = await db.MongoClusters.AsNoTracking().ToListAsync(),
            MongoDatabases = await db.MongoDatabases.AsNoTracking().ToListAsync(),
            RabbitMQClusters = await db.RabbitMQClusters.AsNoTracking().ToListAsync(),
            RegisteredPostgresInstances = await db.RegisteredPostgresInstances.AsNoTracking().ToListAsync(),
            RegisteredPostgresDatabases = await db.RegisteredPostgresDatabases.AsNoTracking().ToListAsync(),
            DatabaseBindings = await db.DatabaseBindings.AsNoTracking().ToListAsync(),
            MessagingBindings = await db.MessagingBindings.AsNoTracking().ToListAsync(),
            KeycloakComponentConfigs = await db.KeycloakComponentConfigs.AsNoTracking().ToListAsync(),
            KeycloakThemes = await db.KeycloakThemes.AsNoTracking().ToListAsync(),
            KeycloakRealms = await db.KeycloakRealms.AsNoTracking().ToListAsync(),
            HarborComponentConfigs = await db.HarborComponentConfigs.AsNoTracking().ToListAsync(),
            HarborProjects = await db.HarborProjects.AsNoTracking().ToListAsync(),
            NotificationChannels = await db.NotificationChannels.AsNoTracking().ToListAsync(),
            SlaTargets = await db.SlaTargets.AsNoTracking().ToListAsync(),
            MaintenanceWindows = await db.MaintenanceWindows.AsNoTracking().ToListAsync(),
            SecretVaults = vaultRecords,
            VaultSecrets = secretRecords,
            DockerCredentials = credRecords,
        };

        using MemoryStream ms = new();
        await using (GZipStream gz = new(ms, CompressionLevel.Optimal, leaveOpen: true))
            await JsonSerializer.SerializeAsync(gz, bundle, JsonOptions);

        logger.LogInformation("Backup exported by {User}: {Tenants} tenants, {Secrets} secrets, {Creds} registry credentials.",
            performedBy, bundle.Tenants.Count, bundle.VaultSecrets.Count, bundle.DockerCredentials.Count);

        return ms.ToArray();
    }

    // ── Import ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Restores a backup bundle. When <paramref name="wipeExisting"/> is true,
    /// ALL existing data is deleted before the import begins (the wipe and the
    /// insert are wrapped in a single transaction so a failed import rolls back
    /// the wipe too). Designed for PostgreSQL; the TRUNCATE CASCADE syntax is
    /// Postgres-specific.
    /// </summary>
    public async Task ImportAsync(Stream bundleStream, bool wipeExisting)
    {
        await using GZipStream gz = new(bundleStream, CompressionMode.Decompress);
        BackupBundle? bundle = await JsonSerializer.DeserializeAsync<BackupBundle>(gz, JsonOptions)
            ?? throw new InvalidDataException("Failed to deserialize backup bundle.");

        if (bundle.Version != 1)
            throw new InvalidDataException($"Unsupported backup version: {bundle.Version}. Only version 1 is supported.");

        await using ApplicationDbContext db = dbFactory.CreateDbContext();
        await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync();

        try
        {
            if (wipeExisting)
            {
                logger.LogInformation("Wiping all existing data before restore.");
                // TRUNCATE ... CASCADE removes the listed tables and all tables that
                // transitively reference them via FK constraints, regardless of ON DELETE
                // behaviour. Three top-level tables cover the entire schema:
                //   Tenants  → every tenant-owned entity (clusters, apps, secrets, …)
                //   AspNetUsers → memberships, customer accesses, group memberships
                //   AspNetRoles → role assignments
                await db.Database.ExecuteSqlRawAsync(@"TRUNCATE ""Tenants"" CASCADE");
                await db.Database.ExecuteSqlRawAsync(@"TRUNCATE ""AspNetUsers"" CASCADE");
                await db.Database.ExecuteSqlRawAsync(@"TRUNCATE ""AspNetRoles"" CASCADE");
                db.ChangeTracker.Clear();
            }

            // Identity — insert roles and users first so FK references from
            // TenantMembership, CustomerAccess, etc. can resolve.
            await InsertBatch(db, bundle.Roles.Select(r => new IdentityRole
            {
                Id = r.Id, Name = r.Name, NormalizedName = r.NormalizedName, ConcurrencyStamp = r.ConcurrencyStamp,
            }));

            await InsertBatch(db, bundle.Users.Select(u => new ApplicationUser
            {
                Id = u.Id, UserName = u.UserName, Email = u.Email, PasswordHash = u.PasswordHash,
                EmailConfirmed = u.EmailConfirmed, NormalizedUserName = u.NormalizedUserName,
                NormalizedEmail = u.NormalizedEmail, SecurityStamp = u.SecurityStamp,
                ConcurrencyStamp = u.ConcurrencyStamp, PhoneNumber = u.PhoneNumber,
                PhoneNumberConfirmed = u.PhoneNumberConfirmed, TwoFactorEnabled = u.TwoFactorEnabled,
                LockoutEnd = u.LockoutEnd, LockoutEnabled = u.LockoutEnabled, AccessFailedCount = u.AccessFailedCount,
            }));

            await InsertBatch(db, bundle.UserRoles.Select(ur =>
                new IdentityUserRole<string> { UserId = ur.UserId, RoleId = ur.RoleId }));

            // Tenant structure — parents before children.
            await InsertEntities(db, db.Tenants, bundle.Tenants);
            await InsertEntities(db, db.TenantRoles, bundle.TenantRoles);
            await InsertEntities(db, db.TenantMemberships, bundle.TenantMemberships);
            await InsertEntities(db, db.Groups, bundle.Groups);
            await InsertEntities(db, db.GroupMemberships, bundle.GroupMemberships);
            await InsertEntities(db, db.Environments, bundle.Environments);
            await InsertEntities(db, db.Customers, bundle.Customers);
            await InsertEntities(db, db.CustomerAccesses, bundle.CustomerAccesses);
            await InsertEntities(db, db.Apps, bundle.Apps);
            await InsertEntities(db, db.AppEnvironments, bundle.AppEnvironments);

            // Infrastructure — OpenStack and K8s clusters before storage links
            // (StorageLink.OpenStackConnectionId and StorageLink.ComponentId are optional
            // FKs that must exist when non-null).
            await InsertEntities(db, db.OpenStackConnections, bundle.OpenStackConnections);
            await InsertEntities(db, db.KubernetesClusters, bundle.KubernetesClusters);
            await InsertEntities(db, db.ClusterComponents, bundle.ClusterComponents);
            await InsertEntities(db, db.ExternalRoutes, bundle.ExternalRoutes);
            await InsertEntities(db, db.StorageLinks, bundle.StorageLinks);

            // Deployments — apps and clusters must exist first (RESTRICT FKs).
            await InsertEntities(db, db.AppDeployments, bundle.AppDeployments);
            await InsertEntities(db, db.DeploymentManifests, bundle.DeploymentManifests);
            await InsertEntities(db, db.StorageBindings, bundle.StorageBindings);

            // Databases — after storage links and k8s clusters.
            await InsertEntities(db, db.RegisteredPostgresInstances, bundle.RegisteredPostgresInstances);
            await InsertEntities(db, db.RegisteredPostgresDatabases, bundle.RegisteredPostgresDatabases);
            await InsertEntities(db, db.CnpgClusters, bundle.CnpgClusters);
            await InsertEntities(db, db.CnpgDatabases, bundle.CnpgDatabases);
            await InsertEntities(db, db.MongoClusters, bundle.MongoClusters);
            await InsertEntities(db, db.MongoDatabases, bundle.MongoDatabases);
            await InsertEntities(db, db.RabbitMQClusters, bundle.RabbitMQClusters);
            await InsertEntities(db, db.DatabaseBindings, bundle.DatabaseBindings);
            await InsertEntities(db, db.MessagingBindings, bundle.MessagingBindings);

            // Identity/Auth providers — after cluster components and databases.
            await InsertEntities(db, db.KeycloakComponentConfigs, bundle.KeycloakComponentConfigs);
            await InsertEntities(db, db.KeycloakThemes, bundle.KeycloakThemes);
            await InsertEntities(db, db.KeycloakRealms, bundle.KeycloakRealms);

            // Container registry.
            await InsertEntities(db, db.HarborComponentConfigs, bundle.HarborComponentConfigs);
            await InsertEntities(db, db.HarborProjects, bundle.HarborProjects);

            // Operations.
            await InsertEntities(db, db.NotificationChannels, bundle.NotificationChannels);
            await InsertEntities(db, db.SlaTargets, bundle.SlaTargets);
            await InsertEntities(db, db.MaintenanceWindows, bundle.MaintenanceWindows);

            // Secrets — generate a fresh DEK for every vault and re-encrypt all
            // secrets with it. The new DEK is sealed with the new server's root key.
            Dictionary<Guid, byte[]> newDekByVaultId = [];

            foreach (VaultRecord vr in bundle.SecretVaults)
            {
                byte[] dek = encryption.GenerateDataKey();
                (byte[] encKey, byte[] nonce) = encryption.SealDataKey(dek);
                newDekByVaultId[vr.Id] = dek;
                db.SecretVaults.Add(new SecretVault
                {
                    Id = vr.Id, TenantId = vr.TenantId,
                    EncryptedDataKey = encKey, Nonce = nonce,
                    CreatedAt = vr.CreatedAt,
                });
            }
            if (bundle.SecretVaults.Count > 0)
            {
                await db.SaveChangesAsync();
                db.ChangeTracker.Clear();
            }

            foreach (VaultSecretRecord sr in bundle.VaultSecrets)
            {
                if (!newDekByVaultId.TryGetValue(sr.VaultId, out byte[]? dek))
                {
                    logger.LogWarning("VaultSecret {Id} vault {VaultId} not in bundle — skipping.", sr.Id, sr.VaultId);
                    continue;
                }
                (byte[] ciphertext, byte[] sNonce) = encryption.Encrypt(dek, sr.PlaintextValue);
                db.VaultSecrets.Add(new VaultSecret
                {
                    Id = sr.Id, VaultId = sr.VaultId, Name = sr.Name,
                    EncryptedValue = ciphertext, Nonce = sNonce,
                    AppId = sr.AppId, ComponentId = sr.ComponentId, StorageLinkId = sr.StorageLinkId,
                    OpenStackConnectionId = sr.OpenStackConnectionId, CnpgClusterId = sr.CnpgClusterId,
                    CnpgDatabaseId = sr.CnpgDatabaseId, MongoDatabaseId = sr.MongoDatabaseId,
                    MongoClusterId = sr.MongoClusterId, RegisteredPostgresDatabaseId = sr.RegisteredPostgresDatabaseId,
                    RabbitMQClusterId = sr.RabbitMQClusterId,
                    SyncToKubernetes = sr.SyncToKubernetes, KubernetesClusterId = sr.KubernetesClusterId,
                    KubernetesSecretName = sr.KubernetesSecretName, KubernetesNamespace = sr.KubernetesNamespace,
                    CreatedAt = sr.CreatedAt, UpdatedAt = sr.UpdatedAt,
                });
            }
            if (bundle.VaultSecrets.Count > 0)
            {
                await db.SaveChangesAsync();
                db.ChangeTracker.Clear();
            }

            foreach (DockerCredentialRecord cr in bundle.DockerCredentials)
            {
                if (!newDekByVaultId.TryGetValue(cr.VaultId, out byte[]? dek))
                {
                    logger.LogWarning("DockerCredential {Id} vault {VaultId} not in bundle — skipping.", cr.Id, cr.VaultId);
                    continue;
                }
                (byte[] encPassword, byte[] pwNonce) = encryption.Encrypt(dek, cr.PlaintextPassword);
                db.DockerRegistryCredentials.Add(new DockerRegistryCredential
                {
                    Id = cr.Id, VaultId = cr.VaultId, Name = cr.Name, RegistryType = cr.RegistryType,
                    Server = cr.Server, Username = cr.Username,
                    EncryptedPassword = encPassword, PasswordNonce = pwNonce,
                    Email = cr.Email, AppId = cr.AppId, KubernetesClusterId = cr.KubernetesClusterId,
                    KubernetesSecretName = cr.KubernetesSecretName, KubernetesNamespace = cr.KubernetesNamespace,
                    CreatedAt = cr.CreatedAt, UpdatedAt = cr.UpdatedAt,
                });
            }
            if (bundle.DockerCredentials.Count > 0)
            {
                await db.SaveChangesAsync();
                db.ChangeTracker.Clear();
            }

            await tx.CommitAsync();

            logger.LogInformation(
                "Restore completed: {Tenants} tenants, {Apps} apps, {Secrets} secrets, {Clusters} clusters.",
                bundle.Tenants.Count, bundle.Apps.Count, bundle.VaultSecrets.Count, bundle.KubernetesClusters.Count);
        }
        catch
        {
            await tx.RollbackAsync();
            throw;
        }
    }

    // ── Helpers ─────────────────────────────────────────────────────────────

    private static async Task InsertEntities<T>(ApplicationDbContext db, DbSet<T> set, List<T> items)
        where T : class
    {
        if (items.Count == 0) return;
        // Clear navigations so EF Core only inserts the scalar/FK columns.
        // Navigations are null or empty after deserialization (no Include was used
        // on export), so this mainly prevents EF from trying to chase them.
        foreach (T item in items)
            set.Add(item);
        await db.SaveChangesAsync();
        db.ChangeTracker.Clear();
    }

    private async Task InsertBatch<T>(ApplicationDbContext db, IEnumerable<T> items)
        where T : class
    {
        List<T> list = items.ToList();
        if (list.Count == 0) return;
        db.Set<T>().AddRange(list);
        await db.SaveChangesAsync();
        db.ChangeTracker.Clear();
    }
}
