using System.Reflection;
using Baya.Application.Contracts.Common;
using Baya.Domain.Common;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.User;
using Baya.Domain.Entities.Verification;
using Baya.Infrastructure.Persistence.ValueConversion;
using Baya.SharedKernel.Extensions;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Baya.Infrastructure.Persistence;

public class ApplicationDbContext: IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>
{
    private readonly IFieldEncryptor _fieldEncryptor;

    // The encryptor ends up captured inside the cached EF model (value converters), so it must be a
    // process-wide singleton with stable keys — which is how the seam is registered.
    public ApplicationDbContext(DbContextOptions options, IFieldEncryptor fieldEncryptor)
        : base(options)
    {
        _fieldEncryptor = fieldEncryptor;
        base.SavingChanges += OnSavingChanges;
    }

    private void OnSavingChanges(object sender, SavingChangesEventArgs e)
    {
        _cleanString();
        _syncUserPhoneIntegrity();
    }

    /// <summary>
    /// Keeps the deterministic <c>PhoneHash</c> lookup column in step with the encrypted phone, and
    /// enforces the product rule that a phone change invalidates the Shahkar phone↔national-id binding
    /// (reset to NULL so b6 re-verifies) — centrally, so no handler can forget it.
    /// </summary>
    private void _syncUserPhoneIntegrity()
    {
        foreach (var entry in ChangeTracker.Entries<User>())
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.PhoneHash = _fieldEncryptor.Hash(entry.Entity.PhoneNumber);
            }
            else if (entry.State == EntityState.Modified)
            {
                var phone = entry.Property(u => u.PhoneNumber);
                if (!string.Equals(phone.OriginalValue, phone.CurrentValue, StringComparison.Ordinal))
                {
                    entry.Entity.PhoneHash = _fieldEncryptor.Hash(phone.CurrentValue);
                    entry.Entity.ShahkarVerifiedAt = null;
                }
            }
        }
    }

    private void _cleanString()
    {
        var changedEntities = ChangeTracker.Entries()
            .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);
        foreach (var item in changedEntities)
        {
            if (item.Entity == null)
                continue;

            var properties = item.Entity.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                var propName = property.Name;
                var val = (string)property.GetValue(item.Entity, null);

                if (val.HasValue())
                {
                    var newVal = val.Fa2En().FixPersianChars();
                    if (newVal == val)
                        continue;
                    property.SetValue(item.Entity, newVal, null);
                }
            }
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

        base.OnModelCreating(modelBuilder);

        var entitiesAssembly = typeof(IEntity).Assembly;
        modelBuilder.RegisterAllEntities<IEntity>(entitiesAssembly);
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
        modelBuilder.AddRestrictDeleteBehaviorConvention();
        modelBuilder.AddPluralizingTableNameConvention();

        // PII encrypted at rest via the seam. These columns are equality-unqueryable by design —
        // phone lookups use PhoneHash. Applied here (not in UserConfig) because the converter needs
        // the encryptor instance.
        var encrypted = new EncryptedStringConverter(_fieldEncryptor);
        modelBuilder.Entity<User>(builder =>
        {
            builder.Property(u => u.PhoneNumber).HasConversion(encrypted);
            builder.Property(u => u.Email).HasConversion(encrypted);
            builder.Property(u => u.NormalizedEmail).HasConversion(encrypted);
            builder.Property(u => u.NationalId).HasConversion(encrypted);
        });

        // b3 PII: emergency contacts, clinical notes, IBAN and the account-holder name are encrypted at
        // rest through the same seam. The IBAN's deterministic lookup uses the iban_hash column instead.
        modelBuilder.Entity<CustomerProfile>(builder =>
        {
            builder.Property(c => c.DefaultEmergencyContactName).HasConversion(encrypted);
            builder.Property(c => c.DefaultEmergencyContactPhone).HasConversion(encrypted);
        });
        modelBuilder.Entity<Patient>(builder =>
        {
            builder.Property(p => p.InitialMedicalNotes).HasConversion(encrypted);
        });
        modelBuilder.Entity<NurseBankAccount>(builder =>
        {
            builder.Property(a => a.AccountHolderName).HasConversion(encrypted);
            builder.Property(a => a.Iban).HasConversion(encrypted);
        });

        // b4 address PII: street line, postal code and recipient contact are encrypted at rest through
        // the same seam; the title label and coordinates are not PII and stay plaintext.
        modelBuilder.Entity<CustomerAddress>(builder =>
        {
            builder.Property(a => a.AddressLine).HasConversion(encrypted);
            builder.Property(a => a.PostalCode).HasConversion(encrypted);
            builder.Property(a => a.RecipientName).HasConversion(encrypted);
            builder.Property(a => a.RecipientPhone).HasConversion(encrypted);
        });

        // b6 credential PII: the license/membership number is encrypted at rest through the same seam and
        // is never serialized on the wire (the trust badge exposes credential types, never numbers).
        modelBuilder.Entity<NurseCredential>(builder =>
        {
            builder.Property(c => c.CredentialNumber).HasConversion(encrypted);
        });

        // b9 snapshot + clinical PII: the frozen address snapshot and every booking_care_instructions field
        // are encrypted at rest through the same seam. The care fields are the stage-2 clinical disclosure —
        // decrypted only in the gated care-instructions read (assigned nurse + admin, post-confirmation).
        modelBuilder.Entity<Booking>(builder =>
        {
            builder.Property(b => b.AddressSnapshotJson).HasConversion(encrypted);
        });
        modelBuilder.Entity<BookingCareInstruction>(builder =>
        {
            builder.Property(c => c.CurrentConditions).HasConversion(encrypted);
            builder.Property(c => c.Medications).HasConversion(encrypted);
            builder.Property(c => c.Allergies).HasConversion(encrypted);
            builder.Property(c => c.SpecialInstructions).HasConversion(encrypted);
            builder.Property(c => c.EmergencyContactName).HasConversion(encrypted);
            builder.Property(c => c.EmergencyContactPhone).HasConversion(encrypted);
        });

        // b10 gateway config: provider-selection/failover config (merchant id, terminal/IBAN registration,
        // base url, sandbox flag) is encrypted at rest through the same seam and never logged in plaintext.
        modelBuilder.Entity<PaymentGateway>(builder =>
        {
            builder.Property(g => g.ConfigJson).HasConversion(encrypted);
        });

        // b13 payout snapshot: the nurse's IBAN is frozen onto each payout at build time and encrypted at rest
        // through the same seam. Reads mask it to the last 4 digits — the plaintext IBAN is never serialized.
        modelBuilder.Entity<Baya.Domain.Entities.Payouts.NursePayout>(builder =>
        {
            builder.Property(p => p.IbanSnapshot).HasConversion(encrypted);
        });

        // b15 partner-center settlement account: the center's IBAN (only when merchant-of-record) is encrypted
        // at rest through the same seam and never serialized in plaintext — reads mask it to the last 4 digits.
        modelBuilder.Entity<Baya.Domain.Entities.PartnerCenters.PartnerCenter>(builder =>
        {
            builder.Property(c => c.SettlementIban).HasConversion(encrypted);
        });

        // refinement-phase-9 §9.5: ticket message bodies are the refund/dispute paper trail — users type phone
        // numbers, addresses and clinical detail into them — so they are encrypted at rest through the same seam.
        // The plaintext-length limit (4000) stays a boundary-validation rule; the stored ciphertext column is
        // widened to nvarchar(max) in TicketMessageConfig. Body is never a search/filter predicate (the admin
        // thread read decrypts per row), so losing SQL-searchability on it is an accepted trade-off.
        modelBuilder.Entity<Baya.Domain.Entities.Messaging.TicketMessage>(builder =>
        {
            builder.Property(m => m.Body).HasConversion(encrypted);
        });
    }
}