using System;
using System.Collections.Generic;


namespace AIRuntime.Runtime.Providers.Fallback
{
    /// <summary>
    /// Stores the active primary provider and an ordered
    /// collection of fallback providers.
    ///
    /// Providers are returned in execution priority:
    /// primary first, followed by fallbacks in registration order.
    /// </summary>
    public sealed class ProviderFallbackChain
    {
        private readonly List<IProvider> _fallbackProviders =
            new List<IProvider>();


        private IProvider _primaryProvider;


        /// <summary>
        /// Returns the configured primary provider.
        /// </summary>
        public IProvider PrimaryProvider
        {
            get
            {
                return _primaryProvider;
            }
        }


        /// <summary>
        /// Returns true when a primary provider is configured.
        /// </summary>
        public bool HasPrimaryProvider
        {
            get
            {
                return _primaryProvider != null;
            }
        }


        /// <summary>
        /// Number of configured fallback providers.
        /// </summary>
        public int FallbackCount
        {
            get
            {
                return _fallbackProviders.Count;
            }
        }


        /// <summary>
        /// Sets or replaces the primary provider.
        ///
        /// Existing fallback providers are preserved.
        /// </summary>
        public void SetPrimaryProvider(
            IProvider provider)
        {
            if (provider == null)
            {
                throw new ArgumentNullException(
                    nameof(provider));
            }


            EnsureProviderIsUnique(
                provider,
                ignorePrimary: true);


            _primaryProvider =
                provider;
        }


        /// <summary>
        /// Adds a fallback provider at the lowest priority.
        /// </summary>
        public void AddFallbackProvider(
            IProvider provider)
        {
            if (provider == null)
            {
                throw new ArgumentNullException(
                    nameof(provider));
            }


            EnsureProviderIsUnique(
                provider,
                ignorePrimary: false);


            _fallbackProviders.Add(
                provider);
        }


        /// <summary>
        /// Inserts a fallback provider at a specific priority.
        ///
        /// Index zero is the highest fallback priority.
        /// </summary>
        public void InsertFallbackProvider(
            int index,
            IProvider provider)
        {
            if (provider == null)
            {
                throw new ArgumentNullException(
                    nameof(provider));
            }


            if (
                index < 0 ||
                index > _fallbackProviders.Count)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(index));
            }


            EnsureProviderIsUnique(
                provider,
                ignorePrimary: false);


            _fallbackProviders.Insert(
                index,
                provider);
        }


        /// <summary>
        /// Removes a fallback provider.
        /// </summary>
        public bool RemoveFallbackProvider(
            IProvider provider)
        {
            if (provider == null)
                return false;


            return _fallbackProviders.Remove(
                provider);
        }


        /// <summary>
        /// Removes a fallback provider by priority index.
        /// </summary>
        public void RemoveFallbackProviderAt(
            int index)
        {
            if (
                index < 0 ||
                index >= _fallbackProviders.Count)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(index));
            }


            _fallbackProviders.RemoveAt(
                index);
        }


        /// <summary>
        /// Removes all fallback providers.
        ///
        /// The primary provider is preserved.
        /// </summary>
        public void ClearFallbackProviders()
        {
            _fallbackProviders.Clear();
        }


        /// <summary>
        /// Returns a snapshot of the fallback providers
        /// in priority order.
        /// </summary>
        public IReadOnlyList<IProvider> GetFallbackProviders()
        {
            return _fallbackProviders.ToArray();
        }


        /// <summary>
        /// Returns a snapshot containing the primary provider
        /// followed by all fallback providers in priority order.
        /// </summary>
        public IReadOnlyList<IProvider> GetProvidersInPriorityOrder()
        {
            if (_primaryProvider == null)
            {
                return Array.Empty<IProvider>();
            }


            List<IProvider> providers =
                new List<IProvider>(
                    1 +
                    _fallbackProviders.Count);


            providers.Add(
                _primaryProvider);


            providers.AddRange(
                _fallbackProviders);


            return providers;
        }


        /// <summary>
        /// Returns the provider at the specified execution priority.
        ///
        /// Index zero is the primary provider.
        /// Index one is the first fallback.
        /// </summary>
        public IProvider GetProviderAt(
            int index)
        {
            if (_primaryProvider == null)
            {
                throw new InvalidOperationException(
                    "No primary provider is configured.");
            }


            if (index == 0)
            {
                return _primaryProvider;
            }


            int fallbackIndex =
                index -
                1;


            if (
                fallbackIndex < 0 ||
                fallbackIndex >= _fallbackProviders.Count)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(index));
            }


            return _fallbackProviders[
                fallbackIndex];
        }


        private void EnsureProviderIsUnique(
            IProvider provider,
            bool ignorePrimary)
        {
            if (
                !ignorePrimary &&
                ReferenceEquals(
                    _primaryProvider,
                    provider))
            {
                throw new InvalidOperationException(
                    $"Provider '{provider.Name}' is already configured as the primary provider.");
            }


            foreach (
                IProvider existingProvider
                in _fallbackProviders)
            {
                if (
                    ReferenceEquals(
                        existingProvider,
                        provider))
                {
                    throw new InvalidOperationException(
                        $"Provider '{provider.Name}' is already registered as a fallback provider.");
                }
            }
        }
    }
}