//
//  Contractor.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NosSmooth.Core.Contracts.Responders;
using Remora.Results;
namespace NosSmooth.Core.Contracts;
/// <summary>
/// A class holding <see cref="IContract"/>s,
/// updates the contracts.
/// </summary>
public class Contractor : IEnumerable<IContract>
{
    /// <summary>
    /// Maximum time a contract may be registered for.
    /// </summary>
    public static readonly TimeSpan Timeout = new TimeSpan(0, 5, 0);
    private readonly List<ContractInfo> _contracts;
    private readonly SemaphoreSlim _semaphore;
    /// <summary>
    /// Initializes a new instance of the <see cref="Contractor"/> class.
    /// </summary>
    public Contractor()
    {
        _semaphore = new SemaphoreSlim(1, 1);
        _contracts = new List<ContractInfo>();
    }
    /// <summary>
    /// Register the given contract to receive feedback for it.
    /// </summary>
    /// <param name="contract">The contract to register.</param>
    public void Register(IContract contract)
    {
        try
        {
            _semaphore.Wait();
            _contracts.Add(new ContractInfo(contract, DateTime.Now));
        }
        finally
        {
            _semaphore.Release();
        }
    }
    /// <summary>
    /// Unregister the given contract, no more info will be received.
    /// </summary>
    /// <param name="contract">The contract.</param>
    public void Unregister(IContract contract)
    {
        try
        {
            _semaphore.Wait();
            _contracts.RemoveAll(ci => ci.contract == contract);
        }
        finally
        {
            _semaphore.Release();
        }
    }
    /// <summary>
    /// Update all of the contracts with the given data.
    /// </summary>
    /// <remarks>
    /// Called from <see cref="ContractPacketResponder"/>
    /// or similar. Used for updating the state.
    /// The contracts look for actions that trigger updates
    /// and in case it matches the <paramref name="data"/>,
    /// the state is switched.
    /// </remarks>
    /// <param name="data">The data that were received.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <typeparam name="TData">The type of the data.</typeparam>
    /// <returns>The result that may or may not have succeeded.</returns>
    public async Task<Result> Update<TData>(TData data, CancellationToken ct = default)
        where TData : notnull
    {
        var errors = new List<IResult>();
        var toRemove = new List<ContractInfo>();
        try
        {
            await _semaphore.WaitAsync(ct);
            foreach (var info in _contracts)
            {
                if (DateTime.Now - info.addedAt > Timeout)
                {
                    errors.Add
                    (
                        (Result)new GenericError
                        (
                            $"A contract {info.contract} has been registered for too long and was unregistered automatically."
                        )
                    );
                    continue;
                }
                var result = await info.contract.Update(data);
                if (!result.IsDefined(out var response))
                {
                    errors.Add(result);
                }
                if (response == ContractUpdateResponse.InterestedAndUnregister)
                {
                    toRemove.Add(info);
                }
            }
            foreach (var contract in toRemove)
            {
                _contracts.Remove(contract);
            }
            return errors.Count switch
            {
                0 => Result.FromSuccess(),
                1 => (Result)errors[0],
                _ => new AggregateError(errors)
            };
        }
        finally
        {
            _semaphore.Release();
        }
    }
    /// <inheritdoc />
    public IEnumerator<IContract> GetEnumerator()
        => _contracts.Select(x => x.contract).GetEnumerator();
    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
        => GetEnumerator();
    private record struct ContractInfo(IContract contract, DateTime addedAt);
}