/// <summary>
        /// 비트맵을 내려받고 DB에 등록합니다.
        /// </summary>
        /// <param name="set">기본 정보가 입력된 Set.
        /// 여기서 기본 정보는 <code>Id, Status, Beatmaps[i].BeatmapID, Beatmaps[i].Version, Beatmaps[i].Creator</code>입니다.</param>
        /// <param name="skipDownload"></param>
        /// <param name="keepSynced"></param>
        /// <returns></returns>
        private static async Task <bool> Sync(Set set)
        {
            try
            {
                var path = await OsuLegacyClient.Context.DownloadBeatmapsetAsync(set.SetId, null, set.SyncOption.HasFlag(SyncOption.SkipDownload));

                Log.Write(set.SetId + " DOWNLOADED");

                var local = Requests.GetSetFromLocal(set.SetId, path);
                local.Title     = set.Title;
                local.Artist    = set.Artist;
                local.Creator   = set.Creator;
                local.CreatorId = set.CreatorId;
                local.StatusId  = set.StatusId;
                local.RankedAt  = set.RankedAt;
                //local.Favorites = set.Favorites;
                local.GenreId    = set.GenreId;
                local.LanguageId = set.LanguageId;
                local.UpdatedAt  = File.GetLastWriteTime(path);
                // 온라인 맵셋 정보를 로컬 맵셋 데이터에 추가
                local.Beatmaps = set.Beatmaps.Select(oBeatmap =>
                {
                    // BeatmapID로 찾지 않는 이유는
                    // 실제로 등록되지 않은 비트맵이 로컬 맵셋에 들어있을 때
                    // 이(백업 파일?)를 등록된 비트맵으로 착각할 수 있기 때문임.
                    var beatmap = local.Beatmaps.Find(lBeatmap =>
                                                      lBeatmap.BeatmapInfo.Hash.Equals(oBeatmap.BeatmapInfo.Hash) ||
                                                      lBeatmap.BeatmapInfo.MD5Hash.Equals(oBeatmap.BeatmapInfo.MD5Hash)
                                                      // 해시값이 제공되는 이상 아래와 같은 비교는
                                                      // 적당한 데이터가 DB에 없고, 현재 온라인에서 삭제된 비트맵만...
                                                      || (
                                                          (
                                                              lBeatmap.BeatmapInfo.Version.Equals(oBeatmap.BeatmapInfo.Version)
                                                              // 비트맵 이름은 공백인데 Normal로 등록된 경우
                                                              // https://osu.ppy.sh/s/1785
                                                              || (
                                                                  string.IsNullOrEmpty(lBeatmap.BeatmapInfo.Version) &&
                                                                  oBeatmap.BeatmapInfo.Version.Equals("Normal")
                                                                  )
                                                          )
                                                          // 맵셋 등록자 이름과 비트맵 작성자 이름을 비교해서
                                                          // 참고용으로 넣은 파일이 등록되었는지 확인
                                                          && lBeatmap.Metadata.AuthorString.Equals(oBeatmap.Metadata.AuthorString)
                                                          ));
                    // 읭? 온라인엔 있는 비트맵이 로컬에 없다구??
                    // 다시 받아봐...
                    if (beatmap == null)
                    {
                        throw new EntryPointNotFoundException("온라인 비트맵과 정보가 일치하지 않습니다.");
                    }

                    //
                    // 온라인 데이터를 로컬 비트맵에 추가
                    // 별 영양가 있는 건 아니다.
                    //
                    beatmap.BeatmapInfo.OnlineBeatmapID = oBeatmap.BeatmapInfo.OnlineBeatmapID;
                    //beatmap.BeatmapInfo.Version = oBeatmap.BeatmapInfo.Version;
                    //TODO delete when it's implemented
                    beatmap.BeatmapInfo.StarDifficulty = oBeatmap.BeatmapInfo.StarDifficulty;
                    beatmap.StatusId = oBeatmap.StatusId;
                    return(beatmap);
                }).ToList();
                // 짜집기 형식의 맵셋은 허용하지 않습니다...
                // 근데 osu!는 해주기 때문에... 언랭맵만 내가 좋아하는 대루..^^
                //if (set.StatusId == 0 && local.Beatmaps.GroupBy(i => $"{i.Metadata.Source}@{i.Metadata.Artist}|{i.Metadata.ArtistUnicode}\\{i.Metadata.Title}:{i.Metadata.TitleUnicode}%{i.Metadata.Tags}").Count() > 1)
                //{
                //    throw new EntryPointNotFoundException("메타데이터가 동일하지 않습니다.");
                //}
                Log.Write(set.SetId + " IS VALID");

                Register(local);
                Log.Write(set.SetId + " REGISTERED");

                try
                {
                    var oldBeatmap = path.Remove(path.LastIndexOf(".download"));
                    if (File.Exists(oldBeatmap))
                    {
                        File.Delete(oldBeatmap);
                    }
                    File.Move(path, oldBeatmap);
                }
                catch { }

                return(true);
            }
            catch (Exception e) when(e is HttpRequestException || e is OperationCanceledException)
            {
                return(await Sync(set));
            }
            catch (EntryPointNotFoundException e)
            {
                Log.Level = 1;
                Log.Write(set.SetId + " CORRUPTED ENTRY");
                Log.Write(e.GetBaseException());
                Log.Level = 0;
            }
            catch (Exception e)
            {
                Log.Level = 1;
                Log.Write(set?.SetId + " " + e.GetBaseException() + ": " + e.Message);
                Log.Level = 0;
            }
            return(false);
        }
        private static async Task Main(string[] args)
        {
            System.AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

            if (!(string.IsNullOrEmpty(Settings.Session)
                ? await OsuLegacyClient.Context.LoginAsync(Settings.OsuId, Settings.OsuPw)
                : await OsuLegacyClient.Context.LoginAsync(Settings.Session)))
            {
                Console.WriteLine("login failed");
                Settings.Session = null;
                await Main(args);

                return;
            }
            Settings.Session = OsuLegacyClient.Context.Session;

            if (args.Length > 0)
            {
                if (args[0] == "/?")
                {
                    Console.WriteLine();
                    Console.WriteLine(Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location) + " (SetID[l][s])*");
                    Console.WriteLine();
                    Console.WriteLine("\tl");
                    Console.WriteLine("\t\t내려받기를 건너뛰고 로컬에 있는 맵셋 파일 사용");
                    Console.WriteLine("\ts");
                    Console.WriteLine("\t\tsynced 열의 값을 유지하면서 데이터베이스 갱신");
                    Console.WriteLine();
                    return;
                }

                if (args[0] == "boo")
                {
                    using (var conn = DB.Connect())
                        using (var tx = conn.BeginTransaction())
                            using (var query = conn.CreateCommand())
                            {
                                var r = new Random(DateTime.Now.Millisecond);
                                query.CommandText = "SELECT id, synced, rankedAt FROM gosu_sets WHERE synced <= rankedAt";
                                var records = new List <Tuple <int, DateTime, DateTime> >();
                                using (var result = query.ExecuteReader())
                                {
                                    while (await result.ReadAsync())
                                    {
                                        var setId    = result.GetInt32(0);
                                        var synced   = result.GetDateTime(1);
                                        var rankedAt = result.GetDateTime(2);
                                        synced = synced.AddMinutes(r.Next(3));
                                        synced = synced.AddSeconds(r.NextDouble());
                                        records.Add(new Tuple <int, DateTime, DateTime>(setId, synced, rankedAt));
                                    }
                                }
                                Console.WriteLine($"{records.Count} to update");
                                query.CommandText = "UPDATE gosu_sets SET synced=@s WHERE id=@i";
                                query.Parameters.Add("@s", MySqlDbType.DateTime);
                                query.Parameters.Add("@i", MySqlDbType.Int32);
                                foreach (var record in records)
                                {
                                    query.Parameters["@s"].Value = record.Item2.ToString("yyyy-MM-dd HH:mm:ss.fff");
                                    query.Parameters["@i"].Value = record.Item1;
                                    query.ExecuteNonQuery();
                                }
                                tx.Commit();
                            }
                    return;
                }

                if (args[0] == "health")
                {
                    Log.WriteLevel = 1;
                    foreach (var file in Directory.EnumerateFiles(Settings.Storage, "*.download"))
                    {
                        try
                        {
                            File.Delete(file);
                        }
                        catch
                        {
                            Console.WriteLine($"{file} 삭제 실패!");
                        }
                    }
                    var queue = new Queue <int>();
                    using (var query = DB.Command)
                    {
                        query.CommandText = "SELECT id FROM gosu_sets {} ORDER BY synced DESC";
                        if (args.Length > 1)
                        {
                            query.CommandText = query.CommandText.Replace("{}", "WHERE synced <= (SELECT synced FROM gosu_sets WHERE id = @id)");
                            query.Parameters.AddWithValue("@id", int.Parse(args[1]));
                        }
                        else
                        {
                            query.CommandText = query.CommandText.Replace("{}", "");
                        }
                        using (var result = query.ExecuteReader())
                        {
                            while (await result.ReadAsync())
                            {
                                queue.Enqueue(result.GetInt32(0));
                            }
                        }
                    }
                    var startedAt = DateTime.Now;
                    while (queue.Any())
                    {
                        Console.Title = $@"Now {queue.Peek()}, {queue.Count} Left, {DateTime.Now.Subtract(startedAt)} Passed";
                        //var set = Requests.GetSetFromDB(result.GetInt32(0));
                        //set.SyncOption |= SyncOption.KeepSyncedAt | SyncOption.SkipDownload;
                        //if (!Sync(set))
                        //{
                        //    faults.Add(result.GetString(0));
                        //}

                        var id  = queue.Dequeue();
                        var set = await Requests.GetSetFromAPIAsync(id) ?? await Requests.GetSetFromDBAsync(id);

                        var saved = await Requests.GetSetFromDBAsync(id);

                        // 랭크 상태가 다르거나, 수정 날짜가 다르면 업데이트
                        //if ((
                        //        (set.StatusId > 0 && (saved == null || (saved.UpdatedAt < set.InfoChangedAt || saved.StatusId != set.StatusId)))
                        //        || (set.StatusId == 0 && (saved != null && (saved.UpdatedAt < set.InfoChangedAt || saved.StatusId != set.StatusId)))
                        //    ))
                        //{
                        set.SyncOption |= SyncOption.SkipDownload;
                        if (saved.UpdatedAt < set.InfoChangedAt || saved.StatusId != set.StatusId)
                        {
                            Console.WriteLine($"{id}가 변경된 것 같습니다.");
                            Console.WriteLine($@"Updated: {saved.UpdatedAt.ToString("yyyy-MM-dd HH:mm:ss")} ==> {set.UpdatedAt.ToString("yyyy-MM-dd HH:mm:ss")}");
                            Console.WriteLine($@"Ranked: {saved.RankedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "null"} ==> {set.RankedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "null"}");
                            Console.WriteLine($@"Status: {saved.StatusId} ==> {set.StatusId}");
                            Console.WriteLine($@"Beatmaps: {saved.Beatmaps.Count} ==> {set.Beatmaps.Count}");
                            Console.Write($"{id}를 내려받을까요? (Y/N): ");
                            if (saved.StatusId == 0 && set.StatusId == 0)
                            {
                                Console.Write("미등록 비트맵은 자동으로 내려받습니다...");
                                set.SyncOption &= ~SyncOption.SkipDownload;
                            }
                            else //if (Console.ReadKey().Key == ConsoleKey.Y)
                            {
                                Console.Write("등록된 비트맵은 첫 시도에 있는 파일로 등록을 시도합니다...");
                                //set.SyncOption &= ~SyncOption.SkipDownload;
                            }
                            Console.WriteLine();
                        }
                        // 1차 시도
                        if (await Sync(set))
                        {
                            continue;
                        }

                        Console.WriteLine($"{id}가 깨진 것 같습니다.");
                        Console.Write($"{id}를 내려받을까요? (Y/N): ");
                        if (saved.StatusId == 0 && set.StatusId == 0)
                        {
                            Console.Write("미등록 비트맵은 자동으로 내려받습니다...");
                            set.SyncOption &= ~SyncOption.SkipDownload;
                        }
                        else //if (Console.ReadKey().Key == ConsoleKey.Y)
                        {
                            Console.Write("자야되니까 자동 체크 합니다!!!!!!!!!!!!!!!!!!!!!!!");
                            set.SyncOption &= ~SyncOption.SkipDownload;
                        }
                        Console.WriteLine();
                        // 2차부터는 그냥 나중에 수동으로 하는 거루...
                        if (await Sync(set))
                        {
                            continue;
                        }

                        faults.Add(id + "");
                        //}
                    }
                    if (faults.Count > 0)
                    {
                        throw new NotImplementedException();
                    }
                    return;
                }

                foreach (Match arg in Regex.Matches(string.Join(" ", args), @"(\d+)([^\s]*)"))
                {
                    Set set = await Requests.GetSetFromAPIAsync(Convert.ToInt32(arg.Groups[1].Value))
                              ?? await Requests.GetSetFromDBAsync(Convert.ToInt32(arg.Groups[1].Value));

                    if (set == null)
                    {
                        faults.Add(arg.Groups[1].Value);
                        continue;
                    }

                    foreach (var op in arg.Groups[2].Value)
                    {
                        if (op == 'l')
                        {
                            set.SyncOption |= SyncOption.SkipDownload;
                        }
                        else if (op == 's')
                        {
                            set.SyncOption |= SyncOption.KeepSyncedAt;
                        }
                    }
                    if (!await Sync(set))
                    {
                        faults.Add(arg.Groups[1].Value);
                    }
                }
                if (faults.Count > 0)
                {
                    throw new NotImplementedException();
                }

                if (args[0] != "manage")
                {
                    return;
                }
                Log.Writer = new StreamWriter(File.Open(Settings.LogPath + ".bot.log", FileMode.Create));
            }

            var bucket        = new Stack <Set>();
            var lastCheckTime = Settings.LastCheckTime;

            foreach (var r in Settings.BeatmapList)
            {
                var page = 1;
                do
                {
                    var ids = await OsuLegacyClient.Context.GrabSetIDFromBeatmapListAsync(r, page);

                    if (!ids.Any())
                    {
                        break;
                    }

                    foreach (var id in ids)
                    {
                        var set = await Requests.GetSetFromAPIAsync(id);

                        if (set == null)
                        {
                            // ID NOT FOUND BUT CONTINUE
                            continue;
                        }

                        if (lastCheckTime < set.InfoChangedAt)
                        {
                            lastCheckTime = set.InfoChangedAt;
                        }
                        // 마지막 확인한 비트맵과 이 비트맵의 날짜를 비교 후 탐색 중지 여부 검사
                        // API에 정보가 늦게 등록될 수 있음 + 비트맵 리스트 캐시 피하기 위함
                        if (set.InfoChangedAt < Settings.LastCheckTime.AddHours(-12))
                        {
                            page = 0;
                            break;
                        }

                        var saved = await Requests.GetSetFromDBAsync(id);

                        // 랭크 상태가 다르거나, 수정 날짜가 다르면 업데이트
                        if ((
                                (set.StatusId > 0 && (saved == null || (saved.UpdatedAt < set.InfoChangedAt || saved.StatusId != set.StatusId))) ||
                                (set.StatusId == 0 && (saved != null && (saved.UpdatedAt < set.InfoChangedAt || saved.StatusId != set.StatusId)))
                                ) &&
                            !bucket.Any(i => i.SetId == id))
                        {
                            bucket.Push(set);
                        }
                    }
                } while (page > 0 && page++ < 125);
            }
            while (bucket.Any())
            {
                if (!await Sync(bucket.Pop()))
                {
                    // 동기화에 실패한 비트맵을 다음 번에 다시 확인해 보아야 한다.
                    lastCheckTime = Settings.LastCheckTime;
                }
            }
            Settings.LastCheckTime = lastCheckTime;
        }