using System.Diagnostics;
using System.Reflection;
using EventFlow.Aggregates;
using EventFlow.Core;
using EventFlow.Core.Caching;
using EventFlow.EventStores;
using EventFlow.Extensions;
using EventFlow.ReadStores;
using Microsoft.Extensions.Caching.Memory;

namespace Books.Api.EventFlow.Infrastructure;

/// <summary>
/// Repopulates read models for a single aggregate by replaying events from the event store.
/// Adapted from nontrade's implementation.
/// </summary>
public class SingleAggregateReadModelPopulator(
    IEventStore eventStore,
    IMemoryCache memoryCache,
    ILogger<SingleAggregateReadModelPopulator> logger,
    IEnumerable<IReadStoreManager> allReadStoreManagers)
    : ISingleAggregateReadModelPopulator
{
    public async Task PopulateAsync<TAggregate, TIdentity>(TIdentity id, Type readModelType)
        where TAggregate : IAggregateRoot<TIdentity>
        where TIdentity : IIdentity
    {
        var events = await eventStore.LoadEventsAsync<TAggregate, TIdentity>(id, CancellationToken.None);
        await ProcessEvents(readModelType, events, CancellationToken.None);
    }

    public async Task PopulateAsyncNonGeneric(Type aggregateType, Type identityType, string id, Type readModelType)
    {
        if (!typeof(IAggregateRoot<>).MakeGenericType(identityType).IsAssignableFrom(aggregateType))
        {
            throw new ArgumentException(
                "Aggregate type must implement IAggregateRoot with the specified identity type.",
                nameof(aggregateType));
        }

        if (!typeof(IIdentity).IsAssignableFrom(identityType))
        {
            throw new ArgumentException("Identity type must implement IIdentity.", nameof(identityType));
        }

        var methodInfo = typeof(SingleAggregateReadModelPopulator)
            .GetMethod(nameof(PopulateAsync), BindingFlags.Public | BindingFlags.Instance);

        if (methodInfo == null)
        {
            throw new InvalidOperationException(
                $"Method PopulateAsync not found for aggregate type {aggregateType} and identity type {identityType}");
        }

        var genericMethodInfo = methodInfo.MakeGenericMethod(aggregateType, identityType);
        var identity = CreateIdentityInstance(identityType, id);

        await (Task)genericMethodInfo.Invoke(this, [identity, readModelType])!;
    }

    public async Task PopulateAllCompatibleReadModelsAsync<TAggregate, TIdentity>(TIdentity id)
        where TAggregate : IAggregateRoot<TIdentity>
        where TIdentity : IIdentity
    {
        try
        {
            var compatibleReadStoreManagers = allReadStoreManagers
                .Where(x => x.GetType().GetGenericArguments().FirstOrDefault() == typeof(TAggregate))
                .ToList();

            var events = await eventStore.LoadEventsAsync<TAggregate, TIdentity>(id, CancellationToken.None);

            foreach (var readStore in compatibleReadStoreManagers)
            {
                await ProcessEvents(readStore.ReadModelType, events, CancellationToken.None);
            }
        }
        catch (Exception e)
        {
            logger.LogError(e, "Exception when populating all compatible read models for {Id}", id);
            throw;
        }
    }

    private async Task ProcessEvents(
        Type readModelType,
        IReadOnlyCollection<IDomainEvent> processEvents,
        CancellationToken cancellationToken)
    {
        try
        {
            var stopwatch = Stopwatch.StartNew();
            var readStoreManagers = ResolveReadStoreManagers(readModelType);
            long relevantEvents = 0;

            var readModelTypes = new[]
            {
                typeof(IAmReadModelFor<,,>)
            };

            var aggregateEventTypes = memoryCache.GetOrCreate(
                CacheKey.With(GetType(), readModelType.ToString(), nameof(ProcessEvents)),
                _ => new HashSet<Type>(readModelType.GetTypeInfo()
                    .GetInterfaces()
                    .Where(i => i.GetTypeInfo().IsGenericType && readModelTypes.Contains(i.GetGenericTypeDefinition()))
                    .Select(i => i.GetTypeInfo().GetGenericArguments()[2])));

            var domainEvents = processEvents
                .Where(e => aggregateEventTypes!.Contains(e.EventType))
                .ToList();
            relevantEvents += domainEvents.Count;

            if (domainEvents.Count == 0)
            {
                logger.LogWarning(
                    "[REPOPULATION] Will not populate {ReadModelType} because no relevant events were found",
                    readModelType.PrettyPrint());
                return;
            }

            var applyTasks = readStoreManagers!
                .Select(m => m.UpdateReadStoresAsync(domainEvents, cancellationToken));
            await Task.WhenAll(applyTasks).ConfigureAwait(false);

            logger.LogInformation(
                "[REPOPULATION] Population of read model {ReadModelType} took {Seconds:F2} seconds, replayed {RelevantEventCount} events",
                readModelType.FullName,
                stopwatch.Elapsed.TotalSeconds,
                relevantEvents);
        }
        catch (Exception e)
        {
            var aggregateId = processEvents.FirstOrDefault()?.GetIdentity()?.Value;

            logger.LogError(e,
                "[REPOPULATION] Exception when populating: {ReadModelType} for {AggregateId}. " +
                "This will cause data integrity issues if not resolved.",
                readModelType.FullName, aggregateId);
            throw;
        }
    }

    private IReadOnlyCollection<IReadStoreManager>? ResolveReadStoreManagers(Type readModelType)
    {
        return memoryCache.GetOrCreate(
            CacheKey.With(GetType(), readModelType.ToString(), nameof(ResolveReadStoreManagers)),
            _ =>
            {
                var readStoreManagers = allReadStoreManagers
                    .Where(m => m.ReadModelType == readModelType)
                    .ToList();

                if (readStoreManagers.Count == 0)
                {
                    throw new ArgumentException(
                        $"Did not find any read store managers for read model type '{readModelType.PrettyPrint()}'");
                }

                return readStoreManagers;
            });
    }

    private static object CreateIdentityInstance(Type identityType, string idString)
    {
        // EventFlow identity types have a static With(string) method on the Identity<T> base class
        // The method signature is: public static T With(string value)
        var withMethod = identityType.GetMethod(
            "With",
            BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy,
            null,
            [typeof(string)],
            null);

        if (withMethod != null)
        {
            return withMethod.Invoke(null, [idString])!;
        }

        // Try the constructor directly (all EventFlow identities have a string constructor)
        var constructor = identityType.GetConstructor([typeof(string)]);
        if (constructor != null)
        {
            return constructor.Invoke([idString])!;
        }

        throw new InvalidOperationException(
            $"The type {identityType.Name} does not contain a public static With method or a string constructor.");
    }
}
