using System.Text.Json;
using foodmarket.Application.Common.Tenancy;
using foodmarket.Domain.Catalog;
using foodmarket.Domain.Organizations;
using foodmarket.Domain.Purchases;
using foodmarket.Domain.Sales;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace foodmarket.Infrastructure.Persistence;

/// <summary>EF Core <c>SaveChangesInterceptor</c>, который снимает diff
/// отслеживаемых сущностей и пишет строки <see cref="OrgAuditLog"/>.
///
/// Срабатывает в <c>SavingChanges</c> (до commit): мы не плодим лог для
/// rolled-back изменений. Лог записывается в тот же DbContext в коллекции
/// будущей SaveChanges'ов — НЕ как отдельный round-trip; одна транзакция,
/// одна атомарность. Параметр <c>SkipAudit</c> позволяет отключить
/// инструментацию для специфических операций (seed, миграция).
///
/// Отслеживаемые типы — белый список (см. <c>IsTracked</c>); расширять
/// сюда, не через рефлексию по всем DbSet'ам, чтобы аудит-лог не
/// раздувался самой меняющей его строкой.</summary>
public sealed class OrgAuditInterceptor : SaveChangesInterceptor
{
    private readonly ITenantContext _tenant;

    public OrgAuditInterceptor(ITenantContext tenant) => _tenant = tenant;

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

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

    private void WriteAuditEntries(DbContext? ctx)
    {
        if (ctx is not AppDbContext db) return;
        if (db.SkipAudit) return;
        // Аудитные записи не аудируем (бесконечная рекурсия).
        var entries = db.ChangeTracker.Entries()
            .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
            .Where(e => IsTracked(e.Entity))
            .ToList();
        if (entries.Count == 0) return;

        var orgId = _tenant.OrganizationId;
        if (orgId is null) return; // системные операции (SuperAdmin без override / seed)

        var userId = _tenant.UserId;
        foreach (var entry in entries)
        {
            var (action, diff) = ComputeDiff(entry);
            // Diff может быть пустым (Update без реальных изменений) — пропускаем.
            if (action == "update" && diff.Count == 0) continue;
            var id = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey())?.CurrentValue as Guid?;
            db.Add(new OrgAuditLog
            {
                OrganizationId = orgId.Value,
                UserId = userId,
                Action = action,
                EntityType = entry.Entity.GetType().Name,
                EntityId = id,
                ChangesJson = JsonSerializer.Serialize(diff),
            });
        }
    }

    private static bool IsTracked(object entity) => entity switch
    {
        Supply or SupplyLine => true,
        SupplierReturn or SupplierReturnLine => true,
        RetailSale or RetailSaleLine => true,
        Demand or DemandLine => true,
        Product or ProductPrice or ProductBarcode => true,
        Counterparty => true,
        _ => false,
    };

    private static (string Action, Dictionary<string, object> Diff) ComputeDiff(EntityEntry entry)
    {
        var diff = new Dictionary<string, object>();
        switch (entry.State)
        {
            case EntityState.Added:
                foreach (var p in entry.Properties)
                {
                    if (IsSkippable(p.Metadata.Name)) continue;
                    if (p.CurrentValue is null) continue;
                    diff[p.Metadata.Name] = new { after = p.CurrentValue };
                }
                return ("create", diff);
            case EntityState.Modified:
                foreach (var p in entry.Properties)
                {
                    if (IsSkippable(p.Metadata.Name)) continue;
                    if (!p.IsModified) continue;
                    if (Equals(p.OriginalValue, p.CurrentValue)) continue;
                    diff[p.Metadata.Name] = new { before = p.OriginalValue, after = p.CurrentValue };
                }
                return ("update", diff);
            case EntityState.Deleted:
                foreach (var p in entry.Properties)
                {
                    if (IsSkippable(p.Metadata.Name)) continue;
                    if (p.OriginalValue is null) continue;
                    diff[p.Metadata.Name] = new { before = p.OriginalValue };
                }
                return ("delete", diff);
            default:
                return ("noop", diff);
        }
    }

    /// <summary>Поля, которые не имеет смысла включать в diff — служебные
    /// и шумные. Tenant, timestamps, навигационные FK дублируются с самими
    /// сущностями, а CreatedAt/UpdatedAt меняются при каждом сохранении.</summary>
    private static bool IsSkippable(string name) => name switch
    {
        "OrganizationId" or "CreatedAt" or "UpdatedAt" => true,
        _ => false,
    };
}
