using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
using SVSim.Database.Repositories.Globals;
using SVSim.Database.Repositories.Viewer;
using SVSim.Database.Services;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Infrastructure;
using SVSim.EmulatedEntrypoint.Models.Dtos;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
using SVSim.EmulatedEntrypoint.Services;

namespace SVSim.EmulatedEntrypoint.Controllers;

public class MyPageController : SVSimController
{
    /// <summary>"yyyy-MM-dd HH:mm:ss" — prod's PHP convention. Used for wire-formatting DateTime
    /// columns that the client parses via DateTime.Parse on its side.</summary>
    private const string WireDateFormat = "yyyy-MM-dd HH:mm:ss";

    private readonly IViewerRepository _viewerRepository;
    private readonly IGlobalsRepository _globalsRepository;
    private readonly IGameConfigService _config;
    private readonly IArenaTwoPickRunRepository _arenaTwoPickRuns;
    private readonly IHomeDialogSessionTracker _homeDialogTracker;

    public MyPageController(IViewerRepository viewerRepository, IGlobalsRepository globalsRepository,
        IGameConfigService config, IArenaTwoPickRunRepository arenaTwoPickRuns,
        IHomeDialogSessionTracker homeDialogTracker)
    {
        _viewerRepository = viewerRepository;
        _globalsRepository = globalsRepository;
        _config = config;
        _arenaTwoPickRuns = arenaTwoPickRuns;
        _homeDialogTracker = homeDialogTracker;
    }

    [HttpPost("index")]
    public async Task<ActionResult<MyPageIndexResponse>> Index(MyPageIndexRequest request)
    {
        var shortUdidClaim = User.Claims.FirstOrDefault(c => c.Type == ShadowverseClaimTypes.ShortUdidClaim)?.Value;
        if (shortUdidClaim is null || !long.TryParse(shortUdidClaim, out long shortUdid))
        {
            return Unauthorized();
        }

        Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
        if (viewer is null)
        {
            return NotFound();
        }

        var deviceHeader = Request.Headers["DEVICE"].FirstOrDefault();
        int deviceType = int.TryParse(deviceHeader, out int parsed) ? parsed : 0;

        // Hydrate all the globals slices in parallel-ish — they're independent reads.
        var rotation = _config.Get<RotationConfig>();
        var colosseum = await _globalsRepository.GetCurrentColosseum();
        var sealedSeason = await _globalsRepository.GetCurrentSealedSeason();
        var masterPointPeriod = await _globalsRepository.GetCurrentMasterPointPeriod();
        var bannerEntries = await _globalsRepository.GetBanners();
        var specialDeckFormats = await _globalsRepository.GetActiveSpecialDeckFormats();
        var activeHomeDialogs = await _globalsRepository.GetActiveHomeDialogsAsync(DateTime.UtcNow);

        var homeDialogList = new List<Models.Dtos.Common.HomeDialog>();
        foreach (var entry in activeHomeDialogs)
        {
            if (_homeDialogTracker.TryReserve(viewer.ShortUdid, entry.Id))
            {
                homeDialogList.Add(BuildHomeDialog(entry));
                break;  // Client only reads [0]; emit at most one per call.
            }
        }

        // Remaining stubs are tagged TODO(mypage-stub) — see docs/api-spec/endpoints/post-login/mypage-index.md.
        return new MyPageIndexResponse
        {
            UserInfo = new UserInfo(deviceType, viewer),
            UnreceivedMissionRewardCount = 0,                       // TODO(mypage-stub): viewer mission progress
            ReceiveFriendApplyCount = 0,                            // TODO(mypage-stub): viewer friend-request inbox
            UnreadPresentCount = 0,                                 // TODO(mypage-stub): viewer presents/mail
            FriendBattleInviteCount = 0,                            // TODO(mypage-stub): viewer room-invite count
            GuildNotification = new GuildNotification(),            // TODO(mypage-stub): viewer guild state
            LastAnnounceId = 0,                                     // TODO(mypage-stub): globals announcement metadata
            LastAnnounceUpdateTime = string.Empty,                  // TODO(mypage-stub): globals announcement metadata
            FeatureMaintenanceList = new(),                         // TODO(mypage-stub): FeatureMaintenanceEntry rows
            ArenaInfo = await BuildArenaInfosAsync(viewer.Id),
            IsArenaChallengePeriod = false,                         // TODO(mypage-stub): globals/ArenaSeason flag
            IsAvailableColosseumFreeEntry = false,                  // TODO(mypage-stub): viewer + globals free-entry quota
            ColosseumInfo = BuildColosseumInfo(colosseum),
            SealedInfo = BuildSealedInfo(sealedSeason),
            Banner = bannerEntries.Select(BuildBannerInfo).ToList(),
            RoomTypeInSession = new RoomTypeInSession
            {
                SpecialDeckFormatList = specialDeckFormats
                    .Select(e => new SpecialDeckFormat
                    {
                        DeckFormat = e.DeckFormat,
                        EndTime = e.EndTime.ToString(WireDateFormat, CultureInfo.InvariantCulture)
                    })
                    .ToList()
            },
            Convention = new Convention                             // TODO(mypage-stub): viewer offline-event participation
            {
                IsJoinTournament = false,
                IsAdminWatchUser = false,
            },
            UserConfig = new UserConfig(),                          // TODO(mypage-stub): persist viewer UserConfig
            Quest = new Quest(),                                    // TODO(mypage-stub): active Quest event + viewer flags
            MasterPointRankingPeriod = BuildMasterPointRankingPeriod(masterPointPeriod),
            PreReleaseStatus = 0,                                   // TODO(mypage-stub): derive from PreReleaseInfo
            UserMyPageInfo = new UserMyPageInfo
            {
                UserMyPageSetting = new MyPageBgSetting
                {
                    MyPageId = viewer.MyPageBgId.ToString(),
                    SelectType = viewer.MyPageBgSelectType.ToString(),
                    MyPageIdList = viewer.MyPageBgRotation
                        .OrderBy(r => r.Slot)
                        .Select(r => r.BgId.ToString())
                        .ToList(),
                },
            },
            BasicPuzzle = new Models.Dtos.Common.BadgeFlag { IsDisplayBadge = false }, // TODO(mypage-stub): viewer practice-puzzle progress
            IsBattlePassPeriod = rotation.IsBattlePassPeriod,
            // The client's MyPageTask.Parse (line 155-163) does `_userItemDict.Clear();` whenever
            // user_item_list is present in the response — not when it's non-empty — and then
            // repopulates from the wire. Emitting [] here wipes the inventory the client populated
            // from /load/index, which makes PackChildGachaInfo.CostGoodsCount return 0 and filters
            // out is_hide=1 tutorial packs (the legendary starter 99047) via PackConfig.EnableBuyPack.
            // Populate from viewer.Items so the client's dict stays in sync with the DB.
            UserItemList = viewer.Items.Select(i => new UserItem(i)).ToList(),
            HomeDialogList = homeDialogList,
            SpecialCrystalInfo = new(),                             // TODO(mypage-stub): same shape/source as /load/index
            // CompetitionInfo, ShopNotification, StoryNotification, GuildNotification, GatheringInfo,
            // IsHiddenBossAppeared, SubBanner/SubBannerList/HomeDialogList/UserOfflineEvent/UserItemList,
            // and the three explicit-null fields (TreasureInfo, LotteryPeriodInfo, AllCardEnabledPeriod)
            // all rely on MyPageIndexResponse field initializers.
            // TODO(mypage-stub): wire competition_info from active tournament row (default false fine until tournaments exist).
            // TODO(mypage-stub): wire shop_notification from per-product shop-appeal state.
            // TODO(mypage-stub): wire story_notification from viewer story progress.
            // TODO(mypage-stub): wire is_hidden_boss_appeared from globals event flag.
            // TODO(mypage-stub): per-viewer state for user_item_list, gathering_info.is_entry, guild_notification, user_offline_event, home_dialog_list.
        };
    }

    /// <summary>
    /// Slim notification-delta endpoint — see MyPageRefreshResponse for the 3-field contract.
    /// Client fires this once after main-menu UI settles (and a second time shortly after; both
    /// calls get the same response). No state changes happen here; everything is read-only.
    /// </summary>
    [HttpPost("refresh")]
    public async Task<ActionResult<MyPageRefreshResponse>> Refresh(MyPageRefreshRequest request)
    {
        var shortUdidClaim = User.Claims.FirstOrDefault(c => c.Type == ShadowverseClaimTypes.ShortUdidClaim)?.Value;
        if (shortUdidClaim is null || !long.TryParse(shortUdidClaim, out long shortUdid))
        {
            return Unauthorized();
        }

        Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
        if (viewer is null)
        {
            return NotFound();
        }

        return new MyPageRefreshResponse
        {
            FriendBattleInviteCount = 0,                           // TODO(mypage-stub): viewer room-invite count
            ShopNotification = new ShopNotification(),             // TODO(mypage-stub): per-product shop-appeal state
            GatheringNotification = new GatheringNotification(),   // empty matching message — correct for fresh viewers
        };
    }

    /// <summary>
    /// Mirrors LoadController.BuildArenaInfosAsync. /mypage/index has no Keys.Contains("arena_info")
    /// guard (ArenaData(jsonData["arena_info"]) at MyPageTask.cs:55 indexes [0] unconditionally), and
    /// the post-parse UI consumer (ChallengeEntry.SetChallengeInfo at ChallengeEntry.cs:35) reads
    /// _twoPickData.ChallengeData which is only built when arena_info[0].format_info is present.
    /// So we always populate format_info from the same ArenaSeason.FormatInfo jsonb /load/index uses.
    /// </summary>
    private async Task<List<ArenaInfo>> BuildArenaInfosAsync(long viewerId)
    {
        var season = await _globalsRepository.GetCurrentArenaSeason();

        // is_join MUST reflect the viewer's actual TK2 state — true iff they have an
        // active ViewerArenaTwoPickRun row. The client uses this to choose between the
        // "Pay to enter" and "Resume run" dialogs (Wizard/ChallengeEntry.cs:165 + ArenaEntryBase).
        // See LoadController.BuildArenaInfosAsync for the matching /load/index path.
        bool hasActiveRun = (await _arenaTwoPickRuns.GetByViewerIdAsync(viewerId)) is not null;

        if (season is null)
        {
            return new List<ArenaInfo>
            {
                new ArenaInfo
                {
                    Mode = 0,
                    Enable = 0,
                    Cost = 0,
                    RupeeCost = 0,
                    TicketCost = 0,
                    IsJoin = hasActiveRun,
                },
            };
        }

        ArenaFormatInfo? format = null;
        if (!string.IsNullOrEmpty(season.FormatInfo) && season.FormatInfo != "{}")
        {
            format = JsonSerializer.Deserialize<ArenaFormatInfo>(season.FormatInfo, JsonbReadOptions.Instance);
        }

        return new List<ArenaInfo>
        {
            new ArenaInfo
            {
                Mode = season.Mode,
                Enable = season.Enable,
                Cost = season.Cost,
                RupeeCost = season.RupyCost,
                TicketCost = season.TicketCost,
                IsJoin = hasActiveRun,
                FormatInfo = format,
            }
        };
    }

    private ColosseumInfo BuildColosseumInfo(ColosseumConfig? row)
    {
        if (row is null) return new ColosseumInfo();

        ColosseumSalesPeriodInfo sales = new();
        if (!string.IsNullOrEmpty(row.SalesPeriodInfo) && row.SalesPeriodInfo != "{}")
        {
            sales = JsonSerializer.Deserialize<ColosseumSalesPeriodInfo>(row.SalesPeriodInfo, JsonbReadOptions.Instance)
                    ?? new ColosseumSalesPeriodInfo();
        }

        return new ColosseumInfo
        {
            ColosseumId = row.ColosseumId,
            IsDisplayTips = row.IsDisplayTips,
            TipsId = row.TipsId,
            CardPoolName = row.CardPoolName,
            IsColosseumPeriod = row.IsColosseumPeriod,
            IsRoundPeriod = row.IsRoundPeriod,
            DeckFormat = row.DeckFormat,
            IsNormalTwoPick = row.IsNormalTwoPick,
            IsSpecialMode = row.IsSpecialMode,
            IsAllCardEnabled = row.IsAllCardEnabled,
            StartTime = row.StartTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
            ColosseumName = row.ColosseumName,
            NowRound = row.NowRound,
            EndTime = row.EndTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
            SalesPeriodInfo = sales,
        };
    }

    private SealedInfo BuildSealedInfo(SealedConfig? row)
    {
        if (row is null) return new SealedInfo();

        List<int> packInfo = new();
        if (!string.IsNullOrEmpty(row.PackInfo) && row.PackInfo != "[]")
        {
            packInfo = JsonSerializer.Deserialize<List<int>>(row.PackInfo, JsonbReadOptions.Instance) ?? new();
        }

        SealedSalesPeriodInfo sales = new();
        if (!string.IsNullOrEmpty(row.SalesPeriodInfo) && row.SalesPeriodInfo != "{}")
        {
            sales = JsonSerializer.Deserialize<SealedSalesPeriodInfo>(row.SalesPeriodInfo, JsonbReadOptions.Instance)
                    ?? new SealedSalesPeriodInfo();
        }

        return new SealedInfo
        {
            Enable = row.Enable,
            CrystalCost = row.CrystalCost,
            RupyCost = row.RupyCost,
            TicketCost = row.TicketCost,
            IsJoin = row.IsJoin,
            PackInfo = packInfo,
            DeckUsingNumMin = row.DeckUsingNumMin,
            ScheduleId = row.ScheduleId,
            IsDeckCodeMaintenance = row.IsDeckCodeMaintenance,
            SalesPeriodInfo = sales,
        };
    }

    /// <summary>
    /// Deserializes the jsonb button_list column into wire-shape DTOs. Truncates >3 buttons —
    /// the client's switch in MyPageHomeDialog.InitializeButtonAction only handles 0/1/2/3,
    /// extras would be silently ignored anyway; doing it server-side keeps the wire honest.
    /// </summary>
    private static Models.Dtos.Common.HomeDialog BuildHomeDialog(HomeDialogEntry row)
    {
        List<Models.Dtos.Common.HomeDialogButtonDto> buttons = new();
        if (!string.IsNullOrEmpty(row.ButtonListJson) && row.ButtonListJson != "[]")
        {
            buttons = JsonSerializer.Deserialize<List<Models.Dtos.Common.HomeDialogButtonDto>>(
                row.ButtonListJson, JsonbReadOptions.Instance) ?? new();
        }
        if (buttons.Count > 3)
        {
            buttons = buttons.Take(3).ToList();
        }
        return new Models.Dtos.Common.HomeDialog
        {
            Type = row.Type?.ToString(CultureInfo.InvariantCulture),
            TitleTextId = row.TitleTextId,
            Image = row.Image,
            ButtonList = buttons,
        };
    }

    private static BannerInfo BuildBannerInfo(BannerEntry row)
    {
        List<string> imagePaths = new();
        if (!string.IsNullOrEmpty(row.ImagePaths) && row.ImagePaths != "[]")
        {
            imagePaths = JsonSerializer.Deserialize<List<string>>(row.ImagePaths, JsonbReadOptions.Instance) ?? new();
        }

        return new BannerInfo
        {
            ImageName = row.ImageName,
            Click = row.Click,
            Status = row.Status,
            // DB stores numeric, wire is string. PHP convention.
            ChangeTime = row.ChangeTime.ToString(CultureInfo.InvariantCulture),
            RemainingTime = row.RemainingTime.ToString(CultureInfo.InvariantCulture),
            ImagePaths = imagePaths,
        };
    }

    /// <summary>
    /// Far-future fallback EndTime so the client's DateTime.Parse(end_time) succeeds and
    /// Data.Load.data._masterResetNextTime gets seeded even when no globals row is present.
    /// </summary>
    private static MasterPointRankingPeriod BuildMasterPointRankingPeriod(MasterPointRankingPeriodEntry? row)
    {
        if (row is null)
        {
            return new MasterPointRankingPeriod
            {
                EndTime = "2030-01-01 00:00:00",
            };
        }

        return new MasterPointRankingPeriod
        {
            Id = row.Id,
            PeriodNum = row.PeriodNum,
            NecessaryScore = row.NecessaryScore,
            BeginTime = row.BeginTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
            EndTime = row.EndTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
        };
    }
}
