using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using JtkLocalServer.Managers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using JtkCoreLibrary;
using WebSocketManager = JtkCoreLibrary.WebSocketManager;
using System.Net;
using System.Security.Cryptography.X509Certificates;

namespace JtkLocalServer
{
    public class Startup {
        public IHostingEnvironment HostingEnvironment { get; }
        public IConfiguration Configuration { get; }

        public readonly string MainServerWebSocketUri;
        public readonly string MainServerWebApiUri;
        public readonly string PlaceId;
        public readonly bool EnableTestClients;
        public const string MainServerWebSocketKey = "_mainserver_";

        public string PlaceName;

        List<JToken> TestList = new List<JToken>();
                                            
        
        /// <summary> 메인 서버에 HTTP 통신할 때 필요한 키 </summary>
        private readonly int AuthorizationKey = 0;

        /// <summary> 연결된 PC의 허가 키를 관리할 딕셔너리 </summary>
        private Dictionary<string, int> AuthorizationKeys = new Dictionary<string, int>();

        /// <summary> 로그인된 유저의 PC 관리할 딕셔너리, Key: UserID/GuestID, Value: (PCID, LoginTime) </summary>
        private Dictionary<string, (string pcId, DateTime loginTime, DateTime lastPaidTime, double totalPaidCash, double remainingCash)> LoggedinPcs = new Dictionary<string, (string pcId, DateTime loginTime, DateTime lastPaidTime, double totalPaidCash, double remainingCash)>();

        /// <summary> 게스트 사용자를 관리할 딕셔너리, Key:GuestID Value:Cash </summary>
        private Dictionary<string, double> Guests = new Dictionary<string, double>();

        /// <summary> 최근 n개의 알림을 저장할 딕셔너리 </summary>
        private List<(DateTime time, string message)> RecentNotifications = new List<(DateTime time, string message)>();

        /// <summary> 시간당 요금 </summary>
        private double FarePerHour {
            //get => Properties.Settings.Default.FarePerHour;
            //set { Properties.Settings.Default.FarePerHour = value; Properties.Settings.Default.Save(); }
            get; set;
        } = 500;

        /// <summary> 초기 설정 및 메인 서버 웹소켓 연결 </summary>
        public Startup(IHostingEnvironment environment, IConfiguration configuration) {
            HostingEnvironment = environment;
            Configuration = configuration;
            //DbManager.ConnectionString = Configuration.GetConnectionString("DefaultConnection");
            WebSocketManager.OnReceive += WebSocketReceive;
            WebSocketManager.OnClosed += WebSocketClosed;

            string serverAddress;
            string placeId;
            string placePassword;
            if (StartArgumentsManager.HasValues) {
                serverAddress = StartArgumentsManager.ServerAddress;
                placeId = StartArgumentsManager.PlaceId;
                placePassword = StartArgumentsManager.PlacePassword;
                EnableTestClients = StartArgumentsManager.EnableTestClients;

                if (StartArgumentsManager.EnableTestClients) {
                    var rand = new Random();
                    var now = DateTime.Now;
                    for (var i = 11; i <= 64; i++) {
                        // 50%의 확률로 리스트 추가
                        if (rand.NextDouble() > 0.5) {
                            var token = JToken.FromObject(new { pc_id = $"pc{i:000}", number=i });
                            // 90%의 확률로 로그인됨
                            if (rand.NextDouble() > 0.1) {
                                token["user_id"] = $"fake{rand.Next(99999):00000}";
                                token["user_is_guest"] = false;
                                var hours = rand.NextDouble() * 3.0;
                                token["login_time"] = now - TimeSpan.FromHours(hours);
                                token["last_paid_time"] = now;
                                token["total_paid_cash"] = hours * FarePerHour;
                                token["cash_left"] = 500.0 + rand.Next(10000);
                            }
                            TestList.Add(token);
                        }
                    }
                }
            } else {
                serverAddress = Configuration.GetValue<string>("MainServerAddress");
                placeId = Configuration.GetValue<string>("PlaceId");
                placePassword = Configuration.GetValue<string>("PlacePassword");
                EnableTestClients = false;
            }
            MainServerWebSocketUri = $"wss://{serverAddress}/ws";
            MainServerWebApiUri = $"https://{serverAddress}/api";

            ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };  // 보안인증서 무조건신뢰

            // 메인 서버와 웹소켓 연결
            mainServerConnect:
            Console.Write("Connecting to main server...");
            var mainServerWebSocket = new ClientWebSocket();
            try {
                mainServerWebSocket.ConnectAsync(new Uri(MainServerWebSocketUri), new CancellationTokenSource(5000).Token).Wait();
                if (mainServerWebSocket.State != WebSocketState.Open) {
                    throw new Exception();
                }
            } catch (Exception e) {
                //Console.WriteLine(e);
                Console.WriteLine($" Failed!\r\nIt will be retried after few seconds.");
                if (e is WebSocketException we) {
                    Console.WriteLine(we.InnerException?.InnerException?.Message);
                } else if (e is AggregateException ae && ae.InnerExceptions[0] is WebSocketException wee) {
                    Console.WriteLine(wee.InnerException?.InnerException?.Message);
                }
               
                Task.Delay(5000).Wait();
                goto mainServerConnect;
            }
            // 웹소켓 인증정보 송신
            Console.Write(" Connected!\r\nSending authorization data...");
            PlaceId = placeId;
            var sendBuffer = Encoding.UTF8.GetBytes(JObject.FromObject(new { place_id = placeId, place_password = placePassword, fare_per_hour = FarePerHour }).ToString());
            mainServerWebSocket.SendAsync(new ArraySegment<byte>(sendBuffer), WebSocketMessageType.Text, true, CancellationToken.None).Wait();
            // 웹소켓 인증결과 수신
            Console.Write(" Succeed!\r\nReceiving response...");
            var receiveBuffer = new byte[1024];
            var receiveTask = mainServerWebSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
            receiveTask.Wait();
            var receiveResult = receiveTask.Result;
            // 거부되었나 확인
            if (receiveResult.CloseStatus != null) {
                Console.WriteLine($" Failed!\r\nDisconnected by server.\nReason: {receiveResult.CloseStatusDescription}");
                // 종료
                Environment.Exit(1);
            }
            // 유효하지 않은 결과인지 확인
            if (receiveResult.MessageType != WebSocketMessageType.Text) {
                Console.WriteLine(" Failed! Received invalid data.");
                // 종료
                Environment.Exit(1);
            }
            var receiveTextBuilder = new StringBuilder(Encoding.UTF8.GetString(receiveBuffer));
            while (receiveResult.EndOfMessage == false) {
                receiveBuffer = new byte[1024];
                receiveTask = mainServerWebSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
                receiveTask.Wait();
                receiveResult = receiveTask.Result;
                receiveTextBuilder.Append(Encoding.UTF8.GetString(receiveBuffer));
            }
            // 결과 JSON 파싱
            var receiveText = receiveTextBuilder.ToString();
            var receiveJObject = JObject.Parse(string.IsNullOrWhiteSpace(receiveText) ? "{}" : receiveText);
            // ok 아닐 시
            if (receiveJObject["result"].ToString() != "ok") {
                Console.WriteLine(" Failed! The response is not ok.");
                // 종료
                Environment.Exit(1);
            }

            // pc map 받기
            PcMapJson = receiveJObject["pc_map"].ToString();
            try {
                var jobj = JObject.Parse(string.IsNullOrWhiteSpace(pcMapJson) ? "{}" : PcMapJson);
                lock (PcIdNumber) {
                    PcIdNumber.Clear();
                    foreach (var tkn in jobj) {
                        PcIdNumber[tkn.Key] = tkn.Value["num"].ToObject<int>();
                    }
                }
            } catch { }

            // 주문 메뉴 받기
            OrderMenuJson = receiveJObject["order_menu"].ToString();

            AuthorizationKey = receiveJObject.GetValue("auth_key").ToObject<int>();
            PlaceName = receiveJObject.GetValue("place_name").ToString();
            SendUsingPcNumbers().Wait();

            // 수신 루프 실행
            Console.WriteLine(" Succeed!\r\nAuthorization complete.");
            WebSocketManager.AddAndLoop(MainServerWebSocketKey, mainServerWebSocket);
        }

        /// <summary> 연결된 매니저 PC의 웹소켓 키, PCID </summary>
        public readonly HashSet<string> ManagerWebSocketKeys = new HashSet<string>();

        public readonly Dictionary<string, int> PcIdToNumber = new Dictionary<string, int>();

        /// <summary> PC맵 JSON 문자열 </summary>
        private string pcMapJson = null;
        public string PcMapJson {
            get => pcMapJson;
            set {
                if (pcMapJson != value) {
                    pcMapJson = value;
                    lock (PcIdToNumber) {
                        try {
                            var obj = JObject.Parse(value);
                            PcIdToNumber.Clear();
                            foreach (var item in obj) {
                                PcIdToNumber[item.Key] = item.Value["num"].ToObject<int>();
                            }
                        } catch {

                        }
                    }
                }
            }
        }

        public readonly Dictionary<string, List<(string name, double price)>> OrderMenu = new Dictionary<string, List<(string name, double price)>>();

        /// <summary> 주문메뉴 JSON 문자열 </summary>
        private string orderMenuJson = null;
        public string OrderMenuJson {
            get => orderMenuJson;
            set {
                if (orderMenuJson != value) {
                    orderMenuJson = value;
                    lock (OrderMenu) {
                        try {
                            var obj = JObject.Parse(value);
                            OrderMenu.Clear();
                            foreach (var cat in obj) {
                                var list = OrderMenu[cat.Key] = new List<(string name, double price)>();
                                foreach (var item in cat.Value) {
                                    list.Add((item["name"].ToString(), item["price"].ToObject<double>()));
                                }
                            }
                        } catch {

                        }
                    }
                }
            }
        }

        /// <summary> PC ID와 Number 딕셔너리 </summary>
        public readonly Dictionary<string, int> PcIdNumber = new Dictionary<string, int>();

        /// <summary> 서비스 구성 </summary>
        public void ConfigureServices(IServiceCollection services) {
            //services.Configure<MvcOptions>(options => options.Filters.Add(new RequireHttpsAttribute()));
            services.AddMvc()
                .AddSessionStateTempDataProvider();
            services.AddSession(options => {
                options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                options.Cookie.Name = ".Jtk.Session";
                options.IdleTimeout = TimeSpan.FromSeconds(Configuration.GetValue<int>("SessionLifeTime"));
            });
        }

        // 매니저에게 알림 송신
        private void SendNotification(string message) {
            var time = DateTime.Now;
            lock (RecentNotifications) {
                RecentNotifications.Add((time, message));
                if (RecentNotifications.Count > 10) {
                    RecentNotifications.RemoveAt(0);
                }
            }
            
            string[] allKeys;
            lock (ManagerWebSocketKeys) {
                allKeys = ManagerWebSocketKeys.ToArray();
            }

            var wsMessage = JObject.FromObject(new { type="notification", time = time, message = message }).ToString();
            foreach (var key in allKeys) {
                WebSocketManager.SendMessageAsync(key, wsMessage);
            }
        }

        // 메인서버에게 사용 번호 리스트 송신
        private int[] lastSentUsingPcNumbers = null;
        private async Task<bool> SendUsingPcNumbers() {
            var list = new List<int>();
            lock (LoggedinPcs) {
                lock (PcIdNumber) {
                    foreach (var item in LoggedinPcs) {
                        if (PcIdNumber.TryGetValue(item.Value.pcId, out var num)) {
                            list.Add(num);
                        } //else면 pc맵에 없는 pcid
                    }
                }
            }

            foreach (var item in TestList) {
                if (string.IsNullOrEmpty(item["user_id"]?.ToString()) == false) {
                    list.Add(item["number"].ToObject<int>());
                }
            }

            var ordered = from n in list orderby n ascending select n;
            var send = false;
            if (lastSentUsingPcNumbers == null) {
                send = true;
                lastSentUsingPcNumbers = ordered.ToArray();
            } else {
                lock (lastSentUsingPcNumbers) {
                    if (lastSentUsingPcNumbers.Length != list.Count) {
                        send = true;
                        lastSentUsingPcNumbers = ordered.ToArray();
                    } else {
                        var i = 0;
                        foreach (var n in ordered) {
                            if (lastSentUsingPcNumbers[i++] != n) {
                                send = true;
                                lastSentUsingPcNumbers = ordered.ToArray();
                                break;
                            }
                        }
                    }
                }
            }

            if (send == true) {
                await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "update_using_pc_nums", place_id = PlaceId, auth_key = AuthorizationKey, list = list, total_pc_count = PcIdNumber.Count }).ToString());
            }
            return send;
        }

        // 웹소켓 수신 처리
        private void WebSocketReceive(object sender, WebSocketManagerEventArgs args) {
            if (args.MessageType != WebSocketMessageType.Text) {
                return;
            }
            Console.WriteLine($"[WebSocket] {args.TextMessage} Received.");
            if (args.Key == MainServerWebSocketKey) {
                // 메인 서버로부터 웹소켓 수신 시
                var jObject = JObject.Parse(args.TextMessage);
                switch (jObject["work"].ToString()) {
                    case "user_cash_charged": {

                        break;
                    }
                    case "user_logout": {
                        // 해당 유저에게 로그아웃 명령
                        break;
                    }
                }
            } else {
                
            }
            
        }

        // 웹소켓 닫기 처리
        private void WebSocketClosed(object sender, WebSocketManagerEventArgs args) {
            Console.WriteLine($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {args.Key} Disconnected.");
            if (args.Key == MainServerWebSocketKey) {
                // 메인 서버 웹소켓 끊길 시, 끝내기
                Console.WriteLine("Lost connection with the main server.");
                Environment.Exit(1);
                return;
            } else {
                // PC 웹소켓 끊길 시
                var pcId = args.Key;
                try {
                    //Monitor.Enter(loginLocker);  // 로그인/로그아웃 작업과 겹치지 않게 하기
                    Monitor.Enter(ManagerWebSocketKeys);
                    Monitor.Enter(LoggedinPcs);  // ★ 뉴모니터
                    Monitor.Enter(Guests);  // ★ 뉴모니터
                    // 로그인 목록에서 닫힌 Pc에 해당하는거 삭제
                    var listToDelete = new List<string>();
                    foreach (var pair in LoggedinPcs) {
                        if (pair.Value.pcId == pcId) {
                            listToDelete.Add(pair.Key);
                        }
                    }
                    int? pcNum = null;
                    lock (PcIdToNumber) {
                        if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                            pcNum = pcNumber;
                        }
                    }

                    foreach (var key in listToDelete) {
                        LoggedinPcs.Remove(key);
                        if (Guests.ContainsKey(key) == false) {
                            // 게스트가 아니면 최후 로그아웃까지 시켜준다
                            var task = GetResponseFromMainServerAsync(JObject.FromObject(new { work = "user_logout", place_id = PlaceId, user_id = key, auth_key = AuthorizationKey }).ToString());
                            SendNotification($"회원 종료 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {key})");
                        } else {
                            SendNotification($"비회원 종료 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {key})");
                        }
                    }
                    // ManagerWebSocketKeys
                    if (ManagerWebSocketKeys.Contains(pcId)) {
                        ManagerWebSocketKeys.Remove(pcId);
                    }
                } finally {
                    //Monitor.Exit(loginLocker);
                    Monitor.Exit(ManagerWebSocketKeys);
                    Monitor.Exit(LoggedinPcs);
                    Monitor.Exit(Guests);
                }
                lock (AuthorizationKeys) {
                    // 등록된 허가키 삭제
                    AuthorizationKeys.Remove(pcId);
                }
            }
        }

        //object loginLocker = new object();
        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            #region 기본 설정
            // 환경 지정
            if (env.IsDevelopment()) {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            } else {
                app.UseExceptionHandler("/Error");
            }
            // HTTPS 리다이렉트
            //var options = new RewriteOptions()
            //    .AddRedirectToHttps();
            //app.UseRewriter(options);
            // 세션 사용
            //app.UseSession();
            // 오류 처리
            app.Map("/Error", mapApp => {
                mapApp.Run(async context => {
                    context.Response.StatusCode = 500;
                    await WriteResponseWithContentLengthAsync(context, "Error!");
                });
            });
            // 연결자 콘솔 출력 
            app.Use(async (context, next) => {
                //Console.WriteLine($" *HostPath:{context.Request.Host}{context.Request.Path}\n *IP:{context.Connection.RemoteIpAddress}");
                await next();
            });
            // 파일 사용
            app.UseStaticFiles();
            // 웹 소켓 사용
            app.UseWebSockets(
                new WebSocketOptions {
                    KeepAliveInterval = TimeSpan.FromSeconds(30),
                    ReceiveBufferSize = 1024
                }
            );
            // 세션 검사 미들웨어
            //app.Use(async (context, next) => {
            //    if (context.Session.IsAvailable == false) {
            //        context.Response.StatusCode = StatusCodes.Status400BadRequest;
            //        await WriteResponseWithContentLengthAsync(context, "Sessions not available.");
            //    } else {
            //        await next();
            //    }
            //});
            #endregion

            // 웹 소켓 미들웨어 (항상 JSON으로 통신, 연결 전 오류는 400 코드 평문, 연결 후 오류는 StatusDescription으로)
            // PC -> 로컬서버(This)
            app.Map("/ws", wsApp => {
                wsApp.Run(async context => {
                    if (context.WebSockets.IsWebSocketRequest/* && context.Request.IsHttps*/) { // ws://
                        WebSocket webSocket = null;
                        WebSocketReceiveResult receiveResult = null;
                        var buffer = new byte[1024];

                        // Accept 후 최초 메시지 Receive
                        try {
                            webSocket = await context.WebSockets.AcceptWebSocketAsync();
                            receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                        } catch { }

                        // Receive 성공 시
                        if (receiveResult != null && receiveResult.CloseStatus.HasValue == false) {
                            try {
                                // 단문 Text JSON 메시지 수신 (id)
                                if (receiveResult.MessageType != WebSocketMessageType.Text || receiveResult.EndOfMessage == false) {
                                    throw new Exception("Bad request.");
                                }
                                var reqJobj = JObject.Parse(Encoding.UTF8.GetString(buffer, 0, receiveResult.Count));
                                var pcId = reqJobj["pc_id"].ToString();

                                // 허가 키 생성
                                var authKey = new Random().Next();
                                lock (AuthorizationKeys) {
                                    // 이미 로그인되었는지 확인 (허가키 등록된게 있나 확인)
                                    if (AuthorizationKeys.ContainsKey(pcId)) {
                                        throw new Exception("Logged in on another device.");
                                    }
                                    // 허가 키 등록
                                    AuthorizationKeys[pcId] = authKey;
                                }

                                // 매니저에 등록 후 수신 루프 실행
                                Console.WriteLine($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {pcId} Connected.");
                                lock (ManagerWebSocketKeys) {
                                    if (reqJobj.TryGetValue("agent", out var value) == true && value.ToString() == "manager") {
                                        ManagerWebSocketKeys.Add(pcId);
                                    }
                                }
                                var pcNumber = 0;
                                PcIdToNumber.TryGetValue(pcId, out pcNumber);
                                var addResult = await WebSocketManager.AddAndLoop(pcId, webSocket, JObject.FromObject(new { result = "ok", auth_key = authKey, place_name = PlaceName, pc_number = pcNumber, fare_per_hour = FarePerHour }).ToString());
                                if (addResult == false) {
                                    throw new Exception("Already logged in.");
                                }
                            } catch (Exception e) {
                                try { await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, e.Message, CancellationToken.None); } catch { }
                            }
                        } else {
                            try { await webSocket.CloseAsync(receiveResult.CloseStatus.Value, receiveResult.CloseStatusDescription, CancellationToken.None); } catch { }
                        }
                    } else {
                        context.Response.StatusCode = StatusCodes.Status400BadRequest;
                        await WriteResponseWithContentLengthAsync(context, "Bad request.");
                    }
                });
            });

            app.Map("/test", apiApp => {
                apiApp.Run(async context => {
                    var response = JObject.FromObject(new { result = "ok", pc_map = PcMapJson }).ToString();
                    //Console.WriteLine(PcMapJson.Length);
                    //Console.WriteLine(response.Length);
                    //Console.WriteLine(response.Substring(response.Length - 100));
                    //Console.WriteLine("Good!");
                    await WriteResponseWithContentLengthAsync(context, response);
                });
            });

            // API 미들웨어 (항상 JSON으로 통신, 오류도 200 또는 500코드로 JSON)
            // PC -> 로컬서버(This)
            app.Map("/api", apiApp => {
                apiApp.Run(async context => {
                    try {
                        // 응답 콘텐츠 타입
                        context.Response.ContentType = "application/json";
                        // 요청(HTTP POST Body) 읽기
                        string body;
                        using (var sr = new StreamReader(context.Request.Body)) {
                            body = sr.ReadToEnd();
                        }
                        // 요청 JSON 역직렬화
                        JObject reqJobj;
                        try {
                            reqJobj = JObject.Parse(body);
                            // 필수요소 검증, work, pc_id, auth_key
                            if (reqJobj.ContainsKey("work") == false || reqJobj.ContainsKey("pc_id") == false || reqJobj.ContainsKey("auth_key") == false) {
                                throw new Exception();
                            }
                        } catch {
                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Bad request." }).ToString());
                            return;
                        }
                        // 필수요소 추출
                        var work = reqJobj["work"].ToString();
                        var pcId = reqJobj["pc_id"].ToString();
                        var authKey = reqJobj["auth_key"].ToObject<int>();
                        // 웹소켓 연결되었나 확인
                        if (AuthorizationKeys.TryGetValue(pcId, out var correctingAuthKey) == false) {
                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "WebSocket is not connected." }).ToString());
                            return;
                        }
                        // 웹소켓 Auth Key 일치 확인
                        if (authKey != correctingAuthKey) {
                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Authorization key is mismatching." }).ToString());
                            return;
                        }

                        // Work로 분기
                        if (ManagerWebSocketKeys.Contains(pcId)) {
                            // 매니저인 경우
                            if (work != "get_using_pc_list" && work != "guest_ping" && work != "user_ping") {
                                Console.WriteLine($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {work} API was requested by {pcId}");
                            }
                            switch (work) {
                                case "get_sales": {
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "get_sales", from=reqJobj["from"], to=reqJobj["to"], place_id = PlaceId, auth_key = AuthorizationKey }).ToString());
                                    if (mainServerResponseString == null) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                        break;
                                    }
                                    await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                    break;
                                }
                                case "get_recent_notifications": {
                                    var resultJObject = new JObject();
                                    resultJObject["result"] = "ok";

                                    var list = new List<JToken>();
                                    lock (RecentNotifications) {
                                        foreach (var notification in RecentNotifications) {
                                            var tkn = JToken.FromObject(new { time = notification.time, message = notification.message });
                                            list.Add(tkn);
                                        }
                                    }

                                    if (list.Count > 0) {
                                        resultJObject["list"] = JToken.FromObject(list);
                                    }

                                    await WriteResponseWithContentLengthAsync(context, resultJObject.ToString());
                                    break;
                                }
                                case "get_pc_map": {
                                    /*if (string.IsNullOrWhiteSpace(PcMapJson)) {
                                        // 메인서버에 요청
                                        var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "get_pc_map", place_id = PlaceId, auth_key = AuthorizationKey }).ToString());
                                        if (mainServerResponseString == null) {
                                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                            break;
                                        }
                                        var mainServerResponseJObject = JObject.Parse(mainServerResponseString);
                                        // ok시
                                        if (mainServerResponseJObject["result"].ToString() == "ok") {
                                            PcMapJson = mainServerResponseJObject["pc_map"].ToString();
                                            var jobj = JObject.Parse(PcMapJson.ToString());
                                            lock (PcIdNumber) {
                                                PcIdNumber.Clear();
                                                foreach (var tkn in jobj) {
                                                    PcIdNumber[tkn.Key] = tkn.Value["num"].ToObject<int>();
                                                }
                                            }
                                            // 알림 송신
                                            //SendNotification($"회원 사용 시작 ({pcId}, {userId})");
                                        }
                                        // 메인서버 응답 그대로 돌려주기
                                        await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                        break;
                                    } else */{
                                        // ok 및 map 응답
                                        
                                        var response = JObject.FromObject(new { result = "ok", pc_map = PcMapJson }).ToString();
                                        //Console.WriteLine(PcMapJson.Length);
                                        //Console.WriteLine(response.Length);
                                        //Console.WriteLine(response.Substring(response.Length - 100));
                                        //Console.WriteLine("Good!");
                                        await WriteResponseWithContentLengthAsync(context, response);
                                        break;
                                    }
                                }
                                case "set_pc_map": {
                                    reqJobj.TryGetValue("pc_map", out var map);
                                    try {
                                        if (map == null) {
                                            throw new Exception();
                                        }
                                        var jobj = JObject.Parse(map.ToString());
                                        lock (PcIdNumber) {
                                            PcIdNumber.Clear();
                                            foreach (var tkn in jobj) {
                                                PcIdNumber[tkn.Key] = tkn.Value["num"].ToObject<int>();
                                            }
                                        }
                                    } catch {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Invalid map." }).ToString());
                                        break;
                                    }

                                    // 메인서버에 요청 오류미검증
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "set_pc_map", place_id = PlaceId, auth_key = AuthorizationKey, pc_map = map }).ToString());
                                    if (mainServerResponseString == null) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                        break;
                                    }

                                    // 맵 저장
                                    PcMapJson = map.ToString();

                                    // 메인서버에 사용중인 PC 목록 전달
                                    await SendUsingPcNumbers();

                                    // 맵 변했음을 알리기
                                    string[] allKeys;
                                    lock (ManagerWebSocketKeys) {
                                        allKeys = ManagerWebSocketKeys.ToArray();
                                    }
                                    Parallel.ForEach(allKeys, key => {
                                        WebSocketManager.SendMessageAsync(key, JObject.FromObject(new { type = "set_pc_map", pc_map = map.ToString() }).ToString());
                                    });
                                    //foreach (var key in allKeys) {
                                        //WebSocketManager.SendMessageAsync(key, JObject.FromObject(new { type= "set_pc_map", pc_map=map.ToString()}).ToString());
                                    //}

                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                    break;
                                }
                                case "set_fare_per_hour": {
                                    if (reqJobj.TryGetValue("fare_per_hour", out var farePerHour) && farePerHour.ToObject<double>() >= .0) {
                                        // 메인서버에 알리기
                                        var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "set_fare_per_hour", fare_per_hour=farePerHour, place_id = PlaceId, auth_key = AuthorizationKey }).ToString());
                                        if (mainServerResponseString == null) {
                                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                            break;
                                        }

                                        FarePerHour = farePerHour.ToObject<double>();

                                        // 요금 변했음을 알리기
                                        string[] allKeys;
                                        lock (ManagerWebSocketKeys) {
                                            allKeys = ManagerWebSocketKeys.ToArray();
                                        }

                                        Parallel.ForEach(allKeys, key => {
                                            WebSocketManager.SendMessageAsync(key, JObject.FromObject(new { type = "set_fare_per_hour", fare_per_hour = farePerHour.ToObject<double>() }).ToString());
                                        });
                                    }
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                    break;
                                }
                                case "get_using_pc_list": {// 로그인된 PC 리스트 응답
                                    var resultJObject = new JObject();
                                    resultJObject["result"] = "ok";

                                    lock (LoggedinPcs) {
                                        lock (Guests) {
                                            var list = new List<JToken>();
                                            foreach (var key in WebSocketManager.Keys) {
                                                if (ManagerWebSocketKeys.Contains(key) || key == MainServerWebSocketKey) {
                                                    continue;
                                                }
                                                var token = JToken.FromObject(new { pc_id = key });
                                                foreach (var pair in LoggedinPcs) {
                                                    var loginPcId = pair.Value.pcId;
                                                    if (loginPcId == key) {
                                                        var loginUserId = pair.Key;
                                                        var loginTime = pair.Value.loginTime;
                                                        token["user_id"] = loginUserId;
                                                        token["user_is_guest"] = Guests.ContainsKey(loginUserId);
                                                        token["login_time"] = loginTime;
                                                        token["total_paid_cash"] = pair.Value.totalPaidCash;
                                                        token["cash_left"] = pair.Value.remainingCash;

                                                        break;
                                                    }
                                                }
                                                list.Add(token);
                                            }

                                            if (TestList.Count > 0) {
                                                var now = DateTime.Now;
                                                foreach (var token in TestList) {
                                                    if (string.IsNullOrEmpty(token["user_id"]?.ToString()) == false) {
                                                        var lastPaidTime = token["last_paid_time"].ToObject<DateTime>();
                                                        var cashLeft = token["cash_left"].ToObject<double>();
                                                        var totalPaidCash = token["total_paid_cash"].ToObject<double>();
                                                        var fare = FarePerHour * (now - lastPaidTime).TotalHours;
                                                        totalPaidCash += fare;
                                                        cashLeft -= fare;
                                                        token["total_paid_cash"] = totalPaidCash;
                                                        token["cash_left"] = cashLeft;
                                                        token["last_paid_time"] = now;
                                                    }
                                                }
                                                list.AddRange(TestList);
                                            }

                                            if (list.Count > 0) {
                                                resultJObject["list"] = JToken.FromObject(list);
                                            }
                                        }
                                    }
                                    
                                    
                                    await WriteResponseWithContentLengthAsync(context, resultJObject.ToString());
                                    break;
                                }
                                case "get_guest_list": {
                                    var resultJObject = new JObject();
                                    resultJObject["result"] = "ok";

                                    var list = new List<JToken>();
                                    lock (LoggedinPcs) {
                                        lock (Guests) {
                                            foreach (var pair in Guests) {
                                                var guestId = pair.Key;
                                                var cash = pair.Value;
                                                var isLoggedIn = LoggedinPcs.ContainsKey(guestId);
                                                if (cash > 0.0) {
                                                    list.Add(JToken.FromObject(new { guest_id = pair.Key, cash = cash, is_logged_in = isLoggedIn }));
                                                }
                                            }
                                        }
                                    }

                                    if (list.Count > 0) {
                                        resultJObject["list"] = JToken.FromObject(list);
                                    }
                                    // 게스트 리스트 응답
                                    await WriteResponseWithContentLengthAsync(context, resultJObject.ToString());
                                    break;
                                }
                                case "guest_add": {
                                    reqJobj.TryGetValue("cash", out var cashTkn);
                                    var cash = cashTkn?.ToObject<double>() ?? 0.0;
                                    var resultJObject = new JObject();
                                    resultJObject["result"] = "ok";

                                    // 임의의 ID로 게스트 생성하여 추가
                                    var randomId = string.Empty;
                                    lock (LoggedinPcs) {
                                        lock (Guests) {
                                            while (resultJObject.ContainsKey("guest_id") == false) {
                                                randomId = "G-" + new Random().Next(0, 9999).ToString("0000");
                                                if (Guests.ContainsKey(randomId) == false && LoggedinPcs.ContainsKey(randomId) == false) {
                                                    // LoggedInPcs 확인한 이유는 혹시 게스트id가 사라진 것을 모른 
                                                    resultJObject["guest_id"] = randomId;
                                                    Guests[randomId] = cash;
                                                }
                                            }
                                        }
                                    }

                                    // 알림 송신
                                    SendNotification($"비회원 추가 ({randomId})");

                                    // 생성된 임의ID의 게스트 응답
                                    await WriteResponseWithContentLengthAsync(context, resultJObject.ToString());
                                    break;
                                }
                                case "guest_delete": {
                                    var guestId = "";
                                    lock (Guests) {
                                        lock (LoggedinPcs) {
                                            guestId = reqJobj["guest_id"].ToString();
                                            Guests.Remove(guestId);
                                            LoggedinPcs.Remove(guestId);
                                        }
                                    }
                                    // 알림 송신
                                    SendNotification($"비회원 삭제 ({guestId})");
                                    // ok 응답
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                    break;
                                }
                                case "guest_cash_charge": {
                                    lock (Guests) {
                                        // 게스트 충전
                                        var guestId = reqJobj["guest_id"].ToString();
                                        var guestCashCharging = reqJobj["cash_charging"].ToObject<double>();
                                        if (Guests.ContainsKey(guestId) == false) {
                                            Guests[guestId] = guestCashCharging;
                                        } else {
                                            Guests[guestId] += guestCashCharging;
                                        }

                                        // Cash 0이하 게스트 삭제
                                        var listToRemove = new List<string>();
                                        foreach (var pair in Guests) {
                                            if (pair.Value <= 0.0) {
                                                listToRemove.Add(pair.Key);
                                            }
                                        }
                                        foreach (var key in listToRemove) {
                                            Guests.Remove(key);  // 어차피 use 요청하면 로그아웃시키므로, 로그아웃 코드 불필요
                                        }
                                    }

                                    // ok 응답
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                    break;
                                }
                            }
                        } else {
                            // 고객PC인 경우
                            switch (work) {
                                case "user_login": {
                                    // 요청에서 ID PW 획득
                                    var userId = reqJobj["user_id"].ToString();
                                    var userPassword = reqJobj["user_password"].ToString();
                                    // 이미 로그인된 PC인지 확인
                                    var alreadyLoggedIn = false;
                                    lock (LoggedinPcs) {
                                        foreach (var obj in LoggedinPcs) {
                                            if (obj.Value.pcId == pcId) {
                                                // 이미 로그인된 PC면 에러
                                                alreadyLoggedIn = true;
                                                break;
                                            }
                                        }
                                    }
                                    if (alreadyLoggedIn) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Already logged in.", is_guest = false, user_id = userId, user_total_cash = 0.0 }).ToString());
                                        break;
                                    }
                                    // 메인서버에 로그인 유효 확인
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "user_login", place_id = PlaceId, user_id = userId, user_password = userPassword, auth_key = AuthorizationKey }).ToString());
                                    if (mainServerResponseString == null) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                        break;
                                    }
                                    var mainServerResponseJObject = JObject.Parse(mainServerResponseString);
                                    // 로그인 ok시 로그인 목록 등록
                                    if (mainServerResponseJObject["result"].ToString() == "ok") {
                                        lock (LoggedinPcs) {
                                            LoggedinPcs[userId] = (pcId, mainServerResponseJObject["login_time"].ToObject<DateTime>(), default(DateTime), 0.0, mainServerResponseJObject["user_total_cash"].ToObject<double>());
                                        }
                                        int? pcNum = null;
                                        lock (PcIdToNumber) {
                                            if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                                                pcNum = pcNumber;
                                            }
                                        }
                                        // 알림 송신
                                        SendNotification($"회원 시작 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {userId})");
                                    }
                                    // 메인서버 응답 그대로 돌려주기
                                    await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                    break;
                                }
                                case "guest_login": {
                                    // 요청에서 ID 획득
                                    var guestId = reqJobj["guest_id"].ToString();
                                    // 이미 로그인된 PC거나 다른 PC에 로그인되어있는지 확인
                                    var alreadyLoggedIn = false;
                                    var loggedInOnAnotherPc = false;
                                    var notExisting = false;
                                    var cashLeft = 0.0;
                                    var now = DateTime.Now;
                                    //var guestTotalCash = 0.0;
                                    lock (LoggedinPcs) {
                                        lock (Guests) {
                                            // 이미 로그인된 PC인지 확인
                                            foreach (var obj in LoggedinPcs) {
                                                if (obj.Value.pcId == pcId) {
                                                    //guestTotalCash = Guests[guestId];
                                                    alreadyLoggedIn = true;
                                                    break;
                                                }
                                            }
                                            // 이미 다른 PC에서 로그인되었는지 확인
                                            if (LoggedinPcs.ContainsKey(guestId)) {
                                                loggedInOnAnotherPc = true;
                                            }
                                            if (alreadyLoggedIn == false && loggedInOnAnotherPc == false) {
                                                // 아이디 존재하는지 확인
                                                if (Guests.TryGetValue(guestId, out cashLeft) == false || cashLeft <= 0.0) {
                                                    notExisting = true;
                                                }
                                                if (notExisting == false) {
                                                    // 로그인 처리
                                                    
                                                    LoggedinPcs[guestId] = (pcId, now, now, 0.0, cashLeft);
                                                }
                                            }
                                        }
                                    }
                                    if (alreadyLoggedIn) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Already logged in.", is_guest = true, guest_id = guestId, guest_total_cash = cashLeft }).ToString());
                                        break;
                                    }
                                    if (loggedInOnAnotherPc) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Logged in on other PC." }).ToString());
                                        break;
                                    }
                                    if (notExisting) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Wrong ID." }).ToString());
                                        break;
                                    }
                                    int? pcNum = null;
                                    lock (PcIdToNumber) {
                                        if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                                            pcNum = pcNumber;
                                        }
                                    }
                                    // 알림 송신
                                    SendNotification($"비회원 시작 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {guestId})");

                                    // ok 응답
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", login_time = now, fare_per_hour=FarePerHour, guest_total_cash = cashLeft }).ToString());
                                    break;
                                }
                                case "user_ping": {
                                    var userId = reqJobj["user_id"].ToString();
                                    // 이 PC에 해당 계정이 로그인 잘 되어있는지 확인
                                    var notLoggedIn = false;
                                    lock (LoggedinPcs) {
                                        if (LoggedinPcs.TryGetValue(userId, out var loggedInPc) == false || pcId != loggedInPc.pcId) {
                                            notLoggedIn = true;
                                        }
                                    }
                                    if (notLoggedIn) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Not logged in." }).ToString());
                                        break;
                                    }
                                    // 메인서버에 캐시 사용 확인
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "user_ping", user_id = userId, place_id = PlaceId, auth_key = AuthorizationKey }).ToString());
                                    if (mainServerResponseString == null) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                        break;
                                    }
                                    var mainServerResponseJObject = JObject.Parse(mainServerResponseString);
                                    if (mainServerResponseJObject["result"].ToString() == "error") {
                                        // 오류 시
                                        var error = mainServerResponseJObject["error"].ToString();
                                        if (error == "Cash is zero." || error == "Not logged in.") {
                                            // 로그아웃된 오류일시
                                            int? pcNum = null;
                                            lock (PcIdToNumber) {
                                                if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                                                    pcNum = pcNumber;
                                                }
                                            }
                                            lock (LoggedinPcs) {
                                                if (LoggedinPcs.TryGetValue(userId, out var tuple) && tuple.pcId == pcId) {
                                                    LoggedinPcs.Remove(userId);
                                                    SendNotification($"회원 종료 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {userId})");
                                                }
                                            }
                                        }
                                    } else {
                                        lock (LoggedinPcs) {
                                            if (LoggedinPcs.TryGetValue(userId, out var loggedInPc) == true || pcId == loggedInPc.pcId) {
                                                loggedInPc.totalPaidCash = mainServerResponseJObject["total_paid_cash"].ToObject<double>();
                                                loggedInPc.remainingCash = mainServerResponseJObject["user_total_cash"].ToObject<double>();
                                                LoggedinPcs[userId] = loggedInPc;
                                            }
                                        }
                                    }
                                    // 메인서버 응답 그대로 돌려주기
                                    await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                    break;
                                }
                                case "guest_ping": {
                                    var guestId = reqJobj["guest_id"].ToString();

                                    // 이 PC에 해당 계정이 로그인 잘 되어있는지 확인
                                    var notLoggedIn = false;
                                    var cashIsZero = false;
                                    var guestTotalCash = 0.0;
                                    lock (LoggedinPcs) {
                                        lock (Guests) {
                                            if (LoggedinPcs.TryGetValue(guestId, out var loggedInPc) == false || pcId != loggedInPc.pcId) {
                                                notLoggedIn = true;
                                            }
                                            if (notLoggedIn == false) {
                                                // 캐시 사용
                                                if (Guests.ContainsKey(guestId) == false) {
                                                    cashIsZero = true;
                                                } else {
                                                    var now = DateTime.Now;
                                                    var guestCashUsed = (now - loggedInPc.lastPaidTime).TotalHours * FarePerHour;
                                                    guestTotalCash = Guests[guestId] -= guestCashUsed;
                                                    loggedInPc.lastPaidTime = now;
                                                    loggedInPc.totalPaidCash += guestCashUsed;
                                                    loggedInPc.remainingCash = guestTotalCash;
                                                    LoggedinPcs[guestId] = loggedInPc;
                                                    if (guestTotalCash <= 0) {
                                                        Guests.Remove(guestId);
                                                        cashIsZero = true;
                                                    }
                                                }
                                                if (cashIsZero) {
                                                    int? pcNum = null;
                                                    lock (PcIdToNumber) {
                                                        if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                                                            pcNum = pcNumber;
                                                        }
                                                    }
                                                    if (LoggedinPcs.TryGetValue(guestId, out var tuple) && tuple.pcId == pcId) {
                                                        LoggedinPcs.Remove(guestId);
                                                        SendNotification($"비회원 종료 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {guestId})");
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    if (notLoggedIn) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Not logged in." }).ToString());
                                        break;
                                    }
                                    if (cashIsZero) {
                                        // error 응답
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Cash is zero." }).ToString());
                                    } else {
                                        // ok 응답
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", guest_total_cash = guestTotalCash }).ToString());
                                    }
                                    break;
                                }
                                case "user_logout": {
                                    // 요청에서 ID 획득
                                    var userId = reqJobj["user_id"].ToString();
                                    // 로그인 이미 풀려있지 않은지 확인
                                    var alreadyLoggedOut = true;
                                    lock (LoggedinPcs) {
                                        foreach (var obj in LoggedinPcs) {
                                            if (obj.Value.pcId == pcId) {
                                                alreadyLoggedOut = false;
                                                break;
                                            }
                                        }
                                    }
                                    // 이미 로그아웃 되어있었으면 ok 응답
                                    if (alreadyLoggedOut) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                        break;
                                    }
                                    // 메인서버에 로그아웃 요청
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "user_logout", place_id = PlaceId, user_id = userId, auth_key = AuthorizationKey }).ToString());
                                    if (mainServerResponseString == null) {
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "The main server is not responding." }).ToString());
                                        break;
                                    }
                                    var mainServerResponseJObject = JObject.Parse(mainServerResponseString);
                                    // 로그아웃 ok시 로그인 목록에서 삭제
                                    if (mainServerResponseJObject["result"].ToString() == "ok") {
                                        int? pcNum = null;
                                        lock (PcIdToNumber) {
                                            if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                                                pcNum = pcNumber;
                                            }
                                        }
                                        lock (LoggedinPcs) {
                                            if (LoggedinPcs.TryGetValue(userId, out var loggedInPc) == true && loggedInPc.pcId == pcId) {
                                                LoggedinPcs.Remove(userId);
                                                SendNotification($"회원 종료 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {userId})");
                                            }
                                        }
                                    }

                                    // 알림 송신
                                    //SendNotification($"회원 사용 종료 ({pcId}, {userId})");

                                    // ok 응답
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new {
                                        result = "ok"
                                    }).ToString());
                                    break;
                                }
                                case "guest_logout": {
                                    // 요청에서 ID 획득
                                    var guestId = reqJobj["guest_id"].ToString();
                                    
                                    lock (LoggedinPcs) {
                                        // 로그인 이미 풀려있지 않은지 확인
                                        var alreadyLoggedOut = true;
                                        foreach (var obj in LoggedinPcs) {
                                            if (obj.Value.pcId == pcId) {
                                                alreadyLoggedOut = false;
                                                break;
                                            }
                                        }
                                        if (alreadyLoggedOut == false) {
                                            // 로그아웃 처리 (로그인 목록에서 삭제)
                                            int? pcNum = null;
                                            lock (PcIdToNumber) {
                                                if (PcIdToNumber.TryGetValue(pcId, out var pcNumber)) {
                                                    pcNum = pcNumber;
                                                }
                                            }
                                            if (LoggedinPcs.TryGetValue(guestId, out var loggedInPc) == true && loggedInPc.pcId == pcId) {
                                                LoggedinPcs.Remove(guestId);
                                                SendNotification($"비회원 종료 ({(pcNum.HasValue ? pcNum.Value.ToString("000") : pcId)}, {guestId})");
                                            }
                                        }
                                    }

                                    // 알림 송신
                                    //SendNotification($"비회원 사용 종료 ({pcId}, {guestId})");

                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                    break;
                                }
                                case "user_review": {
                                    var userId = reqJobj["user_id"].ToString();
                                    var nickname= reqJobj["nickname"].ToString();
                                    var reviewScore = reqJobj["score"].ToObject<double>();
                                    var reviewText = reqJobj["text"].ToString();
                                    Console.WriteLine($"{userId}, {nickname}, {reviewScore}, {reviewText}");
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "user_review", place_id = PlaceId, auth_key = AuthorizationKey, user_id = userId, nickname, score=reviewScore, text=reviewText}).ToString());

                                    // 메인서버 응답 그대로 돌려주기
                                    await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                    break;
                                }
                                case "get_order_menu": {
                                    var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "get_order_menu", place_id = PlaceId, auth_key = AuthorizationKey }).ToString());

                                    // 메인서버 응답 그대로 돌려주기
                                    await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                    break;
                                }
                                case "submit_order": {
                                    var userId = reqJobj["user_id"].ToString();
                                    var orderListJson = reqJobj["order_list"].ToString();
                                    var totalPrice = reqJobj["total_price"].ToObject<double>();
                                    var memo = reqJobj["memo"].ToObject<string>();
                                    var paymentMethod = reqJobj["payment_method"].ToString();
                                    var pcNumber = 0;
                                    lock (PcIdToNumber) {
                                        pcNumber = PcIdToNumber[pcId];
                                    }
                                    
                                    if (paymentMethod == "point") {
                                        var mainServerResponseString = await GetResponseFromMainServerAsync(JObject.FromObject(new { work = "user_order", user_id = userId, total_price = totalPrice, place_id = PlaceId, auth_key = AuthorizationKey }).ToString());
                                        var obj = JObject.Parse(mainServerResponseString);
                                        if (obj["result"].ToString() == "ok") {
                                            // 관리PC에 방송
                                            string[] allKeys;
                                            lock (ManagerWebSocketKeys) {
                                                allKeys = ManagerWebSocketKeys.ToArray();
                                            }
                                            Parallel.ForEach(allKeys, key => {
                                                WebSocketManager.SendMessageAsync(key, JObject.FromObject(new { type = "submit_order", pc_number = pcNumber, payment_method = paymentMethod, order_list = reqJobj["order_list"] }).ToString());
                                            });
                                        }
                                        // 메인서버 응답 그대로 돌려주기
                                        await WriteResponseWithContentLengthAsync(context, mainServerResponseString);
                                        break;
                                    } else {
                                        // 관리PC에 방송
                                        string[] allKeys;
                                        lock (ManagerWebSocketKeys) {
                                            allKeys = ManagerWebSocketKeys.ToArray();
                                        }
                                        Parallel.ForEach(allKeys, key => {
                                            WebSocketManager.SendMessageAsync(key, JObject.FromObject(new { type = "submit_order", pc_number = pcNumber, payment_method = paymentMethod, order_list = reqJobj["order_list"] }).ToString());
                                        });

                                        // ok 반환
                                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                        break;
                                    }
                                    
                                }
                            }
                        }

                        // 요청 온김에 사용중 PC send 처리
                        var t = SendUsingPcNumbers();
                    } catch (Exception exc) {
                        Console.WriteLine($"** {exc.Message}\r\n{exc.StackTrace}");
                        //context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Internal server error." }).ToString());
                    }
                });
            });
        }

        /// <summary>
        /// HTTP Context에 응답 문자열을 씁니다.
        /// </summary>
        /// <param name="context">쓰기 작업을 할 HTTP Context</param>
        /// <param name="text">쓰기 작업을 할 문자열</param>
        private Task WriteResponseWithContentLengthAsync(HttpContext context, string text) =>
            Task.Factory.StartNew(arg => {
                var tuple = ((HttpContext context, string text))arg;
                var buffer = Encoding.UTF8.GetBytes(tuple.text);
                tuple.context.Response.ContentLength = buffer.Length;
                using (var stream = tuple.context.Response.Body) {
                    stream.Write(buffer, 0, buffer.Length);
                }
            }, (context, text));

        CookieContainer cookieContainer = new CookieContainer();
        private Task<string> GetResponseFromMainServerAsync(string requestBody) => Task.Factory.StartNew(_requestBody => {
            // 요청하기
            HttpWebRequest request = WebRequest.Create(MainServerWebApiUri) as HttpWebRequest;
            request.CookieContainer = cookieContainer;
            request.Method = "POST";
            var requestBuffer = Encoding.UTF8.GetBytes(_requestBody as string);
            request.ContentLength = requestBuffer.Length;
            request.ContentType = "application/x-www-form-urlencoded";
            using (var stream = request.GetRequestStream()) {
                stream.Write(requestBuffer, 0, requestBuffer.Length);
            }
            // 응답받기
            try {
                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 result;
            } catch {
                return null;
            }
        }, requestBody);
    }
}
