
using Scalar.AspNetCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using SpotifyAPI.Web;
using Microsoft.AspNetCore.Components.Authorization;

var builder = WebApplication.CreateBuilder(args);


// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddLogging((ILoggingBuilder b) => b.ClearProviders().AddConsole());
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Auth/Login";
    });

builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
builder.Services.AddSingleton<SignalIntegration>();
builder.Services.AddSingleton<LdapIntegration>();
builder.Services.AddSingleton<SpotifyApiClient>();
builder.Services.AddSingleton<PlayListSynchronizer>();
builder.Services.AddSingleton<SongResolver>();

var app = builder.Build();

var logger = app.Logger;

logger.LogTrace("Setting up user check timer");
var userCheckTimer = new CronTimer(AppConfiguration.Instance.UserCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
userCheckTimer.OnOccurence += async (s, ea) =>
{
    var signalIntegration = app.Services.GetService<SignalIntegration>();
    var memberList = await signalIntegration.GetMemberListAsync();
    var dci = DataContext.Instance;
    var needsSaving = false;
    foreach (var memberId in memberList)
    {
        var foundUser = dci.Users?.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
        if (foundUser == null)
        {
            var newUserContact = await signalIntegration.GetContactAsync(memberId);
            logger.LogDebug("New user:");
            logger.LogDebug($"   Name: {newUserContact.Name}");
            logger.LogDebug($"   MemberId: {memberId}");
            User newUser = new User()
            {
                Name = newUserContact.Name,
                SignalMemberId = memberId,
                NickName = string.Empty,
                IsIntroduced = false,
                LdapUserName = string.Empty,
                AssociationInProgress = false,
                WasChosenForSuggestionThisRound = false,
                LikedSongs = new List<Song>()
            };
            dci.Users?.Add(newUser);
            needsSaving = true;
        }
    }

    if (needsSaving)
    {
        await dci.SaveChangesAsync();
    }
    await dci.DisposeAsync();
};

logger.LogTrace("Setting up liked songs playlist creation timer");
var likePlaylistCheckTimer = new CronTimer(AppConfiguration.Instance.LikePlaylistCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
likePlaylistCheckTimer.OnOccurence += async (s, ea) =>
{
    var spotifyApiClient = app.Services.GetService<SpotifyApiClient>();
    var playlistSynchronizer = app.Services.GetService<PlayListSynchronizer>();
    var dci = DataContext.Instance;
    var needsSaving = false;
    var allUsers = dci.Users.ToList();
    await dci.DisposeAsync();
    dci = DataContext.Instance;
    foreach (var user in allUsers)
    {
        if (!await spotifyApiClient.IsUserAuthenticatedAsync(user))
        {
            logger.LogWarning($"User {user.LdapUserName} is not authorized with Spotify - skipping playlist sync");
            continue;
        }
        var foundPlaylist = dci.SmartPlaylistDefinitions?.Where(p => p.CreatedBy == user).ToList().Where(p => p.IsThisUsersLikedSongsPlaylist).SingleOrDefault();
        if (foundPlaylist == null)
        {
            logger.LogWarning($"Creating liked songs playlist for user {user.LdapUserName}");
            var title = $"{user.PreferredName}'s liked songs";
            var description = $"A collection of the songs liked by {user.PreferredName} on their 'Song of the day' server instance.";
            // playlist does not exist yet, creating it
            var newPlaylist = await (await spotifyApiClient.WithUserAuthorizationAsync(user)).CreateSpotifyPlaylist(title, description, false, true, user);
            await playlistSynchronizer.SynchronizePlaylistAsync(newPlaylist);
            needsSaving = true;
        }
        logger.LogWarning($"Syncing playlists for user {user.LdapUserName}");
        await playlistSynchronizer.SynchronizeUserPlaylistsAsync(user);
    }

    if (needsSaving)
    {
        await dci.SaveChangesAsync();
    }
    await dci.DisposeAsync();
};

logger.LogTrace("Setting up user intro timer");
var userIntroTimer = new CronTimer(AppConfiguration.Instance.UserIntroCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
userIntroTimer.OnOccurence += async (s, ea) =>
{
    var dci = DataContext.Instance;
    var introUsers = dci.Users?.Where(u => !u.IsIntroduced);
    if (introUsers == null)
    {
        await dci.DisposeAsync();
        return;
    }

    bool needsSaving = false;
    foreach (var user in introUsers)
    {
        var signalIntegration = app.Services.GetService<SignalIntegration>();
        await signalIntegration.IntroduceUserAsync(user);
        user.IsIntroduced = true;
        needsSaving = true;
    }

    if (needsSaving)
    {
        await dci.SaveChangesAsync();
    }
    await dci.DisposeAsync();
};


logger.LogTrace("Setting up pick of the day timer");
var pickOfTheDayTimer = new CronTimer(AppConfiguration.Instance.PickOfTheDayCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
{
    var dci = DataContext.Instance;

    var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();

    if (lastSong != null && lastSong.Date > DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
    {
        logger.LogWarning("Skipping pick of the day today!");
        await dci.DisposeAsync();
        return;
    }

    if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
    {
        logger.LogError("Unable to properly initialize DB context!");
        await dci.DisposeAsync();
        return;
    }
    var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
    if (!potentialUsers.Any())
    {
        logger.LogTrace("Resetting suggestion count on users before resuming");
        await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
        await dci.SaveChangesAsync();
        potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
    }
    logger.LogDebug("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
    var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
    if (luckyUser == null)
    {
        logger.LogError("Unable to determine today's lucky user!");
        await dci.DisposeAsync();
        return;
    }
    var userName = string.IsNullOrEmpty(luckyUser.NickName) ? luckyUser.Name : luckyUser.NickName;
    var suggestion = await dci.SuggestionHelpers.ElementAtAsync((new Random()).Next(await dci.SuggestionHelpers.CountAsync()));
    var newSongSuggestion = new SongSuggestion()
    {
        User = luckyUser,
        SuggestionHelper = suggestion,
        UserHasSubmitted = false,
        HasUsedSuggestion = false,
        Date = DateTime.Today.ToUniversalTime()
    };
    if (luckyUser.SignalMemberId is string signalId)
    {
        var result = await dci.SongSuggestions.AddAsync(newSongSuggestion);
        newSongSuggestion = result.Entity;
        luckyUser.WasChosenForSuggestionThisRound = true;
        await dci.SaveChangesAsync();
        var signalIntegration = app.Services.GetService<SignalIntegration>();
        await signalIntegration.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
        await signalIntegration.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId);
        await signalIntegration.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId);
        await signalIntegration.SendMessageToUserAsync($"Please navigate to https://sotd.disi.dev/SongSubmission/{newSongSuggestion.Id} to submit your choice!", luckyUser.SignalMemberId);
    }
    await dci.DisposeAsync();
};

var startUserAssociationProcess = async (User userToAssociate) =>
{
    if (userToAssociate.SignalMemberId is string signalId)
    {
        var signalIntegration = app.Services.GetService<SignalIntegration>();
        await signalIntegration.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId);
        await signalIntegration.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId);
        await signalIntegration.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId);
        await signalIntegration.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId);
    }
};    

logger.LogTrace("Setting up LdapAssociation timer");
var ldapAssociationTimer = new CronTimer(AppConfiguration.Instance.LdapAssociationTimerSchedule, "Europe/Vienna", includingSeconds: false);
ldapAssociationTimer.OnOccurence += async (s, ea) =>
{
    var dci = DataContext.Instance;
    if (dci.Users == null)
    {
        logger.LogError("Unable to properly initialize DB context!");
        await dci.DisposeAsync();
        return;
    }
    var nonAssociatedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName) && !u.AssociationInProgress);
    var needsSaving = false;
    foreach (var user in nonAssociatedUsers)
    {
        user.AssociationInProgress = true;

        await startUserAssociationProcess(user);
        user.IsIntroduced = true;
        needsSaving = true;
    }

    if (needsSaving)
    {
        await dci.SaveChangesAsync();
    }
    await dci.DisposeAsync();
};

logger.LogTrace("Setting up MessageSync timer");
var messageSyncTimer = new CronTimer(AppConfiguration.Instance.MessageSyncTimerSchedule, "Europe/Vienna", includingSeconds: false);
messageSyncTimer.OnOccurence += async (s, ea) =>
{
    var signalIntegration = app.Services.GetService<SignalIntegration>();
    await signalIntegration.GetMessagesAsync();
};
// disabled for now, is still buggy
// messageSyncTimer.Start();

// only start interaction timers in production builds
// for local/development testing we want those disabled
if (!app.Environment.IsDevelopment())
{
    logger.LogTrace("Starting timer for scheduled processes.");
    userCheckTimer.Start();
    userIntroTimer.Start();
    pickOfTheDayTimer.Start();
    ldapAssociationTimer.Start();
    likePlaylistCheckTimer.Start();
}
else
{
    logger.LogTrace("This is a debug build - scheduled processes are disabled.");
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
else
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();
app.MapControllerRoute(
    name: "login",
    pattern: "{controller=Auth}/{action=Login}"
);
app.MapControllerRoute(
    name: "logout",
    pattern: "{controller=Auth}/{action=Logout}"
);
app.MapGet("SpotifyLogin", async (HttpRequest request, HttpResponse response) =>
{
    var spotifyClient = app.Services.GetService<SpotifyApiClient>();
    var code = request.Query["code"];
    var oAuthResponse = await new OAuthClient().RequestToken(
        new AuthorizationCodeTokenRequest(AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret, code, new Uri(spotifyClient.GetLoginRedirectUri()))
    );

    var dci = DataContext.Instance;
    var userName = request.HttpContext.User.Identity.Name;
    var loggedInUser = dci.Users.Where(u => u.LdapUserName == userName).SingleOrDefault();
    var userId = loggedInUser.UserId;
    loggedInUser.SpotifyAuthAccessToken = oAuthResponse.AccessToken;
    loggedInUser.SpotifyAuthExpiresAfterSeconds = oAuthResponse.ExpiresIn;
    loggedInUser.SpotifyAuthCreatedAt = oAuthResponse.CreatedAt;
    loggedInUser.SpotifyAuthRefreshToken = oAuthResponse.RefreshToken;
    await dci.SaveChangesAsync();
    dci.Dispose();

    response.Redirect($"/User/{userId}");
});
app.MapGet("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
        string.Join("\n", endpointSources.SelectMany(source => source.Endpoints)));

app.Run();