using System.Globalization;
using log4net;
using SQLiteEF;
using SteamApiWrapper.ReturnTypes;
using SteamGame = SteamApiWrapper.ReturnTypes.Game;
using Player = SQLiteEF.Player;
using SteamPlayer = SteamApiWrapper.ReturnTypes.Player;
using Steam = SteamApiWrapper.SteamApiWrapper;

namespace API;

public class Tracker : IDisposable
{
    private readonly Thread _trackerThread;
    private bool _running = true;
    private ILog Log { get; } = LogManager.GetLogger(typeof(Tracker));
    private TimeSpan Interval { get; init; }

    public Tracker(IServiceProvider serviceProvider, IConfiguration configuration)
    {
        Interval = TimeSpan.Parse(configuration.GetValue<string>("UpdateInterval")!, CultureInfo.InvariantCulture);
        _trackerThread = new (TrackerLoop);
        _trackerThread.Start(serviceProvider);
    }

    internal void UpdatePlayers(Context context)
    {
        Log.Info("Updating Players");
        Player[] dbPlayers = context.Players.ToArray();
        SteamPlayer[] summaries = Steam.GetPlayerSummaries(dbPlayers.Select(p => p.SteamId).ToArray());
        foreach (Player dbPlayer in dbPlayers)
        {
            if(summaries.All(summaryPlayer => summaryPlayer.steamid != dbPlayer.SteamId))
                continue;
            SteamPlayer summaryPlayer = summaries.First(summaryPlayer => summaryPlayer.steamid == dbPlayer.SteamId);
            
            try
            {
                dbPlayer.Name = summaryPlayer.personaname;
                dbPlayer.AvatarUrl = summaryPlayer.avatar;
                dbPlayer.ProfileUrl = summaryPlayer.profileurl;
                dbPlayer.UpdatedAt = DateTime.UtcNow;
                context.SaveChanges();
            }
            catch (Exception e)
            {
                Log.Error(e);
            }
        }
    }

    internal void UpdatePlayer(Context context, Player player)
    {
        SteamPlayer[] summaries = Steam.GetPlayerSummaries([player.SteamId]);
        if(summaries.All(summaryPlayer => summaryPlayer.steamid != player.SteamId))
                return;
        SteamPlayer summaryPlayer = summaries.First(summaryPlayer => summaryPlayer.steamid == player.SteamId);
            
        try
        {
            player.Name = summaryPlayer.personaname;
            player.AvatarUrl = summaryPlayer.avatar;
            player.ProfileUrl = summaryPlayer.profileurl;
            player.UpdatedAt = DateTime.UtcNow;
            context.SaveChanges();
        }
        catch (Exception e)
        {
            Log.Error(e);
        }
    }

    internal void UpdateOwnedGames(Context context)
    {
        Log.Info("Updating Owned Games");
        Player[] players = context.Players.ToArray();
        foreach (Player player in players)
        {
            UpdateOwnedGamesPlayer(context, player);
        }
    }

    internal void UpdateOwnedGamesPlayer(Context context, Player player)
    {
        Log.Debug($"Updating owned games for player {player}");
        SteamGame[] ownedGames = Steam.GetOwnedGames(player.SteamId);
        foreach (SteamGame ownedGame in ownedGames)
        {
            if (context.Games.Find(ownedGame.appid) is not { } game)
            {
                game = new(ownedGame.appid, ownedGame.name);
                context.Games.Add(game);
            }

            if (!player.Games.Contains(game))
            {
                context.Entry(player).Collection(p => p.Games).Load();
                player.Games.Add(game);
                context.Entry(game).Collection(g => g.PlayedBy).Load();
                game.PlayedBy.Add(player);
                context.Entry(player).Collection(p => p.TrackedTimes).Load();
                player.TrackedTimes.Add(new (game, player, ownedGame.playtime_forever));
            }
                
            try
            {
                context.SaveChanges();
            }
            catch (Exception e)
            {
                Log.Error(e);
            }
        }
    }

    internal void UpdateGameTimes(Context context)
    {
        Log.Info("Updating Game Times");
        Player[] players = context.Players.ToArray();
        foreach (Player player in players)
        {
            UpdateGameTimesPlayer(context, player);
        }
    }

    internal void UpdateGameTimesPlayer(Context context, Player player)
    {
        Log.Debug($"Updating game times for player {player}");
        GetRecentlyPlayedGames recentlyPlayed = Steam.GetRecentlyPlayedGames(player.SteamId);
        foreach (SteamGame recentlyPlayedGame in recentlyPlayed.games)
        {
            if (context.Games.Find(recentlyPlayedGame.appid) is not { } game)
            {
                game = new(recentlyPlayedGame.appid, recentlyPlayedGame.name);
                context.Games.Add(game);
            }

            if (!player.Games.Contains(game))
            {
                context.Entry(player).Collection(p => p.Games).Load();
                player.Games.Add(game);
                context.Entry(game).Collection(g => g.PlayedBy).Load();
                game.PlayedBy.Add(player);
            }
                
            context.Entry(player).Collection(p => p.TrackedTimes).Load();
            player.TrackedTimes.Add(new (game, player, recentlyPlayedGame.playtime_forever));
                
            try
            {
                context.SaveChanges();
            }
            catch (Exception e)
            {
                Log.Error(e);
            }
        }
    }

    private void TrackerLoop(object? obj)
    {
        if (obj is not IServiceProvider serviceProvider)
        {
            Log.Fatal("Tracker loop wasn't started.");
            return;
        }

        while (_running)
        {
            IServiceScope scope = serviceProvider.CreateScope();
            Context context = scope.ServiceProvider.GetRequiredService<Context>();
            UpdatePlayers(context);
            UpdateOwnedGames(context);
            UpdateGameTimes(context);
            scope.Dispose();
            try
            {
                Thread.Sleep(Interval);
            }
            catch (ThreadInterruptedException)
            {
                Log.Debug("Thread interrupted");
            }
            catch (ThreadAbortException)
            {
                _running = false;
            }
        }
        Log.Info("Thread exited");
    }

    public void ForceLoop()
    {
        _trackerThread.Interrupt();
    }

    public void Dispose()
    {
        _running = false;
        _trackerThread.Interrupt();
    }
}