using System;
using System.Collections.Generic;
using System.Linq;

namespace FishDice.LuckyDice
{
    public sealed class PoolFunnelRandomService
    {
        private const int MaxLayerVisits = 32;

        private readonly IRandomSource _random;
        private readonly IRandomPoolStateStore _stateStore;
        private readonly ILayerAvailabilityPolicy _availabilityPolicy;
        private readonly IResultWeightCalculator _weightCalculator;
        private readonly IPoolFunnelActionDispatcher _actionDispatcher;
        private readonly IPoolSettlementStrategy _settlementStrategy;
        private readonly IFunnelDecisionStrategy _decisionStrategy;

        public PoolFunnelRandomService(
            IRandomSource random,
            IRandomPoolStateStore stateStore,
            ILayerAvailabilityPolicy availabilityPolicy,
            IResultWeightCalculator weightCalculator,
            IPoolFunnelActionDispatcher actionDispatcher,
            IPoolSettlementStrategy settlementStrategy,
            IFunnelDecisionStrategy decisionStrategy)
        {
            _random = random ?? throw new ArgumentNullException(nameof(random));
            _stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
            _availabilityPolicy = availabilityPolicy ?? new AlwaysAvailableLayerPolicy();
            _weightCalculator = weightCalculator ?? new LinearLuckValueWeightCalculator();
            _actionDispatcher = actionDispatcher ?? new NoOpPoolFunnelActionDispatcher();
            _settlementStrategy = settlementStrategy ?? new DeltaPoolSettlementStrategy();
            _decisionStrategy = decisionStrategy ?? new ConfigFunnelDecisionStrategy();
        }

        public static PoolFunnelRandomService CreateDefault(IRandomSource random, IRandomPoolStateStore stateStore)
        {
            return new PoolFunnelRandomService(
                random,
                stateStore,
                new AlwaysAvailableLayerPolicy(),
                new LinearLuckValueWeightCalculator(),
                new NoOpPoolFunnelActionDispatcher(),
                new DeltaPoolSettlementStrategy(),
                new ConfigFunnelDecisionStrategy());
        }

        public PoolFunnelRunResult Execute(RandomPoolDefinition pool, PoolFunnelRequest request)
        {
            if (pool == null)
            {
                throw new ArgumentNullException(nameof(pool));
            }

            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            var context = new PoolFunnelContext(pool, request);
            var unavailableEntryLayerIds = new List<string>();
            var entryLayer = ResolveEntryLayer(pool, context, unavailableEntryLayerIds);
            var layerResults = new List<PoolFunnelLayerResult>();

            if (entryLayer == null)
            {
                return CreateResult(request, pool, null, unavailableEntryLayerIds, layerResults, "NoAvailableEntryLayer");
            }

            var currentLayer = entryLayer;
            for (var visit = 0; visit < MaxLayerVisits && currentLayer != null; visit++)
            {
                var layerResult = RollLayer(pool, currentLayer, context);
                layerResults.Add(layerResult);

                if (layerResult.Decision.DecisionType != PoolFunnelDecisionType.NextLayer)
                {
                    return CreateResult(request, pool, entryLayer.LayerId, unavailableEntryLayerIds, layerResults, layerResult.Outcome);
                }

                RandomLayerDefinition nextLayer;
                if (!pool.TryGetLayer(layerResult.Decision.NextLayerId, out nextLayer) ||
                    !_availabilityPolicy.IsLayerAvailable(nextLayer, context))
                {
                    return CreateResult(request, pool, entryLayer.LayerId, unavailableEntryLayerIds, layerResults, "NextLayerUnavailable");
                }

                currentLayer = nextLayer;
            }

            return CreateResult(request, pool, entryLayer.LayerId, unavailableEntryLayerIds, layerResults, "MaxLayerVisitsReached");
        }

        private RandomLayerDefinition ResolveEntryLayer(
            RandomPoolDefinition pool,
            PoolFunnelContext context,
            List<string> unavailableEntryLayerIds)
        {
            var available = new List<RandomLayerDefinition>();
            foreach (var entryLayerId in pool.EntryLayerIds)
            {
                var layer = pool.GetLayer(entryLayerId);
                if (_availabilityPolicy.IsLayerAvailable(layer, context))
                {
                    available.Add(layer);
                    continue;
                }

                unavailableEntryLayerIds.Add(layer.LayerId);
            }

            return available
                .OrderByDescending(layer => layer.Priority)
                .ThenBy(layer => layer.LayerId, StringComparer.Ordinal)
                .FirstOrDefault();
        }

        private PoolFunnelLayerResult RollLayer(
            RandomPoolDefinition pool,
            RandomLayerDefinition layer,
            PoolFunnelContext context)
        {
            var stateBefore = _stateStore.Load(pool.PoolId, layer.LayerId);
            var allWeights = layer.Results
                .Select(result => new WeightedPoolFunnelResult(
                    result.ResultKey,
                    result.Priority,
                    _weightCalculator.CalculateWeight(result, stateBefore, context)))
                .ToArray();
            var weightedResults = layer.Results
                .Zip(allWeights, (result, weight) => new WeightedCandidate(result, weight.Weight))
                .Where(item => item.Weight > 0)
                .ToArray();

            if (weightedResults.Length == 0)
            {
                return new PoolFunnelLayerResult(
                    layer.LayerId,
                    stateBefore,
                    stateBefore,
                    allWeights,
                    null,
                    null,
                    null,
                    PoolFunnelDecision.Stop(),
                    "NoWeightedResult");
            }

            var topPriority = weightedResults.Max(item => item.Result.Priority);
            var candidates = weightedResults
                .Where(item => item.Result.Priority == topPriority)
                .ToArray();
            var totalWeight = candidates.Sum(item => item.Weight);
            var randomValue = _random.Next(0, totalWeight);
            var selected = PickWeighted(candidates, randomValue);
            var actionResults = _actionDispatcher.Dispatch(layer, selected.Result, context);
            var stateAfter = _settlementStrategy.Settle(stateBefore, selected.Result, context);
            _stateStore.Save(stateAfter);

            var decision = _decisionStrategy.Decide(layer, selected.Result, stateAfter, context);
            var outcome = decision.DecisionType == PoolFunnelDecisionType.NextLayer ? "NextLayer" : "Stopped";

            return new PoolFunnelLayerResult(
                layer.LayerId,
                stateBefore,
                stateAfter,
                allWeights,
                randomValue,
                selected.Result,
                actionResults,
                decision,
                outcome);
        }

        private static WeightedCandidate PickWeighted(IReadOnlyList<WeightedCandidate> candidates, int randomValue)
        {
            var cursor = 0;
            foreach (var candidate in candidates)
            {
                cursor += candidate.Weight;
                if (randomValue < cursor)
                {
                    return candidate;
                }
            }

            return candidates[candidates.Count - 1];
        }

        private sealed class WeightedCandidate
        {
            public WeightedCandidate(RandomResultDefinition result, int weight)
            {
                Result = result;
                Weight = weight;
            }

            public RandomResultDefinition Result { get; }

            public int Weight { get; }
        }

        private static PoolFunnelRunResult CreateResult(
            PoolFunnelRequest request,
            RandomPoolDefinition pool,
            string selectedEntryLayerId,
            IReadOnlyList<string> unavailableEntryLayerIds,
            IReadOnlyList<PoolFunnelLayerResult> layerResults,
            string outcome)
        {
            var trace = new PoolFunnelTrace(
                request.SessionId,
                pool.PoolId,
                selectedEntryLayerId,
                unavailableEntryLayerIds,
                layerResults);

            return new PoolFunnelRunResult(
                request.SessionId,
                pool.PoolId,
                layerResults,
                outcome,
                trace);
        }
    }
}
