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 JtkMainServer.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;

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

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

        /// <summary> 로그인된 유저의 PC방 관리할 딕셔너리, Key: UserID, Value: PlaceID </summary>
        private Dictionary<string, (string placeId, DateTime loginTime, DateTime lastPaidTime, double totalPaid)> UserLoggedInPlaces = new Dictionary<string, (string placeId, DateTime loginTime, DateTime lastPaidTime, double totalPaid)>();

        /// <summary> 초기 설정 </summary>
        public Startup(IHostingEnvironment environment, IConfiguration configuration) {
            HostingEnvironment = environment;
            Configuration = configuration;
            if (Configuration.GetValue<string>("DbType").ToLower().Trim() == "mysql") {
                DbManager.ConnectionString = Configuration.GetConnectionString("MySql");
                DbManager.Type = DbManager.DbType.MySql;
            } else {
                DbManager.ConnectionString = Configuration.GetConnectionString("Sqlite");
                DbManager.Type = DbManager.DbType.Sqlite;
            }
            //LoginManager.PcLoginKeepAliveInterval = TimeSpan.FromSeconds(Configuration.GetValue<int>("PcLoginKeepAliveInterval"));
            WebSocketManager.OnReceive += WebSocketReceive;
            WebSocketManager.OnClosed += WebSocketClosed;

            PlacePaymentExpenseLoop();
        }

        /// <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"));
            });
        }

        // 웹소켓 수신 처리 (Place 로컬 서버의 웹소켓)
        private void WebSocketReceive(object sender, WebSocketManagerEventArgs args) {
            if (args.MessageType != WebSocketMessageType.Text) {
                return;
            }
            Console.WriteLine($"[WebSocket] {args.TextMessage} Received.");
        }

        // 웹소켓 닫기 처리 (Place 로컬 서버의 웹소켓)
        private void WebSocketClosed(object sender, WebSocketManagerEventArgs args) {
            Console.WriteLine($"[WebSocket] {args.Key} Closed.");
            var placeId = args.Key;
            lock (AuthorizationKeys) {
                lock (UserLoggedInPlaces) {
                    // 로그인 목록에서 닫힌 PlaceID에 해당하는거 다 삭제
                    var listToDelete = new List<string>();
                    foreach (var pair in UserLoggedInPlaces) {
                        if (pair.Value.placeId == placeId) {
                            listToDelete.Add(pair.Key);
                        }
                    }
                    foreach (var key in listToDelete) {
                        // 결제 안한것 최후정산까지 하고 삭제해
                        var userId = key;
                        DoUserPingFinalPayment(userId, placeId, true);
                        UserLoggedInPlaces.Remove(key);
                    }
                }

                // 등록된 허가 키 삭제
                AuthorizationKeys.Remove(placeId);
            }
        }


        // 마지막 지급일시와 현재일시사이에 지급타임이 걸렸으면 지급해주기 (가맹점마다 지급타임 다름)
        public Task PlacePaymentExpenseLoop() => Task.Factory.StartNew(() => {
            bool TimeBetween(TimeSpan dest, DateTime start, DateTime end) {
                if (start.Date == end.Date) {
                    var destDate = (start.Date + dest);
                    return start < destDate && destDate < end;
                } else {
                    if (start.Hour == dest.Hours && start.Minute < dest.Minutes || start.Hour < dest.Hours) {
                        return true;
                    }
                    if (dest.Hours == end.Hour && dest.Minutes < end.Minute || dest.Hours < end.Hour) {
                        return true;
                    }
                }
                return false;
            }
            //DateTime? lastLoopTime = null;
            while (true) {
                

                var now = DateTime.Now;
                /*if (lastLoopTime == null) {
                    var list = DbManager.GetSalesPlacePaymentExpensesList(DateTime.MinValue, now);
                    if (list.Length > 0) {
                        lastLoopTime = (from item in list select item.time).Max();
                    } else {
                        lastLoopTime = DateTime.MinValue;
                    }
                }*/

                var places = DbManager.GetAllPlaces();
                //var payments = DbManager.GetSalesPlacePaymentExpensesList(lastLoopTime.Value, now);  // 최적화를 위해 llt 삽입

                var payments = DbManager.GetSalesPlacePaymentExpensesList(DateTime.MinValue, now);  // 최적화를 위해 llt 삽입하려했지만 안해
                try {
                    foreach (var place in places) {
                        var paidTimes = (from item in payments where item.placeId == place.Id select item.time).ToArray();
                        var lastPaidTime = paidTimes.Length == 0 ? DateTime.MinValue : paidTimes.Max();//이것과 now사이에 place.hour/min이 있으면 pay하는 것이지
                        var payNow = false;
                        if (paidTimes.Length == 0) {
                            payNow = true;
                        } else {
                            payNow = TimeBetween(new TimeSpan(place.PaymentHour, place.PaymentMinute, 0), lastPaidTime, now);
                            /*
                            for (var time = lastPaidTime; time <= now; time += TimeSpan.FromDays(1)) {
                                if (time.Year == now.Year && time.Month == now.Month && time.Day == now.Day) {
                                    // 마지막
                                    if (time == lastPaidTime) {
                                        // 항목이 하나뿐인 경우 (보통 여기 해당)
                                        if ((time.Hour == place.PaymentHour && time.Minute < place.PaymentMinute || time.Hour < place.PaymentHour) && (place.PaymentHour == now.Hour && place.PaymentMinute < now.Minute)) {
                                            payNow = true;
                                            break;
                                        }
                                    } else {
                                        // 항목이 두개 이상인 경우 (하루 넘어가는 타임같은 경우)
                                        if (place.PaymentHour == now.Hour && place.PaymentMinute < now.Minute) {
                                            payNow = true;
                                            break;
                                        }
                                    }
                                } else if (time == lastPaidTime) {
                                    // 처음 (항목 두개이상)
                                    if (time.Hour == place.PaymentHour && time.Minute < place.PaymentMinute || time.Hour < place.PaymentHour) {
                                        payNow = true;
                                        break;
                                    }
                                } else {
                                    // 중간 (서버를 오랜만에 켠경우)
                                    payNow = true;
                                    break;
                                }
                            }*/
                        }

                        if (payNow) {
                            var logs = DbManager.GetPlacePingLogs(place.Id, lastPaidTime, now, true);
                            var total = 0.0;
                            foreach (var item in logs) {
                                total += item.totalCashUse;
                            }
                            total *= 1.0 - place.Royalty * 0.01;
                            DbManager.LogSalePlacePaymentExpense(now, place.Id, total);
                            Console.WriteLine($"[{now.ToString("yyyy-MM-dd HH:mm:ss")}] Paid ￦{total:N0} to place '{place.Name}'");
                        }
                    }
                } catch (Exception e) {
                    Console.WriteLine(e.ToString());
                }

                Task.Delay(TimeSpan.FromSeconds(30)).Wait();
            }
        });

        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($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {context.Connection.RemoteIpAddress} @ {context.Request.Host}{context.Request.Path}");
                await next();
            });
            // 파일 사용
            app.UseStaticFiles();
            // MVC 사용
            app.UseMvc();
            // 웹 소켓 사용
            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으로)
            // 로컬서버 -> 메인서버(This)
            app.Map("/ws", wsApp => {
                wsApp.Run(async context => {
                    if (context.WebSockets.IsWebSocketRequest && context.Request.IsHttps) { // wss://
                        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/pw)
                                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 placeId = reqJobj["place_id"].ToString();
                                var placePassword = reqJobj["place_password"].ToString();
                                var farePerHour = reqJobj["fare_per_hour"].ToObject<double>();

                                // 계정 일치 확인
                                var place = DbManager.GetPlace(placeId);
                                if (place == null || DbManager.ComparePasswords(placePassword, place.Password) == false) {
                                    throw new Exception("Wrong id or password.");
                                }

                                // PC Map
                                var pcMap = place.PcMap;

                                // 주문 메뉴
                                var orderMenu = place.OrderMenu;

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

                                // 요금 등록
                                PlaceManager.UpdateFarePerHour(placeId, farePerHour);

                                // 매니저에 등록 후 수신 루프 실행
                                Console.WriteLine($"[WebSocket] {placeId} Opened.");
                                var addResult = await WebSocketManager.AddAndLoop(placeId, webSocket, JObject.FromObject(new { result = "ok", auth_key = authKey, place_name = place.Name, pc_map = pcMap, order_menu = orderMenu }).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.");
                    }
                });
            });

            // API 미들웨어 (항상 JSON으로 통신, 오류도 200 또는 500코드로 JSON)
            // 로컬서버 -> 메인서버(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, place_id, auth_key
                            if (reqJobj.ContainsKey("work") == false || reqJobj.ContainsKey("place_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 placeId = reqJobj["place_id"].ToString();
                        var authKey = reqJobj["auth_key"].ToObject<int>();
                        // 웹소켓 연결되었나 확인
                        Monitor.Enter(AuthorizationKeys);
                        if (AuthorizationKeys.TryGetValue(placeId, out var correctingAuthkey) == false) {
                            Monitor.Exit(AuthorizationKeys);
                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "WebSocket is not connected." }).ToString());
                            return;
                        }
                        // 웹소켓 Auth Key 일치 확인
                        if (authKey != correctingAuthkey) {
                            Monitor.Exit(AuthorizationKeys);
                            await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Authorization key is mismatching." }).ToString());
                            return;
                        }
                        Monitor.Exit(AuthorizationKeys);
                        // Work로 분기
                        switch (work) {
                            case "user_login": {
                                // 요청에서 ID PW 획득
                                var userId = reqJobj["user_id"].ToString();
                                var userPassword = reqJobj["user_password"].ToString();
                                // DB에서 해당 User 획득
                                var user = DbManager.GetUser(userId);
                                // ID PW 확인
                                if (user == null || DbManager.ComparePasswords(userPassword, user.Password) == false) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Wrong ID or password." }).ToString());
                                    break;
                                }
                                // Cash 확인
                                if (user.Cash <= 0.0) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Cash is zero." }).ToString());
                                    break;
                                }
                                var now = DateTime.Now;
                                lock (UserLoggedInPlaces) {
                                    // 로그인 목록에 이미 있는지 확인
                                    if (UserLoggedInPlaces.TryGetValue(userId, out var tuple) == true && tuple.placeId != placeId) {
                                        // 이미 로그인되어있던 Place에게는 로그아웃하라고 명령
                                        WebSocketManager.SendMessageAsync(tuple.placeId, JObject.FromObject(new { work = "user_logout", user_id = userId }).ToString())/*.Wait()*/;
                                    }
                                    // 로그인 목록에 등록
                                    UserLoggedInPlaces[userId] = (placeId, now, now, 0);
                                }
                                // ok 응답
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", user_total_cash = user.Cash, fare_per_hour = PlaceManager.GetFarePerHour(placeId), login_time = now }).ToString());
                                break;
                            }
                            case "user_ping": {
                                // 요청에서 ID Cash 획득
                                var userId = reqJobj["user_id"].ToString();
                                var now = DateTime.Now;

                                // 로그인 목록을 확인해서 다른 Place에서 cash use 하려는건 아닌지 확인
                                bool notLoggedIn;
                                var userCashSpend = .0;
                                lock (UserLoggedInPlaces) {
                                    notLoggedIn = (UserLoggedInPlaces.TryGetValue(userId, out var tuple) == false || tuple.placeId != placeId);
                                    if (notLoggedIn == false) {
                                        userCashSpend = (now - tuple.lastPaidTime).TotalHours * PlaceManager.GetFarePerHour(placeId);
                                    }
                                }
                                if (notLoggedIn) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Not logged in." }).ToString());
                                    break;
                                }
                                // DB에서 Cash값 변경
                                var afterCash = DbManager.AdjustUserCash(userId, -userCashSpend);
                                // 성공 여부 확인
                                if (afterCash.HasValue == false) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Failed to spend cash." }).ToString());
                                    break;
                                }
                                var loginTime = default(DateTime);
                                var totalPaid = 0.0;
                                lock (UserLoggedInPlaces) {
                                    if (UserLoggedInPlaces.TryGetValue(userId, out var tuple) == true && tuple.placeId == placeId) {
                                        tuple.lastPaidTime = now;
                                        totalPaid = tuple.totalPaid += userCashSpend;
                                        loginTime = tuple.loginTime;
                                        UserLoggedInPlaces[userId] = tuple;
                                    }
                                }
                                // DB에 기록
                                if (loginTime != default(DateTime)) {
                                    DbManager.LogUserPing(loginTime, userId, placeId, totalPaid, now - loginTime);
                                }

                                if (afterCash <= 0.0) {
                                    // 최후까지 시간에 대해 결제 (로그인 목록 삭제도 겸함)
                                    //DoUserPingFinalPayment(userId, placeId);  // 돈없는데 뭔결제 
                                    // error 응답
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Cash is zero." }).ToString());
                                    break;
                                } else {
                                    // ok 응답
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", user_total_cash = afterCash.Value, total_paid_cash = totalPaid, fare_per_hour = PlaceManager.GetFarePerHour(placeId) }).ToString());
                                    break;
                                }
                            }
                            case "user_logout": {
                                // 요청에서 ID 획득
                                var userId = reqJobj["user_id"].ToString();
                                // 최후까지 시간에 대해 결제 (로그인 목록 삭제도 겸함)
                                DoUserPingFinalPayment(userId, placeId);
                                // ok 응답
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                break;
                            }
                            case "user_review": {
                                // ★ 인증생략
                                // 요청에서 ID 획득
                                var userId = reqJobj["user_id"].ToString();
                                // 요청에서 ID 획득
                                var nickname = reqJobj["nickname"].ToString();
                                // 요청에서 점수 획득
                                var reviewScore = reqJobj["score"].ToObject<double>();
                                // 요청에서 리뷰 획득
                                var reviewText = reqJobj["text"].ToString();

                                // 리뷰 기록
                                DbManager.WriteReview(userId, placeId, nickname, reviewScore, reviewText);

                                // ok 응답
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                break;
                            }
                            case "user_order": {
                                // ★ 인증생략
                                var userId = reqJobj["user_id"].ToString();
                                var price = reqJobj["total_price"].ToObject<double>();

                                // Cash 모자란지 확인
                                if (DbManager.GetUser(userId).Cash < price) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "cash_sparse" }).ToString());
                                    break;
                                }

                                // Cash 변화
                                var afterCash = DbManager.AdjustUserCash(userId, -price);

                                // DB에 기록
                                //DbManager.LogUserOrder(userId, placeId, price);

                                // ok 응답
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                break;
                            }
                            case "get_pc_map": {
                                var place = DbManager.GetPlace(placeId);
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", pc_map = place.PcMap }).ToString());
                                break;
                            }
                            case "set_pc_map": {
                                var map = reqJobj["pc_map"].ToString();
                                var result = DbManager.SetPcMap(placeId, map);
                                if (result == true) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                } else {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Failed to set pc map." }).ToString());
                                }
                                break;
                            }
                            case "get_order_menu": {
                                var place = DbManager.GetPlace(placeId);
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", order_menu = place.OrderMenu }).ToString());
                                break;
                            }
                            case "set_order_menu": {
                                var orderMenu = reqJobj["order_menu"].ToString();
                                var result = DbManager.SetOrderMenu(placeId, orderMenu);
                                if (result == true) {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                } else {
                                    await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Failed to set order menu." }).ToString());
                                }
                                break;
                            }
                            case "update_using_pc_nums": {
                                var listTkn = reqJobj["list"];
                                var totalPcCount = reqJobj["total_pc_count"].ToObject<int>();
                                var list = new List<int>();
                                foreach (var numTkn in listTkn) {
                                    var num = numTkn.ToObject<int>();
                                    list.Add(num);
                                }
                                PlaceManager.UpdateUsingPcNumbers(placeId, totalPcCount, list);
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                break;
                            }
                            case "set_fare_per_hour": {
                                var fph = reqJobj["fare_per_hour"].ToObject<double>();
                                PlaceManager.UpdateFarePerHour(placeId, fph);
                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok" }).ToString());
                                break;
                            }
                            case "get_sales": {
                                var from = reqJobj["from"].ToObject<DateTime>();
                                var to = reqJobj["to"].ToObject<DateTime>();
                                var sales = DbManager.GetPlacePingLogs(placeId, from, to);

                                var list = new List<JToken>();

                                foreach (var sale in sales) {
                                    var item = JToken.FromObject(new {
                                        logged_in_time = sale.loginTime,
                                        logged_out_time = sale.logoutTime,
                                        user_id = sale.userId,
                                        cash = sale.totalCashUse
                                    });
                                    list.Add(item);
                                }

                                await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "ok", list=list }).ToString());
                                break;
                            }
                        }
                    } catch (Exception e) {
                        //context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                        Console.WriteLine($"** {e.Message}\r\n{e.StackTrace}");
                        await WriteResponseWithContentLengthAsync(context, JObject.FromObject(new { result = "error", error = "Internal server error." }).ToString());
                    }
                });
            });
        }

        private bool DoUserPingFinalPayment(string userId, string placeId, bool noLock = false) {
            var now = DateTime.Now;
            var userCashSpend = .0;
            var loginTime = default(DateTime);
            var totalPaid = .0;
            // 마지막 결제금 산정하고 로그인 목록에서 삭제 (다른 Place인 경우 삭제할 수 없음)
            if (noLock == false) { Monitor.Enter(UserLoggedInPlaces); }
            try {
                if (UserLoggedInPlaces.TryGetValue(userId, out var tuple) == true) {
                    if (tuple.placeId == placeId) {
                        userCashSpend = (now - tuple.lastPaidTime).TotalHours * PlaceManager.GetFarePerHour(placeId);
                        totalPaid = tuple.totalPaid + userCashSpend;
                        loginTime = tuple.loginTime;
                        UserLoggedInPlaces.Remove(userId);
                    } else {
                        return false;
                    }
                } else {
                    return false;
                }
            } finally {
                if (noLock == false) { Monitor.Exit(UserLoggedInPlaces); }
            }

            // 결제 및 기록
            var afterCash = DbManager.AdjustUserCash(userId, -userCashSpend);
            if (afterCash.HasValue && loginTime != default(DateTime)) {
                DbManager.LogUserPing(loginTime, userId, placeId, totalPaid, now - loginTime, true);
                return true;
            } else {
                return false;
            }
        }

        /// <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));
    }
}
