using Ikar.Shared;

namespace Ikar.Server.Infrastructure.Calls;

public sealed class CallSessionStore
{
    private readonly object _sync = new();
    private readonly Dictionary<Guid, StoredCallSession> _sessions = [];
    private readonly Dictionary<Guid, Guid> _participantCalls = [];

    public bool TryCreate(Guid chatId, Guid callerId, Guid calleeId, CallMediaType mediaType, out ActiveCallSession? session, out string error)
    {
        lock (_sync)
        {
            if (_participantCalls.ContainsKey(callerId))
            {
                error = "У вас уже есть активный звонок.";
                session = default;
                return false;
            }

            if (_participantCalls.ContainsKey(calleeId))
            {
                error = "Собеседник уже занят другим звонком.";
                session = default;
                return false;
            }

            var stored = new StoredCallSession(chatId, callerId, calleeId, mediaType);
            _sessions[stored.CallId] = stored;
            _participantCalls[callerId] = stored.CallId;
            _participantCalls[calleeId] = stored.CallId;
            session = stored.ToSnapshot();
            error = string.Empty;
            return true;
        }
    }

    public bool TryAccept(Guid callId, Guid userId, out ActiveCallSession? session, out string error)
    {
        lock (_sync)
        {
            if (!_sessions.TryGetValue(callId, out var stored))
            {
                error = "Звонок уже завершён.";
                session = default;
                return false;
            }

            if (stored.State != CallSessionState.Ringing)
            {
                error = "Звонок уже недоступен.";
                session = default;
                return false;
            }

            if (stored.CalleeId != userId)
            {
                error = "Принять звонок может только вызываемый пользователь.";
                session = default;
                return false;
            }

            stored.State = CallSessionState.Connected;
            stored.ConnectedAt = DateTimeOffset.UtcNow;
            session = stored.ToSnapshot();
            error = string.Empty;
            return true;
        }
    }

    public bool TryDecline(Guid callId, Guid userId, out ActiveCallSession? session, out string error) =>
        TryTerminate(callId, userId, "declined", requireParticipant: true, out session, out error);

    public bool TryEnd(Guid callId, Guid userId, string reason, out ActiveCallSession? session, out string error) =>
        TryTerminate(callId, userId, reason, requireParticipant: true, out session, out error);

    public bool TryEndByParticipant(Guid userId, string reason, out ActiveCallSession? session)
    {
        lock (_sync)
        {
            if (!_participantCalls.TryGetValue(userId, out var callId) ||
                !_sessions.TryGetValue(callId, out var stored))
            {
                session = default;
                return false;
            }

            RemoveSession(stored, reason);
            session = stored.ToSnapshot();
            return true;
        }
    }

    public bool TryResolveRecipient(Guid callId, Guid senderUserId, out Guid recipientUserId, out ActiveCallSession? session, out string error)
    {
        lock (_sync)
        {
            if (!_sessions.TryGetValue(callId, out var stored))
            {
                recipientUserId = Guid.Empty;
                session = default;
                error = "Звонок уже завершён.";
                return false;
            }

            if (stored.State != CallSessionState.Connected)
            {
                recipientUserId = Guid.Empty;
                session = default;
                error = "Соединение ещё не установлено.";
                return false;
            }

            if (stored.CallerId == senderUserId)
            {
                recipientUserId = stored.CalleeId;
                session = stored.ToSnapshot();
                error = string.Empty;
                return true;
            }

            if (stored.CalleeId == senderUserId)
            {
                recipientUserId = stored.CallerId;
                session = stored.ToSnapshot();
                error = string.Empty;
                return true;
            }

            recipientUserId = Guid.Empty;
            session = default;
            error = "Нет доступа к этому звонку.";
            return false;
        }
    }

    private bool TryTerminate(Guid callId, Guid userId, string reason, bool requireParticipant, out ActiveCallSession? session, out string error)
    {
        lock (_sync)
        {
            if (!_sessions.TryGetValue(callId, out var stored))
            {
                error = "Звонок уже завершён.";
                session = default;
                return false;
            }

            if (requireParticipant && stored.CallerId != userId && stored.CalleeId != userId)
            {
                error = "Нет доступа к этому звонку.";
                session = default;
                return false;
            }

            RemoveSession(stored, reason);
            session = stored.ToSnapshot();
            error = string.Empty;
            return true;
        }
    }

    private void RemoveSession(StoredCallSession stored, string reason)
    {
        stored.State = CallSessionState.Ended;
        stored.EndReason = reason;
        _sessions.Remove(stored.CallId);
        _participantCalls.Remove(stored.CallerId);
        _participantCalls.Remove(stored.CalleeId);
    }

    private sealed class StoredCallSession(Guid chatId, Guid callerId, Guid calleeId, CallMediaType mediaType)
    {
        public Guid CallId { get; } = Guid.NewGuid();

        public Guid ChatId { get; } = chatId;

        public Guid CallerId { get; } = callerId;

        public Guid CalleeId { get; } = calleeId;

        public CallMediaType MediaType { get; } = mediaType;

        public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow;

        public DateTimeOffset? ConnectedAt { get; set; }

        public string EndReason { get; set; } = string.Empty;

        public CallSessionState State { get; set; } = CallSessionState.Ringing;

        public ActiveCallSession ToSnapshot() =>
            new(CallId, ChatId, CallerId, CalleeId, MediaType, CreatedAt, ConnectedAt, EndReason, State);
    }
}

public sealed record ActiveCallSession(
    Guid CallId,
    Guid ChatId,
    Guid CallerId,
    Guid CalleeId,
    CallMediaType MediaType,
    DateTimeOffset CreatedAt,
    DateTimeOffset? ConnectedAt,
    string EndReason,
    CallSessionState State);

public enum CallSessionState
{
    Ringing = 0,
    Connected = 1,
    Ended = 2
}
