using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using MiGu.DB.Abstractions.Entities;
using MiGu.DB.Abstractions.Exceptions;
using MiGu.DB.Abstractions.Persistence;
using MiGu.DB.Abstractions.Runtime;
using MiGu.DB.Kernel.Context;

namespace MiGu.DB.Kernel.Repositories;

public class Repository<TEntity, TKey> : IRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>
{
    protected readonly MiGuDbContext Db;
    protected DbSet<TEntity> Set => Db.Set<TEntity>();

    public Repository(MiGuDbContext db) => Db = db;

    public virtual IQueryable<TEntity> Query(bool asNoTracking = true)
        => asNoTracking ? Set.AsNoTracking() : Set.AsQueryable();

    public virtual Task<TEntity?> FindAsync(TKey id, CancellationToken ct = default)
        => Set.FindAsync([id], ct).AsTask();

    public virtual Task AddAsync(TEntity entity, CancellationToken ct = default)
        => Set.AddAsync(entity, ct).AsTask();

    public virtual void Update(TEntity entity) => Set.Update(entity);
}

public sealed class EditableRepository<TEntity> : Repository<TEntity, Guid>, IEditableRepository<TEntity>
    where TEntity : class, IEntity<Guid>, ISoftDeletable, IVersioned, ILockable
{
    private readonly IActorContextAccessor _actors;

    public EditableRepository(MiGuDbContext db, IActorContextAccessor actors) : base(db)
        => _actors = actors;

    public async Task<TEntity> GetEditableAsync(Guid id, long? expectedVersion, CancellationToken ct = default)
    {
        var entity = await Set.FirstOrDefaultAsync(x => x.Id.Equals(id), ct)
            ?? throw new EntityNotFoundException(typeof(TEntity).Name, id);
        if (entity.IsLock)
            throw new EntityLockedException(typeof(TEntity).Name, id);
        if (expectedVersion.HasValue && entity.Version != expectedVersion.Value)
            throw new ConcurrencyConflictException(typeof(TEntity).Name, id, expectedVersion);
        return entity;
    }

    public async Task SoftDeleteAsync(Guid id, long? expectedVersion, CancellationToken ct = default)
    {
        var entity = await GetEditableAsync(id, expectedVersion, ct);
        entity.IsDeleted = true;
        entity.DeletedAt = DateTimeOffset.UtcNow;
        entity.DeletedBy = _actors.Current.Name;
        if (entity is IAuditable auditable)
            auditable.UpdatedBy = _actors.Current.Name;
    }

    public async Task EnsureUniqueAsync(Expression<Func<TEntity, bool>> predicate, string errorMessage, CancellationToken ct = default)
    {
        if (await Set.AnyAsync(predicate, ct))
            throw new InvalidOperationException(errorMessage);
    }
}

public sealed class HistoryRepository<TEntity> : Repository<TEntity, Guid>, IHistoryRepository<TEntity>
    where TEntity : class, IEntity<Guid>, IHistoryEntry
{
    public HistoryRepository(MiGuDbContext db) : base(db) { }

    public Task AppendAsync(TEntity entry, CancellationToken ct = default)
        => AddAsync(entry, ct);
}

public sealed class UnitOfWork : IUnitOfWork
{
    private readonly MiGuDbContext _db;

    public UnitOfWork(MiGuDbContext db) => _db = db;

    public async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        try
        {
            return await _db.SaveChangesAsync(ct);
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.FirstOrDefault();
            throw new ConcurrencyConflictException(
                entry?.Entity.GetType().Name ?? "Unknown",
                entry?.Property("Id")?.CurrentValue);
        }
    }

    public Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
    {
        var strategy = _db.Database.CreateExecutionStrategy();
        return strategy.ExecuteAsync(async () =>
        {
            await using var tx = await _db.Database.BeginTransactionAsync(ct);
            try
            {
                await action(ct);
                await _db.SaveChangesAsync(ct);
                await tx.CommitAsync(ct);
            }
            catch
            {
                await tx.RollbackAsync(ct);
                throw;
            }
        });
    }
}
