using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ROLAC.API.Entities.Base;
using ROLAC.API.Entities.Logging;
using ROLAC.API.Services.Logging;

namespace ROLAC.API.Data.Interceptors;

/// <summary>
/// Writes a before→after <see cref="AuditLog"/> row for every Create/Update/Delete of an
/// <see cref="IAuditable"/> entity. Two-phase: snapshot changed values BEFORE save (while
/// original values are still available), then — AFTER save succeeds — read DB-generated keys and
/// enqueue the rows. Enqueuing (rather than inserting here) avoids a second SaveChanges, can't
/// fail the user's transaction, and never recurses through AppDbContext.
/// </summary>
public sealed class AuditLogInterceptor : SaveChangesInterceptor
{
    private readonly SystemLogQueue      _queue;
    private readonly CurrentUserAccessor _currentUser;
    private readonly List<PendingAudit>  _pending = [];

    public AuditLogInterceptor(SystemLogQueue queue, CurrentUserAccessor currentUser)
    {
        _queue       = queue;
        _currentUser = currentUser;
    }

    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData, InterceptionResult<int> result)
    {
        Capture(eventData.Context);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData, InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        Capture(eventData.Context);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    public override int SavedChanges(SaveChangesCompletedEventData eventData, int result)
    {
        Flush();
        return base.SavedChanges(eventData, result);
    }

    public override ValueTask<int> SavedChangesAsync(
        SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default)
    {
        Flush();
        return base.SavedChangesAsync(eventData, result, cancellationToken);
    }

    public override void SaveChangesFailed(DbContextErrorEventData eventData) => _pending.Clear();

    public override Task SaveChangesFailedAsync(
        DbContextErrorEventData eventData, CancellationToken cancellationToken = default)
    {
        _pending.Clear();
        return Task.CompletedTask;
    }

    // ── Phase 1: snapshot before save ─────────────────────────────────────────
    private void Capture(DbContext? db)
    {
        if (db is null)
            return;

        foreach (var entry in db.ChangeTracker.Entries())
        {
            if (entry.Entity is not IAuditable)
                continue;

            switch (entry.State)
            {
                case EntityState.Added:
                    _pending.Add(new PendingAudit(entry, AuditActions.Create, null, BuildValues(entry, current: true)));
                    break;

                case EntityState.Deleted:
                    _pending.Add(new PendingAudit(entry, AuditActions.Delete, BuildValues(entry, current: false), null));
                    break;

                case EntityState.Modified:
                    var before = new Dictionary<string, object?>();
                    var after  = new Dictionary<string, object?>();
                    foreach (var property in entry.Properties)
                    {
                        if (!property.IsModified)
                            continue;
                        var name = property.Metadata.Name;
                        before[name] = Read(name, property.OriginalValue);
                        after[name]  = Read(name, property.CurrentValue);
                    }
                    if (after.Count == 0)
                        break; // no real change (e.g. only audit timestamps touched on a no-op)

                    // A soft-delete (IsDeleted false→true) reads more naturally as a Delete.
                    var action = IsSoftDelete(after) ? AuditActions.Delete : AuditActions.Update;
                    _pending.Add(new PendingAudit(entry, action, before, after));
                    break;
            }
        }
    }

    // ── Phase 2: keys exist, enqueue ──────────────────────────────────────────
    private void Flush()
    {
        if (_pending.Count == 0)
            return;

        var userId    = _currentUser.UserId;
        var userEmail = _currentUser.Email;
        var ip        = _currentUser.IpAddress;
        var corr      = _currentUser.CorrelationId;

        foreach (var item in _pending)
        {
            _queue.TryEnqueue(new AuditLog
            {
                Timestamp     = DateTimeOffset.UtcNow,
                Level         = LogLevelEnum.Information,
                Action        = item.Action,
                Category      = AuditCategories.DataChange,
                EntityName    = item.Entry.Metadata.ClrType.Name,
                EntityId      = ReadKey(item.Entry),
                Changes       = AuditChangeSerializer.BuildChanges(item.Before, item.After),
                UserId        = userId,
                UserEmail     = userEmail,
                IpAddress     = ip,
                CorrelationId = corr,
            });
        }

        _pending.Clear();
    }

    private static Dictionary<string, object?> BuildValues(EntityEntry entry, bool current)
    {
        var values = new Dictionary<string, object?>();
        foreach (var property in entry.Properties)
        {
            if (property.Metadata.IsPrimaryKey())
                continue;
            var name = property.Metadata.Name;
            values[name] = Read(name, current ? property.CurrentValue : property.OriginalValue);
        }
        return values;
    }

    private static object? Read(string propertyName, object? value) =>
        AuditChangeSerializer.IsSensitive(propertyName) ? AuditChangeSerializer.MaskValue : value;

    private static bool IsSoftDelete(Dictionary<string, object?> after) =>
        after.TryGetValue("IsDeleted", out var value) && value is true;

    private static string? ReadKey(EntityEntry entry)
    {
        var key = entry.Metadata.FindPrimaryKey();
        if (key is null)
            return null;

        var parts = key.Properties
            .Select(p => entry.Property(p.Name).CurrentValue?.ToString())
            .Where(v => v is not null);
        return string.Join(",", parts);
    }

    private sealed record PendingAudit(
        EntityEntry Entry,
        string Action,
        Dictionary<string, object?>? Before,
        Dictionary<string, object?>? After);
}
