using System.Data;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using PowderCoating.Core.Entities;

namespace PowderCoating.Infrastructure.Data;

/// <summary>
/// EF Core SaveChanges interceptor that writes an AuditLog row for every
/// meaningful create / update / soft-delete on tracked entities.
/// Writes via raw SQL so it never re-triggers itself.
/// </summary>
public class AuditInterceptor : SaveChangesInterceptor
{
    // Entity types we care about auditing
    private static readonly HashSet<string> AuditedTypes = new(StringComparer.Ordinal)
    {
        nameof(Customer), nameof(Job), nameof(Quote), nameof(Equipment),
        nameof(MaintenanceRecord), nameof(Vendor),
        nameof(InventoryItem), nameof(Company),
        // Financial entities
        nameof(Invoice), nameof(Payment), nameof(Bill), nameof(BillPayment),
        nameof(Deposit), nameof(CreditMemo), nameof(GiftCertificate),
        nameof(Expense), nameof(PurchaseOrder),
        // Operations
        nameof(Appointment), nameof(InventoryTransaction), nameof(JobTemplate),
        // User management
        nameof(ApplicationUser)
    };

    // Properties to always exclude from change capture (noisy / binary)
    private static readonly HashSet<string> ExcludedProperties = new(StringComparer.Ordinal)
    {
        "LogoData", "ProfilePictureData", "Settings", "UpdatedAt", "CreatedAt"
    };

    // Per-context pending audit entries (ConditionalWeakTable gives us automatic cleanup)
    private readonly ConditionalWeakTable<DbContext, List<AuditLog>> _pending = new();

    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger<AuditInterceptor> _logger;

    public AuditInterceptor(IHttpContextAccessor httpContextAccessor, ILogger<AuditInterceptor> logger)
    {
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
    }

    // ── Collect entries BEFORE save (so we can capture OriginalValues) ──────────

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

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

    // ── Write entries AFTER save (entity IDs are populated now) ─────────────────

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

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

    // ────────────────────────────────────────────────────────────────────────────

    private void Collect(DbContext? context)
    {
        if (context == null) return;
        var entries = BuildEntries(context);
        if (entries.Count == 0) return;

        // Remove any stale entries left by a previous failed SaveChanges on this context.
        // Without this, EF Core's EnableRetryOnFailure would trigger a second SavingChanges
        // on the same DbContext instance, causing _pending.Add to throw ArgumentException.
        _pending.Remove(context);
        _pending.Add(context, entries);
    }

    private void FlushSync(DbContext? context)
    {
        if (context == null || !_pending.TryGetValue(context, out var entries)) return;
        _pending.Remove(context);
        foreach (var log in entries)
        {
            try { InsertSync(context, log); }
            catch (Exception ex) { _logger.LogError(ex, "Failed to write audit log entry for {Action} on {EntityType} {EntityId}", log.Action, log.EntityType, log.EntityId); }
        }
    }

    private async Task FlushAsync(DbContext? context, CancellationToken ct)
    {
        if (context == null || !_pending.TryGetValue(context, out var entries)) return;
        _pending.Remove(context);
        foreach (var log in entries)
        {
            try { await InsertAsync(context, log, ct); }
            catch (Exception ex) { _logger.LogError(ex, "Failed to write audit log entry for {Action} on {EntityType} {EntityId}", log.Action, log.EntityType, log.EntityId); }
        }
    }

    // ── Build AuditLog objects from change tracker ───────────────────────────────

    private List<AuditLog> BuildEntries(DbContext context)
    {
        var logs = new List<AuditLog>();
        var (userId, userName, companyId, ip) = GetRequestContext();

        foreach (var entry in context.ChangeTracker.Entries())
        {
            var typeName = entry.Entity.GetType().Name;
            if (!AuditedTypes.Contains(typeName)) continue;

            string action;
            string? oldJson = null;
            string? newJson = null;

            switch (entry.State)
            {
                case EntityState.Added:
                    action = "Created";
                    newJson = SerializeProperties(entry.CurrentValues, null);
                    break;

                case EntityState.Modified:
                    // Detect soft-delete vs plain update
                    var isDeletedProp = entry.Properties.FirstOrDefault(p => p.Metadata.Name == "IsDeleted");
                    if (isDeletedProp != null &&
                        isDeletedProp.CurrentValue is true &&
                        isDeletedProp.OriginalValue is false)
                    {
                        action = "Deleted";
                    }
                    else if (isDeletedProp != null &&
                             isDeletedProp.CurrentValue is false &&
                             isDeletedProp.OriginalValue is true)
                    {
                        action = "Restored";
                    }
                    else
                    {
                        action = "Updated";
                    }
                    oldJson = SerializeProperties(entry.OriginalValues, entry.CurrentValues);
                    newJson = SerializeProperties(entry.CurrentValues, entry.OriginalValues);
                    break;

                case EntityState.Deleted:
                    action = "Deleted";
                    oldJson = SerializeProperties(entry.OriginalValues, null);
                    break;

                default:
                    continue;
            }

            // Entity ID — try "Id" property
            var idProp = entry.Properties.FirstOrDefault(p => p.Metadata.Name == "Id");
            var entityId = idProp?.CurrentValue?.ToString();

            // Entity description — try common naming properties
            var description = GetDescription(entry);

            // Company ID on the entity itself
            var entityCompanyProp = entry.Properties.FirstOrDefault(p => p.Metadata.Name == "CompanyId");
            var entityCompanyId = entityCompanyProp?.CurrentValue as int? ?? companyId;

            logs.Add(new AuditLog
            {
                UserId = userId,
                UserName = userName,
                CompanyId = entityCompanyId,
                Action = action,
                EntityType = typeName,
                EntityId = entityId,
                EntityDescription = description,
                OldValues = oldJson,
                NewValues = newJson,
                IpAddress = ip,
                Timestamp = DateTime.UtcNow
            });
        }

        return logs;
    }

    private static string? SerializeProperties(Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues values,
        Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues? compareAgainst)
    {
        var dict = new Dictionary<string, object?>();
        foreach (var prop in values.Properties)
        {
            if (ExcludedProperties.Contains(prop.Name)) continue;
            var current = values[prop];

            // When comparing, only include changed properties
            if (compareAgainst != null)
            {
                var other = compareAgainst[prop];
                if (Equals(current, other)) continue;
            }

            dict[prop.Name] = current;
        }
        return dict.Count == 0 ? null : JsonSerializer.Serialize(dict);
    }

    private static string? GetDescription(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry)
    {
        // Try common "name" properties in priority order
        string[] candidates = ["JobNumber", "QuoteNumber", "InvoiceNumber", "BillNumber", "PaymentNumber",
            "ReceiptNumber", "CertificateNumber", "Email", "CompanyName", "Name", "EquipmentName", "ItemName"];
        foreach (var name in candidates)
        {
            var prop = entry.Properties.FirstOrDefault(p => p.Metadata.Name == name);
            if (prop?.CurrentValue is string s && !string.IsNullOrWhiteSpace(s))
                return s;
        }
        return null;
    }

    // ── Raw SQL insert (bypasses change tracker → no recursion) ─────────────────

    private static void InsertSync(DbContext context, AuditLog log)
    {
        context.Database.ExecuteSqlRaw(InsertSql, BuildParams(log));
    }

    private static async Task InsertAsync(DbContext context, AuditLog log, CancellationToken ct)
    {
        await context.Database.ExecuteSqlRawAsync(InsertSql, BuildParams(log), ct);
    }

    private static SqlParameter[] BuildParams(AuditLog log) =>
    [
        new SqlParameter("@userId",            SqlDbType.NVarChar, -1) { Value = (object?)log.UserId            ?? DBNull.Value },
        new SqlParameter("@userName",          SqlDbType.NVarChar, -1) { Value = (object)log.UserName },
        new SqlParameter("@companyId",         SqlDbType.Int)          { Value = (object?)log.CompanyId         ?? DBNull.Value },
        new SqlParameter("@companyName",       SqlDbType.NVarChar, -1) { Value = (object?)log.CompanyName       ?? DBNull.Value },
        new SqlParameter("@action",            SqlDbType.NVarChar, -1) { Value = (object)log.Action },
        new SqlParameter("@entityType",        SqlDbType.NVarChar, -1) { Value = (object)log.EntityType },
        new SqlParameter("@entityId",          SqlDbType.NVarChar, -1) { Value = (object?)log.EntityId          ?? DBNull.Value },
        new SqlParameter("@entityDescription", SqlDbType.NVarChar, -1) { Value = (object?)log.EntityDescription ?? DBNull.Value },
        new SqlParameter("@oldValues",         SqlDbType.NVarChar, -1) { Value = (object?)log.OldValues         ?? DBNull.Value },
        new SqlParameter("@newValues",         SqlDbType.NVarChar, -1) { Value = (object?)log.NewValues         ?? DBNull.Value },
        new SqlParameter("@ipAddress",         SqlDbType.NVarChar, -1) { Value = (object?)log.IpAddress         ?? DBNull.Value },
        new SqlParameter("@timestamp",         SqlDbType.DateTime2)    { Value = (object)log.Timestamp },
    ];

    private const string InsertSql = """
        INSERT INTO AuditLogs
            (UserId, UserName, CompanyId, CompanyName, Action, EntityType, EntityId,
             EntityDescription, OldValues, NewValues, IpAddress, Timestamp)
        VALUES (@userId, @userName, @companyId, @companyName, @action, @entityType, @entityId,
                @entityDescription, @oldValues, @newValues, @ipAddress, @timestamp)
        """;

    // ── Request context helpers ───────────────────────────────────────────────────

    private (string? userId, string userName, int? companyId, string? ip) GetRequestContext()
    {
        var ctx = _httpContextAccessor.HttpContext;
        if (ctx == null) return (null, "System", null, null);

        var user = ctx.User;
        var userId = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
        var userName = user.Identity?.Name ?? user.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "Unknown";
        var companyIdStr = user.FindFirst("CompanyId")?.Value;
        int? companyId = companyIdStr != null && int.TryParse(companyIdStr, out var cid) ? cid : null;
        var ip = ctx.Connection.RemoteIpAddress?.ToString();

        return (userId, userName, companyId, ip);
    }
}
