using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using OmsorgCore.Application.Abstractions;
using OmsorgCore.Domain.Common;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Domain.Enums;

namespace OmsorgCore.Infrastructure.Persistence;

/// <summary>
/// Erfasst automatisch jede Create/Update/(Soft-)Delete-Änderung an einer AuditableEntity als
/// AuditLogEntry - läuft im selben SaveChanges-Aufruf/derselben Transaktion wie die eigentliche
/// Änderung (kein separater Commit, kein Risiko einer verlorenen Audit-Spur). Erfasst NICHT
/// Nicht-Entity-Aktionen (Login/Logout/Session-Kill) - dafür AuditEvent (siehe Engine).
/// Neue AuditableEntity-Subklassen (Facility, Contract, ...) werden ohne Codeänderung hier erfasst.
/// </summary>
public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly ICurrentUserService _currentUserService;

    public AuditSaveChangesInterceptor(ICurrentUserService currentUserService)
    {
        _currentUserService = currentUserService;
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is not null)
        {
            CaptureAuditEntries(eventData.Context);
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void CaptureAuditEntries(DbContext context)
    {
        var isAuthenticated = _currentUserService.IsAuthenticated;
        var actorUserId = isAuthenticated ? _currentUserService.UserId : null;
        var actorUsername = isAuthenticated ? _currentUserService.Username : null;
        var ipAddress = _currentUserService.IpAddress;

        // ToList(): Add() unten mutiert den ChangeTracker - ohne Materialisierung würde die laufende
        // Enumeration von Entries<T>() dadurch invalidiert (InvalidOperationException).
        foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>().ToList())
        {
            if (entry.State is not (EntityState.Added or EntityState.Modified or EntityState.Deleted))
            {
                continue;
            }

            var (action, details) = BuildActionAndDiff(entry);
            if (details is null)
            {
                // Modified-Eintrag ohne tatsächlich geänderte Skalar-Property (z.B. nur eine Navigation
                // berührt) - keine Rausch-Zeile im Audit-Log.
                continue;
            }

            context.Set<AuditLogEntry>().Add(new AuditLogEntry
            {
                OccurredAtUtc = DateTime.UtcNow,
                ActorUserId = actorUserId,
                ActorUsername = actorUsername,
                IpAddress = ipAddress,
                Category = AuditEventCategory.EntityChange,
                Action = action,
                EntityType = entry.Entity.GetType().Name,
                EntityId = entry.Entity.Id,
                Details = details
            });
        }
    }

    private static (string Action, string? DetailsJson) BuildActionAndDiff(EntityEntry<AuditableEntity> entry)
    {
        var redactedProperties = entry.Entity.GetType()
            .GetProperties()
            .Where(p => Attribute.IsDefined(p, typeof(AuditRedactedAttribute)))
            .Select(p => p.Name)
            .ToHashSet();

        switch (entry.State)
        {
            case EntityState.Added:
            {
                var payload = new Dictionary<string, object?>();
                foreach (var p in entry.Properties)
                {
                    payload[p.Metadata.Name] = redactedProperties.Contains(p.Metadata.Name) ? "***redacted***" : p.CurrentValue;
                }
                return ("Created", JsonSerializer.Serialize(payload));
            }
            case EntityState.Deleted:
            {
                var payload = new Dictionary<string, object?>();
                foreach (var p in entry.Properties)
                {
                    payload[p.Metadata.Name] = redactedProperties.Contains(p.Metadata.Name) ? "***redacted***" : p.OriginalValue;
                }
                return ("Deleted", JsonSerializer.Serialize(payload));
            }
            default:
            {
                var changed = entry.Properties
                    .Where(p => p.IsModified && !Equals(p.OriginalValue, p.CurrentValue))
                    .ToList();

                if (changed.Count == 0)
                {
                    return ("Updated", null);
                }

                var isSoftDelete = changed.Any(p =>
                    p.Metadata.Name == nameof(AuditableEntity.IsDeleted) &&
                    p.OriginalValue is false &&
                    p.CurrentValue is true);

                var diff = new Dictionary<string, object>();
                foreach (var p in changed)
                {
                    diff[p.Metadata.Name] = redactedProperties.Contains(p.Metadata.Name)
                        ? new { old = "***redacted***", @new = "***redacted***" }
                        : new { old = p.OriginalValue, @new = p.CurrentValue };
                }

                return (isSoftDelete ? "Deleted" : "Updated", JsonSerializer.Serialize(diff));
            }
        }
    }
}
