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

namespace FishDice.LuckyDice
{
    public enum PoolFunnelDecisionType
    {
        Stop,
        NextLayer
    }

    public sealed class PoolFunnelDecision
    {
        private PoolFunnelDecision(PoolFunnelDecisionType decisionType, string nextLayerId)
        {
            DecisionType = decisionType;
            NextLayerId = nextLayerId;
        }

        public PoolFunnelDecisionType DecisionType { get; }

        public string NextLayerId { get; }

        public static PoolFunnelDecision Stop()
        {
            return new PoolFunnelDecision(PoolFunnelDecisionType.Stop, null);
        }

        public static PoolFunnelDecision NextLayer(string layerId)
        {
            if (string.IsNullOrWhiteSpace(layerId))
            {
                throw new ArgumentException("Next layer id is required.", nameof(layerId));
            }

            return new PoolFunnelDecision(PoolFunnelDecisionType.NextLayer, layerId);
        }
    }

    public sealed class PoolFunnelRequest
    {
        public PoolFunnelRequest(string sessionId, string triggerSource)
        {
            if (string.IsNullOrWhiteSpace(sessionId))
            {
                throw new ArgumentException("Session id is required.", nameof(sessionId));
            }

            SessionId = sessionId;
            TriggerSource = triggerSource;
        }

        public string SessionId { get; }

        public string TriggerSource { get; }
    }

    public sealed class PoolFunnelContext
    {
        public PoolFunnelContext(RandomPoolDefinition pool, PoolFunnelRequest request)
        {
            Pool = pool ?? throw new ArgumentNullException(nameof(pool));
            Request = request ?? throw new ArgumentNullException(nameof(request));
        }

        public RandomPoolDefinition Pool { get; }

        public PoolFunnelRequest Request { get; }
    }

    public sealed class RandomPoolDefinition
    {
        private readonly IReadOnlyDictionary<string, RandomLayerDefinition> _layersById;

        public RandomPoolDefinition(
            string poolId,
            IReadOnlyList<string> entryLayerIds,
            IReadOnlyList<RandomLayerDefinition> layers)
        {
            if (string.IsNullOrWhiteSpace(poolId))
            {
                throw new ArgumentException("Pool id is required.", nameof(poolId));
            }

            if (entryLayerIds == null || entryLayerIds.Count == 0)
            {
                throw new ArgumentException("At least one entry layer is required.", nameof(entryLayerIds));
            }

            if (layers == null || layers.Count == 0)
            {
                throw new ArgumentException("At least one layer is required.", nameof(layers));
            }

            PoolId = poolId;
            EntryLayerIds = entryLayerIds.ToArray();
            Layers = layers.ToArray();
            _layersById = Layers.ToDictionary(layer => layer.LayerId, layer => layer);
        }

        public string PoolId { get; }

        public IReadOnlyList<string> EntryLayerIds { get; }

        public IReadOnlyList<RandomLayerDefinition> Layers { get; }

        public bool TryGetLayer(string layerId, out RandomLayerDefinition layer)
        {
            return _layersById.TryGetValue(layerId, out layer);
        }

        public RandomLayerDefinition GetLayer(string layerId)
        {
            RandomLayerDefinition layer;
            if (!TryGetLayer(layerId, out layer))
            {
                throw new InvalidOperationException("Missing random layer: " + layerId);
            }

            return layer;
        }
    }

    public sealed class RandomLayerDefinition
    {
        public RandomLayerDefinition(
            string layerId,
            int priority,
            string availabilityKey,
            IReadOnlyList<RandomResultDefinition> results)
        {
            if (string.IsNullOrWhiteSpace(layerId))
            {
                throw new ArgumentException("Layer id is required.", nameof(layerId));
            }

            if (results == null || results.Count == 0)
            {
                throw new ArgumentException("At least one result is required.", nameof(results));
            }

            LayerId = layerId;
            Priority = priority;
            AvailabilityKey = availabilityKey;
            Results = results.ToArray();
        }

        public string LayerId { get; }

        public int Priority { get; }

        public string AvailabilityKey { get; }

        public IReadOnlyList<RandomResultDefinition> Results { get; }
    }

    public sealed class RandomResultDefinition
    {
        public RandomResultDefinition(
            string resultKey,
            int priority,
            int baseWeight,
            int luckValueWeightFactor,
            int luckValueDelta,
            PoolFunnelDecision decision,
            IReadOnlyList<RollActionSpec> actions = null,
            string settlementKey = null)
        {
            if (string.IsNullOrWhiteSpace(resultKey))
            {
                throw new ArgumentException("Result key is required.", nameof(resultKey));
            }

            ResultKey = resultKey;
            Priority = priority;
            BaseWeight = baseWeight;
            LuckValueWeightFactor = luckValueWeightFactor;
            LuckValueDelta = luckValueDelta;
            Decision = decision ?? PoolFunnelDecision.Stop();
            Actions = actions == null ? Array.Empty<RollActionSpec>() : actions.ToArray();
            SettlementKey = settlementKey;
        }

        public string ResultKey { get; }

        public int Priority { get; }

        public int BaseWeight { get; }

        public int LuckValueWeightFactor { get; }

        public int LuckValueDelta { get; }

        public PoolFunnelDecision Decision { get; }

        public IReadOnlyList<RollActionSpec> Actions { get; }

        public string SettlementKey { get; }
    }

    public sealed class RandomPoolState
    {
        public RandomPoolState(string poolId, string layerId, int luckValue, int rollCount)
        {
            if (string.IsNullOrWhiteSpace(poolId))
            {
                throw new ArgumentException("Pool id is required.", nameof(poolId));
            }

            if (string.IsNullOrWhiteSpace(layerId))
            {
                throw new ArgumentException("Layer id is required.", nameof(layerId));
            }

            PoolId = poolId;
            LayerId = layerId;
            LuckValue = Math.Max(0, luckValue);
            RollCount = Math.Max(0, rollCount);
        }

        public string PoolId { get; }

        public string LayerId { get; }

        public int LuckValue { get; }

        public int RollCount { get; }

        public RandomPoolState With(int luckValue, int rollCount)
        {
            return new RandomPoolState(PoolId, LayerId, luckValue, rollCount);
        }
    }

    public sealed class WeightedPoolFunnelResult
    {
        public WeightedPoolFunnelResult(string resultKey, int priority, int weight)
        {
            ResultKey = resultKey;
            Priority = priority;
            Weight = weight;
        }

        public string ResultKey { get; }

        public int Priority { get; }

        public int Weight { get; }
    }

    public sealed class PoolFunnelLayerResult
    {
        public PoolFunnelLayerResult(
            string layerId,
            RandomPoolState stateBefore,
            RandomPoolState stateAfter,
            IReadOnlyList<WeightedPoolFunnelResult> weights,
            int? randomValue,
            RandomResultDefinition selectedResult,
            IReadOnlyList<RollActionResult> actionResults,
            PoolFunnelDecision decision,
            string outcome)
        {
            LayerId = layerId;
            StateBefore = stateBefore;
            StateAfter = stateAfter;
            Weights = weights == null ? Array.Empty<WeightedPoolFunnelResult>() : weights.ToArray();
            RandomValue = randomValue;
            SelectedResult = selectedResult;
            ActionResults = actionResults == null ? Array.Empty<RollActionResult>() : actionResults.ToArray();
            Decision = decision ?? PoolFunnelDecision.Stop();
            Outcome = outcome;
        }

        public string LayerId { get; }

        public RandomPoolState StateBefore { get; }

        public RandomPoolState StateAfter { get; }

        public IReadOnlyList<WeightedPoolFunnelResult> Weights { get; }

        public int? RandomValue { get; }

        public RandomResultDefinition SelectedResult { get; }

        public IReadOnlyList<RollActionResult> ActionResults { get; }

        public PoolFunnelDecision Decision { get; }

        public string Outcome { get; }
    }

    public sealed class PoolFunnelTrace
    {
        public PoolFunnelTrace(
            string sessionId,
            string poolId,
            string selectedEntryLayerId,
            IReadOnlyList<string> unavailableEntryLayerIds,
            IReadOnlyList<PoolFunnelLayerResult> layers)
        {
            SessionId = sessionId;
            PoolId = poolId;
            SelectedEntryLayerId = selectedEntryLayerId;
            UnavailableEntryLayerIds = unavailableEntryLayerIds == null
                ? Array.Empty<string>()
                : unavailableEntryLayerIds.ToArray();
            Layers = layers == null ? Array.Empty<PoolFunnelLayerResult>() : layers.ToArray();
        }

        public string SessionId { get; }

        public string PoolId { get; }

        public string SelectedEntryLayerId { get; }

        public IReadOnlyList<string> UnavailableEntryLayerIds { get; }

        public IReadOnlyList<PoolFunnelLayerResult> Layers { get; }
    }

    public sealed class PoolFunnelRunResult
    {
        public PoolFunnelRunResult(
            string sessionId,
            string poolId,
            IReadOnlyList<PoolFunnelLayerResult> layers,
            string outcome,
            PoolFunnelTrace trace)
        {
            SessionId = sessionId;
            PoolId = poolId;
            Layers = layers == null ? Array.Empty<PoolFunnelLayerResult>() : layers.ToArray();
            Outcome = outcome;
            Trace = trace;
        }

        public string SessionId { get; }

        public string PoolId { get; }

        public IReadOnlyList<PoolFunnelLayerResult> Layers { get; }

        public string Outcome { get; }

        public PoolFunnelTrace Trace { get; }

        public PoolFunnelLayerResult LastLayer => Layers.Count == 0 ? null : Layers[Layers.Count - 1];
    }
}
