using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.LogStack.Flows;

// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.

/// <summary>
/// Fans out every log event to multiple inner flows in parallel, each with an
/// optional per-flow predicate guard.
///
/// Differences vs simply adding multiple flows to a <see cref="EonaCat.LogStack.EonaCatLogStack"/>:
///   • The fan-out is fired in parallel (Task.WhenAll), reducing wall-clock latency.
///   • Per-target predicates let you express "send to Slack only if level ≥ Error"
///     inside one place rather than wrapping each flow in a <see cref="ConditionalFlow"/>.
///   • Targets can be added/removed at runtime via <see cref="Add"/> / <see cref="Remove"/>.
///
/// <example>
/// <code>
/// var multicast = new MulticastFlow()
///     .Add(consoleFlow)
///     .Add(fileFlow)
///     .Add(slackFlow, e => e.Level >= LogLevel.Error);
///
/// new LogBuilder().WriteTo(multicast).Build();
/// </code>
/// </example>
/// </summary>
public sealed class MulticastFlow : FlowBase
{
    private readonly struct Target
    {
        public readonly IFlow Flow;
        public readonly Func<LogEvent, bool>? Predicate;
        public Target(IFlow flow, Func<LogEvent, bool>? predicate) { Flow = flow; Predicate = predicate; }
    }

    private readonly List<Target> _targets = new();
    private readonly object _lock = new();

    public MulticastFlow(LogLevel minimumLevel = LogLevel.Trace)
        : base("Multicast", minimumLevel) { }

    /// <summary>
    /// Adds an inner flow, optionally gated by a predicate.
    /// </summary>
    public MulticastFlow Add(IFlow flow, Func<LogEvent, bool>? predicate = null)
    {
        if (flow == null)
        {
            throw new ArgumentNullException(nameof(flow));
        }

        lock (_lock)
        {
            _targets.Add(new Target(flow, predicate));
        }

        return this;
    }

    /// <summary>
    /// Removes all targets backed by the given flow instance.
    /// </summary>
    public MulticastFlow Remove(IFlow flow)
    {
        if (flow == null)
        {
            throw new ArgumentNullException(nameof(flow));
        }

        lock (_lock)
        {
            _targets.RemoveAll(t => t.Flow == flow);
        }

        return this;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
    {
        if (!IsEnabled || !IsLogLevelEnabled(logEvent))
        {
            return WriteResult.LevelFiltered;
        }

        Target[] snapshot;
        lock (_lock)
        {
            snapshot = _targets.ToArray();
        }

        if (snapshot.Length == 0)
        {
            return WriteResult.NoBlastZone;
        }

        var tasks = new Task<WriteResult>[snapshot.Length];
        for (int i = 0; i < snapshot.Length; i++)
        {
            var target = snapshot[i];
            bool shouldSend;
            try { shouldSend = target.Predicate == null || target.Predicate(logEvent); }
            catch { shouldSend = false; }

            tasks[i] = shouldSend
                ? target.Flow.BlastAsync(logEvent, cancellationToken)
                : Task.FromResult(WriteResult.Dropped);
        }

        var results = await Task.WhenAll(tasks).ConfigureAwait(false);

        var overall = WriteResult.Success;
        foreach (var r in results)
        {
            if (r == WriteResult.Dropped)
            {
                Interlocked.Increment(ref DroppedCount);
            }
            else
            {
                Interlocked.Increment(ref BlastedCount);
            }

            if (r != WriteResult.Success && r != WriteResult.Dropped && overall == WriteResult.Success)
            {
                overall = r;
            }
        }

        return overall;
    }

    public override async Task FlushAsync(CancellationToken cancellationToken = default)
    {
        Target[] snapshot;
        lock (_lock)
        {
            snapshot = _targets.ToArray();
        }

        var tasks = new Task[snapshot.Length];
        for (int i = 0; i < snapshot.Length; i++)
        {
            tasks[i] = snapshot[i].Flow.FlushAsync(cancellationToken);
        }

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }

    public override async ValueTask DisposeAsync()
    {
        IsEnabled = false;
        await FlushAsync(default).ConfigureAwait(false);

        Target[] snapshot;
        lock (_lock)
        {
            snapshot = _targets.ToArray();
        }

        foreach (var t in snapshot)
        {
            try { await t.Flow.DisposeAsync().ConfigureAwait(false); } catch { }
        }

        GC.SuppressFinalize(this);
    }
}
