using DodoSSH.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace DodoSSH.Infrastructure.Configurations;

/// <summary>Maps <see cref="UserAccount"/>.</summary>
public sealed class UserAccountConfiguration : IEntityTypeConfiguration<UserAccount>
{
    /// <inheritdoc />
    public void Configure(EntityTypeBuilder<UserAccount> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.ToTable("user_account");
        builder.HasKey(u => u.Id);
        builder.Property(u => u.Id).ValueGeneratedNever();
        builder.UseXminConcurrencyToken();

        builder.Property(u => u.Issuer).HasMaxLength(512).IsRequired();
        builder.Property(u => u.Subject).HasMaxLength(256).IsRequired();

        // citext, so lookups and uniqueness are case-insensitive without lower() everywhere.
        builder.Property(u => u.Email).HasColumnType("citext").HasMaxLength(320);
        builder.Property(u => u.DisplayName).HasMaxLength(256);

        builder.Property(u => u.Status).HasConversion<int>();

        // The natural key. Multi-issuer from the start so a second provider does not require a
        // schema change; the issuer is part of the identity, not a detail.
        builder.HasIndex(u => new { u.Issuer, u.Subject }).IsUnique();

        // Email is not unique in general — only among live accounts, and only when present.
        builder.HasIndex(u => u.Email)
            .IsUnique()
            .HasFilter("email IS NOT NULL AND deleted_at_utc IS NULL");

        builder.HasMany(u => u.Keys)
            .WithOne(k => k.User)
            .HasForeignKey(k => k.UserId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasMany(u => u.KeyWraps)
            .WithOne(w => w.User)
            .HasForeignKey(w => w.UserId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasMany(u => u.Devices)
            .WithOne(d => d.User)
            .HasForeignKey(d => d.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

/// <summary>Maps <see cref="UserKey"/>.</summary>
public sealed class UserKeyConfiguration : IEntityTypeConfiguration<UserKey>
{
    /// <inheritdoc />
    public void Configure(EntityTypeBuilder<UserKey> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.ToTable("user_key");
        builder.HasKey(k => k.Id);
        builder.Property(k => k.Id).ValueGeneratedNever();

        builder.Property(k => k.EncryptionPublicKey).HasMaxLength(32).IsRequired();
        builder.Property(k => k.SigningPublicKey).HasMaxLength(32).IsRequired();
        builder.Property(k => k.FingerprintSha256).HasMaxLength(32).IsRequired();
        builder.Property(k => k.Statement).HasColumnType("jsonb").IsRequired();
        builder.Property(k => k.StatementSignature).HasMaxLength(64).IsRequired();
        builder.Property(k => k.IdentityProviderBinding).HasColumnType("jsonb");

        builder.HasIndex(k => new { k.UserId, k.Generation }).IsUnique();

        // Exactly one current generation per user, enforced by the database rather than by
        // convention: two "current" keys would make it ambiguous which one to wrap to.
        builder.HasIndex(k => k.UserId)
            .IsUnique()
            .HasFilter("is_current")
            .HasDatabaseName("ix_user_key_current");

        builder.HasIndex(k => k.FingerprintSha256).IsUnique();
    }
}

/// <summary>Maps <see cref="UserKeyWrap"/>.</summary>
public sealed class UserKeyWrapConfiguration : IEntityTypeConfiguration<UserKeyWrap>
{
    /// <inheritdoc />
    public void Configure(EntityTypeBuilder<UserKeyWrap> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.ToTable("user_key_wrap");
        builder.HasKey(w => w.Id);
        builder.Property(w => w.Id).ValueGeneratedNever();
        builder.UseXminConcurrencyToken();

        builder.Property(w => w.Kind).HasConversion<int>();
        builder.Property(w => w.Wrap).IsRequired();
        builder.Property(w => w.KdfAlgorithm).HasMaxLength(64);
        builder.Property(w => w.KdfSalt).HasMaxLength(64);

        builder.HasOne(w => w.Device)
            .WithMany()
            .HasForeignKey(w => w.DeviceId)
            .OnDelete(DeleteBehavior.Cascade);

        // One passphrase wrap and one recovery wrap per user; one device wrap per device.
        builder.HasIndex(w => new { w.UserId, w.Kind })
            .IsUnique()
            .HasFilter("device_id IS NULL")
            .HasDatabaseName("ix_user_key_wrap_user_kind");

        builder.HasIndex(w => new { w.UserId, w.DeviceId })
            .IsUnique()
            .HasFilter("device_id IS NOT NULL")
            .HasDatabaseName("ix_user_key_wrap_user_device");

        // A password-derived wrap is useless without its parameters, and a wrap that is not
        // password-derived must not carry them. Enforced here so a partial write cannot leave a
        // bundle permanently unopenable.
        builder.ToTable(t => t.HasCheckConstraint(
            "ck_user_key_wrap_kdf",
            """
            (kind IN (1, 3) AND kdf_algorithm IS NOT NULL AND kdf_salt IS NOT NULL
                AND kdf_memory_kibibytes IS NOT NULL AND kdf_passes IS NOT NULL
                AND kdf_parallelism IS NOT NULL)
            OR (kind IN (2, 4) AND kdf_algorithm IS NULL AND kdf_salt IS NULL)
            """));

        // A device wrap must name its device; the others must not.
        builder.ToTable(t => t.HasCheckConstraint(
            "ck_user_key_wrap_device",
            "(kind = 2 AND device_id IS NOT NULL) OR (kind <> 2 AND device_id IS NULL)"));
    }
}

/// <summary>Maps <see cref="Device"/>.</summary>
public sealed class DeviceConfiguration : IEntityTypeConfiguration<Device>
{
    /// <inheritdoc />
    public void Configure(EntityTypeBuilder<Device> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.ToTable("device");
        builder.HasKey(d => d.Id);
        builder.Property(d => d.Id).ValueGeneratedNever();

        builder.Property(d => d.Name).HasMaxLength(256).IsRequired();
        builder.Property(d => d.Platform).HasConversion<int>();
        builder.Property(d => d.PublicKey).HasMaxLength(32).IsRequired();

        builder.HasIndex(d => d.UserId);
    }
}

/// <summary>Maps <see cref="KeyLogEntry"/>.</summary>
public sealed class KeyLogEntryConfiguration : IEntityTypeConfiguration<KeyLogEntry>
{
    /// <inheritdoc />
    public void Configure(EntityTypeBuilder<KeyLogEntry> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.ToTable("key_log");
        builder.HasKey(e => e.Sequence);
        builder.Property(e => e.Sequence).UseIdentityAlwaysColumn();

        builder.Property(e => e.EncryptionPublicKey).HasMaxLength(32).IsRequired();
        builder.Property(e => e.SigningPublicKey).HasMaxLength(32).IsRequired();
        builder.Property(e => e.StatementSignature).HasMaxLength(64).IsRequired();
        builder.Property(e => e.PreviousHash).HasMaxLength(32).IsRequired();
        builder.Property(e => e.Hash).HasMaxLength(32).IsRequired();

        builder.HasIndex(e => e.UserId);
        builder.HasIndex(e => e.Hash).IsUnique();
    }
}
