using System;
using Intrepid.Diagnostics;
using Intrepid.Gameplay.Journey.Runtime.Messages;
using UnityEngine;

namespace Intrepid.Gameplay.Journey.Runtime.Resolvers
{
    public sealed class JourneySocketTravelResolver : MonoBehaviour
    {
        public bool TryCreateSocketTravelRequest(
            StageTag currentStageTag,
            StageTag originStageTag,
            string originEndpointId,
            out JourneyTravelRequest request)
        {
            request = null;

            if (!ValidateOriginStage(currentStageTag, originStageTag, originEndpointId))
            {
                return false;
            }

            if (!TryGetOriginEndpoint(
                    originStageTag,
                    originEndpointId,
                    out var originEndpoint))
            {
                return false;
            }

            if (!TryGetSingleSocketConnection(
                    originStageTag,
                    originEndpointId,
                    out var connection))
            {
                return false;
            }

            if (!ValidateConnectionDirection(
                    connection,
                    originEndpointId,
                    originStageTag))
            {
                return false;
            }

            if (!TryGetDestinyEndpoint(
                    connection,
                    originEndpointId,
                    originStageTag,
                    out var destinyEndpointId,
                    out var destinyEndpoint))
            {
                return false;
            }

            if (!TryGetDestinyStageTag(
                    destinyEndpointId,
                    originStageTag,
                    originEndpointId,
                    out var destinyStageTag))
            {
                return false;
            }

            request = new JourneyTravelRequest
            {
                OriginStageTag = originStageTag,
                OriginEndpointId = originEndpoint.id,
                DestinyStageTag = destinyStageTag,
                DestinyEndpointId = destinyEndpoint.id,
                Connection = connection,
            };

            DLogger.LogDebug($"resolved socket travel: {request}", ctx: this);

            return true;
        }

        private bool ValidateOriginStage(
            StageTag currentStageTag,
            StageTag originStageTag,
            string originEndpointId)
        {
            if (originStageTag.IsNull())
            {
                DLogger.LogWarn(
                    "cannot resolve travel because origin stage is null. " +
                    $"Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            if (string.IsNullOrWhiteSpace(originStageTag.StageId))
            {
                DLogger.LogWarn(
                    "cannot resolve travel because origin stage has no StageId. " +
                    $"Stage: {originStageTag}, Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            if (string.IsNullOrWhiteSpace(originEndpointId))
            {
                DLogger.LogWarn(
                    "cannot resolve travel because origin endpoint id is empty. " +
                    $"Stage: {originStageTag.StageId}",
                    ctx: this);

                return false;
            }

            if (currentStageTag.IsNull())
            {
                DLogger.LogWarn(
                    "cannot resolve travel because current stage is null. " +
                    $"Origin stage: {originStageTag.StageId}, Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            if (currentStageTag != originStageTag)
            {
                DLogger.LogWarn(
                    "rejected travel from a stage that is not current. " +
                    $"Current: {currentStageTag.StageId}, " +
                    $"Origin: {originStageTag.StageId}, " +
                    $"Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            return true;
        }

        private bool TryGetOriginEndpoint(
            StageTag originStageTag,
            string originEndpointId,
            out JEndpoint originEndpoint)
        {
            originEndpoint = null;

            if (!JourneyService.Instance.TryGetEndpoint(originEndpointId, out originEndpoint))
            {
                DLogger.LogWarn(
                    "could not find origin endpoint. " +
                    $"Stage: {originStageTag.StageId}, Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            if (!string.Equals(originEndpoint.stageId, originStageTag.StageId, StringComparison.Ordinal))
            {
                DLogger.LogWarn(
                    "rejected endpoint because it does not belong to the origin stage. " +
                    $"Origin stage: {originStageTag.StageId}, " +
                    $"Endpoint: {originEndpoint.id}, " +
                    $"Endpoint stage: {originEndpoint.stageId}",
                    ctx: this);

                return false;
            }

            if (!originEndpoint.IsSocket)
            {
                DLogger.LogWarn(
                    "rejected endpoint because it is not a socket. " +
                    $"Stage: {originStageTag.StageId}, " +
                    $"Endpoint: {originEndpoint.id}, " +
                    $"Kind: {originEndpoint.kind}",
                    ctx: this);

                return false;
            }

            return true;
        }

        private bool TryGetSingleSocketConnection(
            StageTag originStageTag,
            string originEndpointId,
            out JConnection connection)
        {
            connection = null;

            if (!JourneyService.Instance.TryGetJConnections(
                    originStageTag.StageId,
                    originEndpointId,
                    out var connections))
            {
                DLogger.LogWarn(
                    "could not find connection list for socket endpoint. " +
                    $"Stage: {originStageTag.StageId}, Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            switch (connections.Count)
            {
                case <= 0:
                    DLogger.LogWarn(
                        $"found no connection from socket endpoint. " +
                        $"Stage: {originStageTag.StageId}, Endpoint: {originEndpointId}",
                        ctx: this);

                    return false;

                case > 1:
                    DLogger.LogWarn(
                        "found multiple connections from socket endpoint. " +
                        "Socket endpoints should have exactly one connection. " +
                        $"Stage: {originStageTag.StageId}, Endpoint: {originEndpointId}, " +
                        $"Connections: {connections.Count}",
                        ctx: this);

                    return false;
            }

            connection = connections[0];

            if (connection == null)
            {
                DLogger.LogWarn(
                    "found a null connection. " +
                    $"Stage: {originStageTag.StageId}, Endpoint: {originEndpointId}",
                    ctx: this);

                return false;
            }

            if (!connection.ContainsEndpoint(originEndpointId))
            {
                DLogger.LogWarn(
                    "rejected connection because it does not contain the origin endpoint. " +
                    $"Stage: {originStageTag.StageId}, " +
                    $"Endpoint: {originEndpointId}, " +
                    $"Connection: {connection}",
                    ctx: this);

                return false;
            }

            return true;
        }

        private bool ValidateConnectionDirection(
            JConnection connection,
            string originEndpointId,
            StageTag originStageTag)
        {
            if (CanTravelThrough(connection, originEndpointId))
            {
                return true;
            }

            DLogger.LogWarn(
                "rejected travel because connection direction does not allow it. " +
                $"Stage: {originStageTag.StageId}, " +
                $"Origin endpoint: {originEndpointId}, " +
                $"Connection: {connection}",
                ctx: this);

            return false;
        }

        private bool TryGetDestinyEndpoint(
            JConnection connection,
            string originEndpointId,
            StageTag originStageTag,
            out string destinyEndpointId,
            out JEndpoint destinyEndpoint)
        {
            destinyEndpointId = null;
            destinyEndpoint = null;

            destinyEndpointId = connection.GetOtherEndpointId(originEndpointId);

            if (string.IsNullOrWhiteSpace(destinyEndpointId))
            {
                DLogger.LogWarn(
                    "could not resolve destiny endpoint from connection. " +
                    $"Stage: {originStageTag.StageId}, " +
                    $"Origin endpoint: {originEndpointId}, " +
                    $"Connection: {connection}",
                    ctx: this);

                return false;
            }

            if (!JourneyService.Instance.TryGetEndpoint(destinyEndpointId, out destinyEndpoint))
            {
                DLogger.LogWarn(
                    "could not find destiny endpoint. " +
                    $"Origin stage: {originStageTag.StageId}, " +
                    $"Origin endpoint: {originEndpointId}, " +
                    $"Destiny endpoint: {destinyEndpointId}, " +
                    $"Connection: {connection}",
                    ctx: this);

                return false;
            }

            if (!destinyEndpoint.IsSocket)
            {
                DLogger.LogWarn(
                    "rejected destiny endpoint because it is not a socket. " +
                    $"Origin stage: {originStageTag.StageId}, " +
                    $"Origin endpoint: {originEndpointId}, " +
                    $"Destiny endpoint: {destinyEndpoint.id}, " +
                    $"Kind: {destinyEndpoint.kind}",
                    ctx: this);

                return false;
            }

            return true;
        }

        private bool TryGetDestinyStageTag(
            string destinyEndpointId,
            StageTag originStageTag,
            string originEndpointId,
            out StageTag destinyStageTag)
        {
            destinyStageTag = null;

            if (!JourneyService.Instance.TryGetStageTagFromEndpoint(
                    destinyEndpointId,
                    out destinyStageTag))
            {
                DLogger.LogWarn(
                    "could not resolve destiny stage from endpoint. " +
                    $"Origin stage: {originStageTag.StageId}, " +
                    $"Origin endpoint: {originEndpointId}, " +
                    $"Destiny endpoint: {destinyEndpointId}",
                    ctx: this);

                return false;
            }

            if (destinyStageTag.IsNull())
            {
                DLogger.LogWarn(
                    "resolved a null destiny stage. " +
                    $"Origin stage: {originStageTag.StageId}, " +
                    $"Origin endpoint: {originEndpointId}, " +
                    $"Destiny endpoint: {destinyEndpointId}",
                    ctx: this);

                return false;
            }

            return true;
        }

        private static bool CanTravelThrough(
            JConnection connection,
            string originEndpointId)
        {
            return connection.directionality switch
            {
                JConnectionDirectionality.Bidirectional => true,
                JConnectionDirectionality.AToB => string.Equals(
                    connection.endpointAId,
                    originEndpointId,
                    StringComparison.Ordinal),

                JConnectionDirectionality.BToA => string.Equals(
                    connection.endpointBId,
                    originEndpointId,
                    StringComparison.Ordinal),

                _ => false
            };
        }
    }
}