#nullable enable
using System.Text.Json;
using Baya.Application.Contracts.Audit;
using Baya.Application.Contracts.Common;
using Baya.Application.Models.Audit;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Audit;
using Microsoft.EntityFrameworkCore;

namespace Baya.Infrastructure.Persistence.Services.Audit;

/// <summary>
/// Explicit append-only audit writer + trail reader. The trail is immutable: there is deliberately no
/// update or delete path here. Row-level diffs on auditable entities are captured automatically by the
/// SaveChanges interceptor; this contract covers state changes that have no tracked-entity diff.
/// </summary>
internal sealed class AuditLogger(
    ApplicationDbContext db,
    ICurrentUser currentUser,
    IDateTimeProvider dateTimeProvider) : IAuditLogger
{
    public async ValueTask WriteAsync(
        string entityType,
        string entityId,
        string action,
        IReadOnlyDictionary<string, object?>? changedFields = null,
        CancellationToken cancellationToken = default)
    {
        db.Set<AuditLog>().Add(new AuditLog
        {
            EntityType = entityType,
            EntityId = entityId,
            Action = action,
            ChangedFieldsJson = changedFields is null ? null : JsonSerializer.Serialize(changedFields),
            ActorUserId = currentUser.UserId,
            OccurredAt = dateTimeProvider.UtcNow
        });

        await db.SaveChangesAsync(cancellationToken);
    }

    public async ValueTask<PagedResult<AuditLogDto>> GetTrailAsync(
        string? entityType,
        string? entityId,
        int? actorUserId,
        string? action,
        System.DateTimeOffset? from,
        System.DateTimeOffset? to,
        int page,
        int pageSize,
        CancellationToken cancellationToken = default)
    {
        var filtered = db.Set<AuditLog>().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(entityType))
            filtered = filtered.Where(a => a.EntityType == entityType);
        if (!string.IsNullOrWhiteSpace(entityId))
            filtered = filtered.Where(a => a.EntityId == entityId);
        if (actorUserId is { } actor)
            filtered = filtered.Where(a => a.ActorUserId == actor);
        if (!string.IsNullOrWhiteSpace(action))
            filtered = filtered.Where(a => a.Action == action);
        if (from is { } f)
            filtered = filtered.Where(a => a.OccurredAt >= f);
        if (to is { } t)
            filtered = filtered.Where(a => a.OccurredAt <= t);

        // Id is a monotonic identity → newest-first and deterministic (no timestamp ties).
        var query = filtered.OrderByDescending(a => a.Id);

        var total = await query.CountAsync(cancellationToken);
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(a => new AuditLogDto(a.Id, a.EntityType, a.EntityId, a.Action, a.ChangedFieldsJson, a.ActorUserId, a.OccurredAt))
            .ToListAsync(cancellationToken);

        return new PagedResult<AuditLogDto>(items, total, page, pageSize);
    }

    public async ValueTask<int> PurgeExpiredAsync(
        int generalRetentionDays,
        int financialRetentionDays,
        IReadOnlyCollection<string> financialEntityTypes,
        int maxRowsPerRun,
        CancellationToken cancellationToken = default)
    {
        var now = dateTimeProvider.UtcNow;
        var generalCutoff = now.AddDays(-generalRetentionDays);
        var financialCutoff = now.AddDays(-financialRetentionDays);

        // Oldest-first (Id is monotonic with OccurredAt), capped. The age comparison is done in memory — the
        // SQLite test provider can't translate a DateTimeOffset predicate — and the delete is a single id-keyed
        // statement that translates on every provider. A financial/compliance row survives until the longer window.
        var candidates = await db.Set<AuditLog>().AsNoTracking()
            .OrderBy(a => a.Id)
            .Select(a => new { a.Id, a.EntityType, a.OccurredAt })
            .Take(maxRowsPerRun)
            .ToListAsync(cancellationToken);

        var expiredIds = candidates
            .Where(a => a.OccurredAt < (financialEntityTypes.Contains(a.EntityType) ? financialCutoff : generalCutoff))
            .Select(a => a.Id)
            .ToList();

        if (expiredIds.Count == 0)
            return 0;

        return await db.Set<AuditLog>()
            .Where(a => expiredIds.Contains(a.Id))
            .ExecuteDeleteAsync(cancellationToken);
    }
}
