// <copyright file="LibraryOptionsRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>

#nullable disable

namespace Emby.Server.Implementations.Library;

using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

/// <summary>
/// Repository for persisting collection-folder library options in the database.
/// </summary>
public class LibraryOptionsRepository : ILibraryOptionsRepository
{
    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
    private readonly IServerApplicationHost _appHost;
    private readonly ILogger<LibraryOptionsRepository> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="LibraryOptionsRepository"/> class.
    /// </summary>
    /// <param name="dbProvider">The db context factory.</param>
    /// <param name="appHost">The application host.</param>
    /// <param name="logger">The logger.</param>
    public LibraryOptionsRepository(
        IDbContextFactory<JellyfinDbContext> dbProvider,
        IServerApplicationHost appHost,
        ILogger<LibraryOptionsRepository> logger)
    {
        _dbProvider = dbProvider;
        _appHost = appHost;
        _logger = logger;
    }

    /// <inheritdoc />
    public LibraryOptions GetLibraryOptions(string libraryPath)
        => GetLibraryOptionsAsync(libraryPath, CancellationToken.None).GetAwaiter().GetResult();

    /// <inheritdoc />
    public void SaveLibraryOptions(string libraryPath, LibraryOptions options)
        => SaveLibraryOptionsAsync(libraryPath, options, CancellationToken.None).GetAwaiter().GetResult();

    /// <summary>
    /// Gets library options from the database if the backing table is available.
    /// </summary>
    /// <param name="libraryPath">The collection folder path.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The expanded library options, or <c>null</c> if no row exists.</returns>
    public async Task<LibraryOptions> GetLibraryOptionsAsync(string libraryPath, CancellationToken cancellationToken = default)
    {
        try
        {
            var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
            await using (context.ConfigureAwait(false))
            {
                var entity = await context.LibraryOptions
                    .AsNoTracking()
                    .SingleOrDefaultAsync(e => e.LibraryPath == libraryPath, cancellationToken)
                    .ConfigureAwait(false);

                if (entity is null)
                {
                    return null;
                }

                var options = JsonSerializer.Deserialize<LibraryOptions>(entity.OptionsJson, JsonDefaults.Options) ?? new LibraryOptions();

                foreach (var mediaPath in options.PathInfos)
                {
                    if (!string.IsNullOrEmpty(mediaPath.Path))
                    {
                        mediaPath.Path = _appHost.ExpandVirtualPath(mediaPath.Path);
                    }
                }

                return options;
            }
        }
        catch (Exception ex) when (IsMissingTableException(ex))
        {
            _logger.LogDebug(ex, "LibraryOptions table is not available yet. Falling back to XML for {LibraryPath}", libraryPath);
            return null;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error loading library options from database for {LibraryPath}", libraryPath);
            return null;
        }
    }

    /// <summary>
    /// Saves library options to the database if the backing table is available.
    /// </summary>
    /// <param name="libraryPath">The collection folder path.</param>
    /// <param name="options">The options to save.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task SaveLibraryOptionsAsync(string libraryPath, LibraryOptions options, CancellationToken cancellationToken = default)
    {
        try
        {
            var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
            await using (context.ConfigureAwait(false))
            {
                var executionStrategy = context.Database.CreateExecutionStrategy();
                await executionStrategy.ExecuteAsync(async () =>
                {
                    var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, JsonDefaults.Options), JsonDefaults.Options)
                        ?? new LibraryOptions();

                    foreach (var mediaPath in clone.PathInfos)
                    {
                        if (!string.IsNullOrEmpty(mediaPath.Path))
                        {
                            mediaPath.Path = _appHost.ReverseVirtualPath(mediaPath.Path);
                        }
                    }

                    var entity = await context.LibraryOptions
                        .SingleOrDefaultAsync(e => e.LibraryPath == libraryPath, cancellationToken)
                        .ConfigureAwait(false);

                    var utcNow = DateTime.UtcNow;
                    var json = JsonSerializer.Serialize(clone, JsonDefaults.Options);

                    if (entity is null)
                    {
                        entity = new LibraryOptionsEntity
                        {
                            LibraryPath = libraryPath,
                            OptionsJson = json,
                            Version = 1,
                            DateCreated = utcNow,
                            DateModified = utcNow
                        };

                        await context.LibraryOptions.AddAsync(entity, cancellationToken).ConfigureAwait(false);
                    }
                    else
                    {
                        entity.OptionsJson = json;
                        entity.DateModified = utcNow;
                    }

                    await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
                }).ConfigureAwait(false);
            }
        }
        catch (Exception ex) when (IsMissingTableException(ex))
        {
            _logger.LogDebug(ex, "LibraryOptions table is not available yet. Skipping database save for {LibraryPath}", libraryPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error saving library options to database for {LibraryPath}", libraryPath);
        }
    }

    private static bool IsMissingTableException(Exception exception)
    {
        return exception.Message.Contains("LibraryOptions", StringComparison.OrdinalIgnoreCase)
            && (exception.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase)
                || exception.Message.Contains("no such table", StringComparison.OrdinalIgnoreCase));
    }
}
