using System.Text.Json;
using System.Text.Json.Serialization;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace ERPCore.Infra.Persistence.Auditing;

/// <summary>A mutation captured before save, awaiting its (possibly generated) key.</summary>
public sealed record PendingAudit(EntityEntry Entry, string EntityType, AuditAction Action, string ChangeSet, int CapturedId, bool IsAdded);

/// <summary>
/// Builds audit-trail rows from the EF change tracker (FR-X-02). High-volume /
/// derived / self-referential tables are excluded (the stock ledger is itself the
/// stock movement audit). Change sets are captured <b>before</b> save so old→new is
/// accurate; generated keys for inserts are read <b>after</b> save.
/// </summary>
public static class AuditScribe
{
    private static readonly HashSet<Type> Excluded =
    [
        typeof(AuditLog), typeof(JournalEntryStub), typeof(NumberSequence),
        typeof(StockLedger), typeof(StockLayer),
    ];

    private static readonly JsonSerializerOptions Json = new()
    {
        Converters = { new JsonStringEnumConverter() },
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };

    public static List<PendingAudit> Capture(ChangeTracker tracker)
    {
        var pending = new List<PendingAudit>();
        foreach (var entry in tracker.Entries())
        {
            if (entry.State is not (EntityState.Added or EntityState.Modified or EntityState.Deleted)) continue;
            if (Excluded.Contains(entry.Entity.GetType())) continue;

            var action = entry.State switch
            {
                EntityState.Added => AuditAction.Create,
                EntityState.Deleted => AuditAction.Delete,
                _ => AuditAction.Update,
            };

            var changeSet = BuildChangeSet(entry, action);
            if (action == AuditAction.Update && changeSet == "{}") continue; // only concurrency token touched, etc.

            var isAdded = entry.State == EntityState.Added;
            pending.Add(new PendingAudit(entry, entry.Entity.GetType().Name, action, changeSet, isAdded ? 0 : ReadKey(entry), isAdded));
        }
        return pending;
    }

    public static AuditLog ToLog(PendingAudit p, int userId, DateTime now) => new()
    {
        UserId = userId,
        EntityType = p.EntityType,
        EntityId = p.IsAdded ? ReadKey(p.Entry) : p.CapturedId,
        Action = p.Action,
        ChangeSet = p.ChangeSet,
        CreatedAt = now,
    };

    private static int ReadKey(EntityEntry entry)
    {
        var pk = entry.Metadata.FindPrimaryKey();
        if (pk is null || pk.Properties.Count != 1) return 0;
        var value = entry.Property(pk.Properties[0].Name).CurrentValue;
        return value is null ? 0 : Convert.ToInt32(value);
    }

    private static string BuildChangeSet(EntityEntry entry, AuditAction action)
    {
        var set = new Dictionary<string, object?>();
        foreach (var p in entry.Properties)
        {
            if (p.Metadata.IsPrimaryKey()) continue;
            if (p.Metadata.Name == nameof(Item.RowVersion)) continue;

            switch (action)
            {
                case AuditAction.Create when p.CurrentValue is not null:
                    set[p.Metadata.Name] = p.CurrentValue;
                    break;
                case AuditAction.Delete:
                    set[p.Metadata.Name] = p.OriginalValue;
                    break;
                case AuditAction.Update when p.IsModified && !Equals(p.OriginalValue, p.CurrentValue):
                    set[p.Metadata.Name] = new Dictionary<string, object?> { ["old"] = p.OriginalValue, ["new"] = p.CurrentValue };
                    break;
            }
        }
        return JsonSerializer.Serialize(set, Json);
    }
}
