using System.ComponentModel;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using TeamsLocalLibary.EventArgs;

namespace TeamsLocalLibary;

public class Client : INotifyPropertyChanged
{
    public bool IsBackgroundBlurred { get => isBackgroundBlurred; set { if (value != IsBackgroundBlurred) _ = SendCommand(value ? MeetingAction.BlurBackground : MeetingAction.UnblurBackground); } }
    public bool IsHandRaised { get => isHandRaised; set { if (value != IsHandRaised) _ = SendCommand(value ? MeetingAction.RaiseHand : MeetingAction.LowerHand); } }
    public bool IsMuted { get => isMuted; set { if (value != IsMuted) _ = SendCommand(value ? MeetingAction.Mute : MeetingAction.Unmute); } }
    public bool IsVideoOn { get => isVideoOn; set { if (value != IsVideoOn) _ = SendCommand(value ? MeetingAction.ShowVideo : MeetingAction.HideVideo); } }
    public bool IsSharing { get => isSharing; set { if (value != IsSharing) _ = value ? SendCommand(MeetingAction.ToggleUI, ClientMessageParameterType.ToggleUiSharing) : _ = SendCommand(MeetingAction.StopSharing); } }

    public bool IsRecordingOn { get => isRecordingOn; }
    public bool IsInMeeting { get => isInMeeting; }

    public bool HasUnreadMessages { get => hasUnreadMessages; }

    public bool IsConnected => ws is not null && ws.State == WebSocketState.Open;

    public bool CanToggleBlur { get; private set; }
    public bool CanToggleHand { get; private set; }
    public bool CanToggleMute { get; private set; }
    public bool CanToggleShareTray { get; private set; }
    public bool CanToggleVideo { get; private set; }

    public bool CanToggleChat { get; private set; }
    public bool CanStopSharing { get; private set; }

    public bool CanLeave { get; private set; }
    public bool CanPair { get; private set; }
    public bool CanReact { get; private set; }

    private bool isMuted;
    private bool isVideoOn;
    private bool isHandRaised;
    private bool isInMeeting;
    private bool isRecordingOn;
    private bool isBackgroundBlurred;
    private bool hasUnreadMessages;
    private bool isSharing;

    private readonly SemaphoreSlim sendSemaphore       = new(1);
    private readonly SemaphoreSlim receiveSemaphore    = new(1);
    private readonly SemaphoreSlim connectingSemaphore = new(1);

    private readonly JsonSerializerOptions jsonSerializerOptions = new()
    {
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    private ClientInfo clientInfo;

    private ClientWebSocket? ws;

    public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionState;

    public event PropertyChangedEventHandler? PropertyChanged;

    public event EventHandler<TokenReceivedEventArgs>? TokenReceived;
    public event EventHandler<SuccessReceivedEventArgs>? SuccessReceived;
    public event EventHandler<ErrorReceivedEventArgs>? ErrorReceived;

    public Client(bool autoConnect = false, string manufacturer = "krjan02", string device = "TeamsLocalLibary", string app = "NetPhone Teams Synchronisation", string appVersion = "1.0.0", string? token = null, CancellationToken cancellationToken = default)
    {
        clientInfo = new() {
            App = app,
            AppVersion = appVersion,
            Device = device,
            Manufacturer = manufacturer,
            Token = token
        };

        if (autoConnect)
            _ = Connect(true, cancellationToken);
    }

    public async Task Disconnect(CancellationToken cancellationToken = default)
    {
        if (ws is null) return;

        var initialState = ws.State;

        switch (ws.State)
        {
            case WebSocketState.Open:
            case WebSocketState.Connecting:
                await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken);
                break;
            case WebSocketState.CloseSent:
                await WaitClose(cancellationToken);
                break;
            case WebSocketState.CloseReceived:
            case WebSocketState.Aborted:
            case WebSocketState.Closed:
            case WebSocketState.None:
                break;
        }

        ws.Dispose();
        ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(ws.State));
        ws = null;
    }

    public async Task Reconnect(bool waitForTeams, CancellationToken cancellationToken = default)
    {
        await Disconnect(cancellationToken);
        await Connect(waitForTeams, cancellationToken);
    }

    private async Task WaitClose(CancellationToken cancellationToken = default)
    {
        if (ws is null) return;

        while (ws is not null && ws.State != WebSocketState.Closed && ws.State != WebSocketState.Aborted)
            await Task.Delay(25, cancellationToken);
    }

    private async Task WaitOpen(CancellationToken cancellationToken = default)
    {
        if (ws is null) throw new InvalidOperationException();

        while (ws.State == WebSocketState.Connecting)
            await Task.Delay(25, cancellationToken);
    }

    private async void Receive(CancellationToken cancellationToken = default)
    {
        if (ws is null || ws.State != WebSocketState.Open)
            throw new InvalidOperationException("Not connected");

        try
        {
            await receiveSemaphore.WaitAsync(cancellationToken);

            var buffer = new byte[1024];

            while (true)
            {
                var result = await ws.ReceiveAsync(buffer, cancellationToken);

                if (result.MessageType == WebSocketMessageType.Close)
                {
                    if (ws.CloseStatus != WebSocketCloseStatus.NormalClosure)
                    {
                        ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(this.ws.State));
                        _ = Reconnect(true, cancellationToken);
                    }

                    return;
                }

                if (!result.EndOfMessage || result.Count == 0)
                    throw new NotImplementedException("Invalid Message received");

                var data = Encoding.UTF8.GetString(buffer, 0, result.Count);

                var message = JsonSerializer.Deserialize<ServerMessage>(data, jsonSerializerOptions) ?? throw new InvalidDataException();

                if (message.Response == "Success")
                    SuccessReceived?.Invoke(this, new SuccessReceivedEventArgs(message.RequestId ?? 0));

                if (!string.IsNullOrEmpty(message.ErrorMsg))
                {
                    if (message.ErrorMsg.EndsWith("no active call"))
                        continue;

                    ErrorReceived?.Invoke(this, new ErrorReceivedEventArgs(message.ErrorMsg));
                }

                if (message.TokenRefresh is not null)
                {
                    clientInfo.Token = message.TokenRefresh;
                    TokenReceived?.Invoke(this, new TokenReceivedEventArgs(message.TokenRefresh));
                }

                if (message.MeetingUpdate is null || message.MeetingUpdate.MeetingState is null)
                    continue;

                if (isSharing != message.MeetingUpdate.MeetingState.IsSharing)
                {
                    isSharing = message.MeetingUpdate.MeetingState.IsSharing;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSharing)));
                }

                if (isMuted != message.MeetingUpdate.MeetingState.IsMuted)
                {
                    isMuted = message.MeetingUpdate.MeetingState.IsMuted;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMuted)));
                }

                if (hasUnreadMessages != message.MeetingUpdate.MeetingState.HasUnreadMessages)
                {
                    hasUnreadMessages = message.MeetingUpdate.MeetingState.HasUnreadMessages;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasUnreadMessages)));
                }

                if (isVideoOn != message.MeetingUpdate.MeetingState.IsVideoOn)
                {
                    isVideoOn = message.MeetingUpdate.MeetingState.IsVideoOn;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsVideoOn)));
                }

                if (isHandRaised != message.MeetingUpdate.MeetingState.IsHandRaised)
                {
                    isHandRaised = message.MeetingUpdate.MeetingState.IsHandRaised;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHandRaised)));
                }

                if (isInMeeting != message.MeetingUpdate.MeetingState.IsInMeeting)
                {
                    isInMeeting = message.MeetingUpdate.MeetingState.IsInMeeting;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsInMeeting)));
                }

                if (isRecordingOn != message.MeetingUpdate.MeetingState.IsRecordingOn)
                {
                    isRecordingOn = message.MeetingUpdate.MeetingState.IsRecordingOn;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecordingOn)));
                }

                if (isBackgroundBlurred != message.MeetingUpdate.MeetingState.IsBackgroundBlurred)
                {
                    isBackgroundBlurred = message.MeetingUpdate.MeetingState.IsBackgroundBlurred;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsBackgroundBlurred)));
                }

                if (CanToggleMute != message.MeetingUpdate.MeetingPermissions.CanToggleMute)
                {
                    CanToggleMute = message.MeetingUpdate.MeetingPermissions.CanToggleMute;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleMute)));
                }

                if (CanToggleShareTray != message.MeetingUpdate.MeetingPermissions.CanToggleShareTray)
                {
                    CanToggleShareTray = message.MeetingUpdate.MeetingPermissions.CanToggleShareTray;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleShareTray)));
                }

                if (CanToggleVideo != message.MeetingUpdate.MeetingPermissions.CanToggleVideo)
                {
                    CanToggleVideo = message.MeetingUpdate.MeetingPermissions.CanToggleVideo;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleVideo)));
                }

                if (CanToggleHand != message.MeetingUpdate.MeetingPermissions.CanToggleHand)
                {
                    CanToggleHand = message.MeetingUpdate.MeetingPermissions.CanToggleHand;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleHand)));
                }

                if (CanToggleBlur != message.MeetingUpdate.MeetingPermissions.CanToggleBlur)
                {
                    CanToggleBlur = message.MeetingUpdate.MeetingPermissions.CanToggleBlur;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleBlur)));
                }

                if (CanToggleChat != message.MeetingUpdate.MeetingPermissions.CanToggleChat)
                {
                    CanToggleChat = message.MeetingUpdate.MeetingPermissions.CanToggleChat;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleChat)));
                }

                if (CanStopSharing != message.MeetingUpdate.MeetingPermissions.CanStopSharing)
                {
                    CanStopSharing = message.MeetingUpdate.MeetingPermissions.CanStopSharing;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanStopSharing)));
                }

                if (CanLeave != message.MeetingUpdate.MeetingPermissions.CanLeave)
                {
                    CanLeave = message.MeetingUpdate.MeetingPermissions.CanLeave;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanLeave)));
                }

                if (CanPair != message.MeetingUpdate.MeetingPermissions.CanPair)
                {
                    CanPair = message.MeetingUpdate.MeetingPermissions.CanPair;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanPair)));
                }

                if (CanReact != message.MeetingUpdate.MeetingPermissions.CanReact)
                {
                    CanReact = message.MeetingUpdate.MeetingPermissions.CanReact;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanReact)));
                }
            }
        }
        catch (OperationCanceledException) { }
        finally
        {
            receiveSemaphore.Release();

            Console.WriteLine(ws == null);

            if (ws is not null && ws.State != WebSocketState.Open && ws.State != WebSocketState.Connecting)
                _ = Reconnect(true, cancellationToken);
        }
    }

    public async Task<bool> Connect(bool waitForTeams = true, CancellationToken cancellationToken = default)
    {
        bool connectionSuccess = false;

        try
        {
            await connectingSemaphore.WaitAsync(cancellationToken);

            if (Process.GetProcessesByName("ms-teams").Length == 0 && !waitForTeams)
                return false;

            while (Process.GetProcessesByName("ms-teams").Length == 0)
                await Task.Delay(1000, cancellationToken);

            if (ws is not null)
            {
                switch (ws.State)
                {
                    case WebSocketState.Open:
                        connectionSuccess = true;   
                        break;
                    case WebSocketState.Connecting:
                        await WaitOpen(cancellationToken);
                        connectionSuccess = true;
                        break;
                    case WebSocketState.Aborted:
                    case WebSocketState.Closed:
                    case WebSocketState.None:
                        await Disconnect(cancellationToken);
                        break;
                    case WebSocketState.CloseReceived:
                    case WebSocketState.CloseSent:
                        await WaitClose(cancellationToken);
                        await Disconnect(cancellationToken);
                        break;
                }
            }

            ws = new();

            var url = clientInfo.GetServerUrl();

            await ws.ConnectAsync(url, cancellationToken);
            ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(ws.State));

            await WaitOpen(cancellationToken);

            if (ws.State != WebSocketState.Open)
                throw new WebSocketException($"Invalid state after connect: ${ws.State}");

            connectionSuccess = true;
            ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(this.ws.State));
            Receive(cancellationToken);
            await SendCommand(MeetingAction.QueryMeetingState, null, cancellationToken);
        }
        catch (Exception)
        {
            connectionSuccess = false;
        }
        finally
        {
            connectingSemaphore.Release();
        }

        if (!connectionSuccess)
            Connect(true, cancellationToken); //CS4014 I dont want to wait btw :)

        return connectionSuccess;
    }


    private async Task SendCommand(MeetingAction action, ClientMessageParameterType? type = null, CancellationToken cancellationToken = default)
    {
        if (ws is null)
        {
            await Connect(true, cancellationToken);
        }

        switch (ws!.State)
        {
            case WebSocketState.Aborted:
            case WebSocketState.CloseReceived:
                await Connect(true, cancellationToken);
                break;
            case WebSocketState.Connecting:
                await WaitOpen(cancellationToken);
                break;
            case WebSocketState.CloseSent:
            case WebSocketState.Closed:
                throw new InvalidOperationException("Not Connected");
            case WebSocketState.Open:
            case WebSocketState.None:
                break;
        }

        var message = JsonSerializer.Serialize(new ClientMessage()
        {
            Action = action,
            Parameters = type is null ? null : new ClientMessageParameter
            {
                Type = type.Value
            }
        }, jsonSerializerOptions);

        try
        {
            await sendSemaphore.WaitAsync(cancellationToken);

            await ws.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken);
        }
        finally
        {
            sendSemaphore.Release();
        }
    }

    public async Task LeaveCall(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.LeaveCall, null, cancellationToken);

    public async Task ReactApplause(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactApplause, cancellationToken);

    public async Task ReactLaugh(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactLaugh, cancellationToken);

    public async Task ReactLike(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactLike, cancellationToken);

    public async Task ReactLove(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactLove, cancellationToken);

    public async Task ReactWow(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactWow, cancellationToken);

    public async Task UpdateState(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.QueryMeetingState, null, cancellationToken);

    public async Task ToggleChat(CancellationToken cancellationToken = default)
        => await SendCommand(MeetingAction.ToggleUI, ClientMessageParameterType.ToggleUiChat, cancellationToken);
}