using JtkLibrary;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace JtkPcClientWpf {
    public enum LoginManagerState {
        Disconnected, LoggedOut, LoggedIn
    }
    public enum LoginManagerError {
        None,
        Unknown,
        Disconnected,
        AccessDenied,
        AlreadyConnected,
        AlreadyDisconnected,
        NotConnected,
        NotLoggedIn,
        AlreadyLoggedIn,
        AlreadyLoggedOut,
        InternalServerError,
        CashIsZero,
        WrongIdOrPassword,
        LoggedInOnOtherPc,
        NoResponse
    }
    public sealed class LoginManager : INotifyPropertyChanged, IDisposable {
        public string PcId { get; private set; } = "pc001";
        private Uri webApiUri = null;
        private Uri webSocketUri = null;
        private ClientWebSocket webSocket = null;
        public int AuthorizationKey { get; private set; }
        private bool isGuest = false;
        private Task loginPingLoopTask = null;
        private bool loginPingLoopTaskStop = false;
        private LoginManagerState state = LoginManagerState.Disconnected;
        private static readonly bool designMode = !(Application.Current is App);
        public LoginManagerState State {
            get => state;
            private set {
                if (state != value) {
                    var prev = state;
                    state = value;
                    StateChanged?.Invoke(this, (prev, value));
                }
            }
        }

        //public string PcId = "pc001";
        public string LocalServerAddress = "localhost";

        public event EventHandler<(LoginManagerState previousState, LoginManagerState newState)> StateChanged;
        public event EventHandler<LoginManagerError> LoggedOut;
        public event PropertyChangedEventHandler PropertyChanged;

        //private static LoginManager sharedInstance;
        //public static LoginManager SharedInstance => sharedInstance == null ? sharedInstance = new LoginManager() : sharedInstance;

        public string name;
        /// <summary>
        /// 구별을 위한 이름입니다.
        /// </summary>
        public string Name {
            get => name;
            private set { name = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"))); }
        }

        public string placeName = designMode ? "샹떼 부천점" : null;
        /// <summary>
        /// PC방의 이름입니다.
        /// </summary>
        public string PlaceName {
            get => placeName;
            private set { placeName = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("PlaceName"))); }
        }

        public int pcNumber = designMode ? 23 : 0;
        /// <summary>
        /// PC 번호입니다.
        /// </summary>
        public int PcNumber {
            get => pcNumber;
            private set { pcNumber = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("PcNumber"))); }
        }

        public double farePerHour = designMode ? 500 : 0;
        /// <summary>
        /// 연결된 로컬 서버의 시간당 요금입니다.
        /// </summary>
        public double FarePerHour {
            get => farePerHour;
            private set { farePerHour = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("FarePerHour"))); TimeLeft = TimeSpan.FromHours(FarePerHour == 0 ? 0 : cash / FarePerHour); }
        }
        
        public DateTime loginTime = designMode ? DateTime.Now : default(DateTime);
        /// <summary>
        /// 로그인한 시간입니다.
        /// </summary>
        public DateTime LoginTime {
            get => loginTime;
            private set { loginTime = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("LoginTime"))); }
        }

        public double cash = designMode ? 8431 : 0;
        /// <summary>
        /// 로그인된 계정의 남은 Cash입니다.
        /// </summary>
        public double Cash {
            get => cash;
            private set { cash = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Cash"))); TimeLeft = TimeSpan.FromHours(FarePerHour == 0 ? 0 : cash / FarePerHour); }
        }

        public TimeSpan timeLeft = designMode ? TimeSpan.FromHours(16.862) : default(TimeSpan);
        /// <summary>
        /// 로그인된 계정의 남은 시간입니다.
        /// </summary>
        public TimeSpan TimeLeft {
            get => timeLeft;
            private set { timeLeft = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TimeLeft"))); }
        }

        private string userId = designMode ? "rsatang4" : null;
        /// <summary>
        /// 로그인된 계정의 User ID 또는 Guest ID 입니다.
        /// </summary>
        public string UserId {
            get => userId;
            private set { userId = value; Application.Current.Dispatcher.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("UserId"))); }
        }

        
        
        public LoginManager() {
            WebSocketManager.OnReceive += WebSocketReceived;
            WebSocketManager.OnClosed += WebSocketClosed;
        }

        #region IDisposable Support
        private bool disposedValue = false;
        public void Dispose() {
            if (!disposedValue) {
                WebSocketManager.OnReceive -= WebSocketReceived;
                WebSocketManager.OnClosed -= WebSocketClosed;
                disposedValue = true;
            }
        }
        #endregion

        /// <summary>
        /// 주어진 정보로 로컬 서버와 연결합니다.
        /// </summary>
        /// <param name="pcId">연결할 때 사용할 PC ID</param>
        /// <param name="address">연결할 로컬 서버의 주소</param>
        /// <param name="port">연결할 포트</param>
        /// <returns>오류값</returns>
        public async Task<LoginManagerError> Connect(string pcId, string address, int port = 5000) {
            if (State != LoginManagerState.Disconnected) {
                return LoginManagerError.AlreadyConnected;
            }
            webApiUri = new Uri($"http://{address}:{port}/api");
            webSocketUri = new Uri($"ws://{address}:{port}/ws");
            this.PcId = pcId;
            try {
                webSocket = new ClientWebSocket();

                // 로컬서버와 웹소켓 연결
                await webSocket.ConnectAsync(webSocketUri, new CancellationTokenSource(5000).Token);
                if (webSocket.State != WebSocketState.Open) {
                    //throw new Exception("Failed to connect to the server.");
                    return LoginManagerError.NoResponse;
                }
                // 인증 요청
                var sendBuffer = Encoding.UTF8.GetBytes(JObject.FromObject(new { pc_id = pcId }).ToString());
                await webSocket.SendAsync(new ArraySegment<byte>(sendBuffer), WebSocketMessageType.Text, true, CancellationToken.None);
                // 인증 결과 수신
                var receiveBuffer = new byte[1024];
                var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
                // 거부되었나 확인
                if (receiveResult.CloseStatus != null) {
                    //throw new Exception(receiveResult.CloseStatusDescription);
                    return LoginManagerError.AccessDenied;
                }
                // 유효하지 않은 결과인지 확인
                if (receiveResult.MessageType != WebSocketMessageType.Text || receiveResult.EndOfMessage == false) {
                    //throw new Exception("Received invalid data.");
                    return LoginManagerError.InternalServerError;
                }
                // 결과 JSON 파싱
                var receiveText = Encoding.UTF8.GetString(receiveBuffer);
                var receiveJObject = JObject.Parse(receiveText);
                // ok 아닐 시
                if (receiveJObject.GetValue("result").ToString() != "ok") {
                    //throw new Exception("The response is not ok.");
                    return LoginManagerError.InternalServerError;
                }
                // 허가키 획득 (세션없이 HTTP 통신 위해)
                AuthorizationKey = receiveJObject.GetValue("auth_key").ToObject<int>();
                PcNumber = receiveJObject.GetValue("pc_number").ToObject<int>();
                PlaceName = receiveJObject.GetValue("place_name").ToString();
                // 수신 루프 구성
                var loop = WebSocketManager.AddAndLoop(string.Empty, webSocket);
                State = LoginManagerState.LoggedOut;
                return LoginManagerError.None;
            } catch {
                return LoginManagerError.NoResponse;
            }
        }

        /// <summary>
        /// Guest 계정으로 로그인합니다.
        /// </summary>
        /// <param name="guestId">Guests ID</param>
        /// <returns>오류값</returns>
        public async Task<LoginManagerError> Login(string guestId)
            => await ActualLogin(guestId);

        /// <summary>
        /// User 계정으로 로그인합니다.
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="password"></param>
        /// <returns>오류값</returns>
        public async Task<LoginManagerError> Login(string userId, string password)
            => await ActualLogin(userId, password);

        public Task<LoginManagerError> WriteReview(string userId, string nickname, double score, string text) => Task.Factory.StartNew((Func<object, LoginManagerError>)(state => {
            var _userId = (string)(state as object[])[0];
            var _nickname = (string)(state as object[])[1];
            var _score= (double)(state as object[])[2];
            var _text= (string)(state as object[])[3];
            
            
            // 서버에 평가 저장 요청
            var task = GetPostResponseAsync(new {
                work = "user_review",
                user_id = userId,
                nickname,
                score,
                text,
                pc_id = this.PcId,
                auth_key = AuthorizationKey
            });
            task.Wait();
            var response = task.Result;
            if (response == null) {
                // 서버 무응답시, Disconnect
                return LoginManagerError.Disconnected;
            }
            return LoginManagerError.None;
        }), new object[] { userId, nickname, score, text });

        /// <summary>
        /// 로그인한 계정을 로그아웃합니다.
        /// </summary>
        /// <returns></returns>
        public async Task<LoginManagerError> Logout() 
            => await ActualLogout(LoginManagerError.None);

        /// <summary>
        /// 로컬 서버와의 연결을 끊습니다.
        /// </summary>
        /// <returns>오류값</returns>
        public async Task<LoginManagerError> Disconnect(bool webSocketClosed = false) {
            if (State == LoginManagerState.Disconnected) {
                return LoginManagerError.AlreadyDisconnected;
            }

            // 웹소켓 닫기
            if (webSocketClosed == false) {
                if (State == LoginManagerState.LoggedIn) {
                    // LoggedIn이면 Logout
                    await ActualLogout(LoginManagerError.Disconnected);
                }
                await WebSocketManager.CloseWebSocket(string.Empty);
            }

            // 상태 변경
            State = LoginManagerState.Disconnected;

            // 결과 반환
            return LoginManagerError.None;
        }

        private void WebSocketReceived(object sender, WebSocketManagerEventArgs e) {
            if (e.MessageType != WebSocketMessageType.Text) {
                return;
            }

        }

        private void WebSocketClosed(object sender, WebSocketManagerEventArgs e) {
            if (e.Key == string.Empty) {
                Disconnect(true).Wait();
            }
        }

        /// <summary>
        /// 서버에 로그인을 요청한다. 서버의 응답이 없어도 Disconnect 해버리진 않는다.
        /// </summary>
        private async Task<LoginManagerError> ActualLogin(string id, string password = null) {
            if (State == LoginManagerState.Disconnected) {
                return LoginManagerError.NotConnected;
            }
            if (State == LoginManagerState.LoggedIn) {
                return LoginManagerError.AlreadyLoggedIn;
            }

            isGuest = password == null;

            // user_login / guest_login 시도
            var response = await (isGuest ? GetPostResponseAsync(new {
                work = "guest_login",
                pc_id = PcId,
                auth_key = AuthorizationKey,
                guest_id = id
            }) : GetPostResponseAsync(new {
                work = "user_login",
                pc_id = PcId,
                auth_key = AuthorizationKey,
                user_id = id,
                user_password = password
            }));
            if (response == null) {
                return LoginManagerError.NoResponse;
            }
            response.TryGetValue("result", out var resultToken);
            if (resultToken?.ToString() != "ok") {
                switch (response["error"].ToString()) {
                    case "Cash is zero.": return LoginManagerError.CashIsZero;
                    case "Already logged in.":
                        isGuest = response["is_guest"].ToObject<bool>();
                        Cash = response[isGuest ? "guest_total_cash" : "user_total_cash"].ToObject<double>();
                        UserId = response[isGuest ? "guest_id" : "user_id"].ToString();
                        State = LoginManagerState.LoggedIn;
                        return LoginManagerError.AlreadyLoggedIn;
                    case "Wrong ID or password.": case "Wrong ID.": return LoginManagerError.WrongIdOrPassword;
                    case "Logged in on other PC.": return LoginManagerError.LoggedInOnOtherPc;
                    default: return LoginManagerError.Unknown;
                }
            }

            FarePerHour = response["fare_per_hour"].ToObject<double>();
            LoginTime = response["login_time"].ToObject<DateTime>();
            Cash = response[isGuest ? "guest_total_cash" : "user_total_cash"].ToObject<double>();
            UserId = id;

            loginPingLoopTask = Task.Factory.StartNew(LoginPingLoop);
            State = LoginManagerState.LoggedIn;
            return LoginManagerError.None;
        }

        private void LoginPingLoop() {
            Task.Delay(5000).Wait();
            while (loginPingLoopTaskStop == false) {
                var task = isGuest ? GetPostResponseAsync(new {
                    work = "guest_ping",
                    guest_id = userId,
                    pc_id = PcId,
                    auth_key = AuthorizationKey
                }) : GetPostResponseAsync(new {
                    work = "user_ping",
                    user_id = userId,
                    pc_id = PcId,
                    auth_key = AuthorizationKey
                });
                task.Wait();
                var response = task.Result;
                if (response == null) {
                    // 서버 무응답
                    ActualLogout(LoginManagerError.NoResponse, true).Wait();
                    break;
                }
                response.TryGetValue("result", out var resultToken);
                if (resultToken?.ToString() != "ok") {
                    response.TryGetValue("error", out var errorToken);  // error는 서버에서 logout 처리 되었음을 의미함
                    switch (errorToken.ToString()) {
                        case "Cash is zero.": ActualLogout(LoginManagerError.CashIsZero, true).Wait(); break;
                        case "Not logged in.": ActualLogout(LoginManagerError.NotLoggedIn, true).Wait(); break;
                        default: ActualLogout(LoginManagerError.Unknown, true).Wait(); break;
                    }
                    break;
                }
                Cash = response[isGuest ? "guest_total_cash" : "user_total_cash"].ToObject<double>();
                FarePerHour = response["fare_per_hour"].ToObject<double>();
                Task.Delay(5000).Wait();
            }
            loginPingLoopTaskStop = false;
        }

        /// <summary>
        /// 서버에 로그아웃을 요청한다. 서버의 응답이 없으면 Disconnect 해버린다.
        /// </summary>
        private Task<LoginManagerError> ActualLogout(LoginManagerError reason, bool loopEnded = false) => Task.Factory.StartNew((Func<object, LoginManagerError>)(state => {
            var _reason = (LoginManagerError)(state as object[])[0];
            var _loopEnded = (bool)(state as object[])[1];

            if (State == LoginManagerState.LoggedOut) {
                return LoginManagerError.AlreadyLoggedOut;
            } else if (State == LoginManagerState.Disconnected) {
                return LoginManagerError.NotConnected;
            }

            // Pay Loop 정지
            if (_loopEnded == false) { // loop가 끝났음은 서버로부터 error를 응답받은 것이고, error는 서버에서 logout 처리 되었음을 의미함
                loginPingLoopTaskStop = true;
                loginPingLoopTask?.Wait();
                loginPingLoopTaskStop = false;
                loginPingLoopTask = null;

                // 서버에 logout 요청
                var task = (isGuest ? GetPostResponseAsync(new {
                    work = "guest_logout",
                    guest_id = userId,
                    pc_id = this.PcId,
                    auth_key = AuthorizationKey
                }) : GetPostResponseAsync(new {
                    work = "user_logout",
                    user_id = userId,
                    pc_id = this.PcId,
                    auth_key = AuthorizationKey
                }));
                task.Wait();
                var response = task.Result;
                if (response == null) {
                    // 서버 무응답시, Disconnect
                    State = LoginManagerState.LoggedOut;
                    Application.Current.Dispatcher.Invoke(() => LoggedOut?.Invoke(this, LoginManagerError.Disconnected));
                    Disconnect().Wait();
                    return LoginManagerError.Disconnected;
                }
            }

            State = LoginManagerState.LoggedOut;
            Application.Current.Dispatcher.Invoke(() => LoggedOut?.Invoke(this, _reason));
            return LoginManagerError.None;
        }), new object[] { reason, loopEnded });

        public Task<JObject> GetPostResponseAsync(object requestObject) {
            try { return GetPostResponseAsync(JObject.FromObject(requestObject)); } catch { return null; }
        }
        private readonly object requestLocker = new object();
        private readonly CookieContainer cookieContainer = new CookieContainer();
        public Task<JObject> GetPostResponseAsync(JObject requestJObject) =>
            Task.Factory.StartNew(obj => {
                try {
                    lock (requestLocker) {
                        var jObject = obj as JObject;

                        // 요청 준비
                        HttpWebRequest request = WebRequest.Create(webApiUri) as HttpWebRequest;
                        request.CookieContainer = cookieContainer;
                        request.Method = "POST";
                        var requestBuffer = Encoding.UTF8.GetBytes(jObject.ToString());
                        request.ContentLength = requestBuffer.Length;
                        request.ContentType = "application/x-www-form-urlencoded";
                        using (var stream = request.GetRequestStream()) {
                            stream.Write(requestBuffer, 0, requestBuffer.Length);
                        }

                        // 응답 받기
                        string result;
                        using (var response = request.GetResponse()) {
                            if (response.ContentLength <= 0) {
                                return null;
                            }
                            var responseBuffer = new byte[response.ContentLength];
                            using (var stream = response.GetResponseStream()) {
                                using (var reader = new StreamReader(stream)) {
                                    result = reader.ReadToEnd();
                                }
                            }
                        }
                        return JObject.Parse(result);
                    }
                } catch /*(Exception e)*/ {
                    return null;
                }
            }, requestJObject);

        
    }
}
