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

#pragma warning disable SA1202

namespace Jellyfin.Database.Implementations;

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Implementations.Interfaces;
using Jellyfin.Database.Implementations.Locking;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;

/// <inheritdoc/>
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
/// </summary>
/// <param name="options">The database context options.</param>
/// <param name="logger">Logger.</param>
/// <param name="jellyfinDatabaseProvider">The provider for the database engine specific operations.</param>
/// <param name="entityFrameworkCoreLocking">The locking behavior.</param>
public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger, IJellyfinDatabaseProvider jellyfinDatabaseProvider, IEntityFrameworkCoreLockingBehavior entityFrameworkCoreLocking) : DbContext(options)
{
    private static readonly Action<ILogger, Exception?> SaveChangesError =
    LoggerMessage.Define(
        LogLevel.Error,
        new EventId(1, nameof(SaveChanges)),
        "Error trying to save changes.");

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
    /// </summary>
    public DbSet<AccessSchedule> AccessSchedules => this.Set<AccessSchedule>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the activity logs.
    /// </summary>
    public DbSet<ActivityLog> ActivityLogs => this.Set<ActivityLog>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the API keys.
    /// </summary>
    public DbSet<ApiKey> ApiKeys => this.Set<ApiKey>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the devices.
    /// </summary>
    public DbSet<Device> Devices => this.Set<Device>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the device options.
    /// </summary>
    public DbSet<DeviceOptions> DeviceOptions => this.Set<DeviceOptions>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the display preferences.
    /// </summary>
    public DbSet<DisplayPreferences> DisplayPreferences => this.Set<DisplayPreferences>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the image infos.
    /// </summary>
    public DbSet<ImageInfo> ImageInfos => this.Set<ImageInfo>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the item display preferences.
    /// </summary>
    public DbSet<ItemDisplayPreferences> ItemDisplayPreferences => this.Set<ItemDisplayPreferences>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the custom item display preferences.
    /// </summary>
    public DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences => this.Set<CustomItemDisplayPreferences>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the permissions.
    /// </summary>
    public DbSet<Permission> Permissions => this.Set<Permission>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the preferences.
    /// </summary>
    public DbSet<Preference> Preferences => this.Set<Preference>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the users.
    /// </summary>
    public DbSet<User> Users => this.Set<User>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
    /// </summary>
    public DbSet<TrickplayInfo> TrickplayInfos => this.Set<TrickplayInfo>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
    /// </summary>
    public DbSet<MediaSegment> MediaSegments => this.Set<MediaSegment>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
    /// </summary>
    public DbSet<UserData> UserData => this.Set<UserData>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
    /// </summary>
    public DbSet<AncestorId> AncestorIds => this.Set<AncestorId>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
    /// </summary>
    public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => this.Set<AttachmentStreamInfo>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
    /// </summary>
    public DbSet<BaseItemEntity> BaseItems => this.Set<BaseItemEntity>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
    /// </summary>
    public DbSet<Chapter> Chapters => this.Set<Chapter>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<ItemValue> ItemValues => this.Set<ItemValue>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<ItemValueMap> ItemValuesMap => this.Set<ItemValueMap>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<MediaStreamInfo> MediaStreamInfos => this.Set<MediaStreamInfo>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing audio-specific stream details.
    /// </summary>
    public DbSet<AudioStreamDetails> AudioStreamDetails => this.Set<AudioStreamDetails>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing video-specific stream details.
    /// </summary>
    public DbSet<VideoStreamDetails> VideoStreamDetails => this.Set<VideoStreamDetails>();

    // ── Read-only view projections (PostgreSQL only) ──────────────────────

    /// <summary>
    /// Gets the flat view over MediaStreamInfos joined with audio and video detail tables.
    /// Maps to library."MediaStreamInfos_v".
    /// </summary>
    public DbSet<MediaStreamInfoView> MediaStreamInfoViews => this.Set<MediaStreamInfoView>();

    /// <summary>
    /// Gets the view of video items joined with their primary stream details.
    /// Maps to library."VideoItems_v".
    /// </summary>
    public DbSet<VideoItemView> VideoItemViews => this.Set<VideoItemView>();

    /// <summary>
    /// Gets the view of per-user watch history with calculated progress.
    /// Maps to library."UserPlaybackHistory_v".
    /// </summary>
    public DbSet<UserPlaybackHistoryView> UserPlaybackHistoryViews => this.Set<UserPlaybackHistoryView>();

    /// <summary>
    /// Gets the view of items with pivoted external provider IDs.
    /// Maps to library."ItemProviders_v".
    /// </summary>
    public DbSet<ItemProviderView> ItemProviderViews => this.Set<ItemProviderView>();

    /// <summary>
    /// Gets the view of cast and crew entries linked to their items.
    /// Maps to library."ItemPeople_v".
    /// </summary>
    public DbSet<ItemPersonView> ItemPersonViews => this.Set<ItemPersonView>();

    /// <summary>
    /// Gets the aggregate library statistics view, one row per item type.
    /// Maps to library."LibrarySummary_v".
    /// </summary>
    public DbSet<LibrarySummaryView> LibrarySummaryViews => this.Set<LibrarySummaryView>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<People> Peoples => this.Set<People>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => this.Set<PeopleBaseItemMap>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
    /// </summary>
    public DbSet<BaseItemProvider> BaseItemProviders => this.Set<BaseItemProvider>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<BaseItemImageInfo> BaseItemImageInfos => this.Set<BaseItemImageInfo>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<BaseItemMetadataField> BaseItemMetadataFields => this.Set<BaseItemMetadataField>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => this.Set<BaseItemTrailerType>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/>.
    /// </summary>
    public DbSet<KeyframeData> KeyframeData => this.Set<KeyframeData>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing persisted library options.
    /// </summary>
    public DbSet<LibraryOptionsEntity> LibraryOptions => this.Set<LibraryOptionsEntity>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing TV-specific extension data for BaseItems.
    /// </summary>
    public DbSet<BaseItemTvExtras> BaseItemTvExtras => this.Set<BaseItemTvExtras>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing LiveTV-specific extension data for BaseItems.
    /// </summary>
    public DbSet<BaseItemLiveTvExtras> BaseItemLiveTvExtras => this.Set<BaseItemLiveTvExtras>();

    /// <summary>
    /// Gets the <see cref="DbSet{TEntity}"/> containing music-specific extension data for BaseItems.
    /// </summary>
    public DbSet<BaseItemAudioExtras> BaseItemAudioExtras => this.Set<BaseItemAudioExtras>();

    /*public DbSet<Artwork> Artwork => Set<Artwork>();

    public DbSet<Book> Books => Set<Book>();

    public DbSet<BookMetadata> BookMetadata => Set<BookMetadata>();

    public DbSet<Chapter> Chapters => Set<Chapter>();

    public DbSet<Collection> Collections => Set<Collection>();

    public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();

    public DbSet<Company> Companies => Set<Company>();

    public DbSet<CompanyMetadata> CompanyMetadata => Set<CompanyMetadata>();

    public DbSet<CustomItem> CustomItems => Set<CustomItem>();

    public DbSet<CustomItemMetadata> CustomItemMetadata => Set<CustomItemMetadata>();

    public DbSet<Episode> Episodes => Set<Episode>();

    public DbSet<EpisodeMetadata> EpisodeMetadata => Set<EpisodeMetadata>();

    public DbSet<Genre> Genres => Set<Genre>();

    public DbSet<Group> Groups => Set<Groups>();

    public DbSet<Library> Libraries => Set<Library>();

    public DbSet<LibraryItem> LibraryItems => Set<LibraryItems>();

    public DbSet<LibraryRoot> LibraryRoot => Set<LibraryRoot>();

    public DbSet<MediaFile> MediaFiles => Set<MediaFiles>();

    public DbSet<MediaFileStream> MediaFileStream => Set<MediaFileStream>();

    public DbSet<Metadata> Metadata => Set<Metadata>();

    public DbSet<MetadataProvider> MetadataProviders => Set<MetadataProvider>();

    public DbSet<MetadataProviderId> MetadataProviderIds => Set<MetadataProviderId>();

    public DbSet<Movie> Movies => Set<Movie>();

    public DbSet<MovieMetadata> MovieMetadata => Set<MovieMetadata>();

    public DbSet<MusicAlbum> MusicAlbums => Set<MusicAlbum>();

    public DbSet<MusicAlbumMetadata> MusicAlbumMetadata => Set<MusicAlbumMetadata>();

    public DbSet<Person> People => Set<Person>();

    public DbSet<PersonRole> PersonRoles => Set<PersonRole>();

    public DbSet<Photo> Photo => Set<Photo>();

    public DbSet<PhotoMetadata> PhotoMetadata => Set<PhotoMetadata>();

    public DbSet<ProviderMapping> ProviderMappings => Set<ProviderMapping>();

    public DbSet<Rating> Ratings => Set<Rating>();

    /// <summary>
    /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
    /// store review ratings, not age ratings.
    /// </summary>
    public DbSet<RatingSource> RatingSources => Set<RatingSource>();

    public DbSet<Release> Releases => Set<Release>();

    public DbSet<Season> Seasons => Set<Season>();

    public DbSet<SeasonMetadata> SeasonMetadata => Set<SeasonMetadata>();

    public DbSet<Series> Series => Set<Series>();

    public DbSet<SeriesMetadata> SeriesMetadata => Set<SeriesMetadata();

    public DbSet<Track> Tracks => Set<Track>();

    public DbSet<TrackMetadata> TrackMetadata => Set<TrackMetadata>();*/

    /// <inheritdoc/>
    public override async Task<int> SaveChangesAsync(
        bool acceptAllChangesOnSuccess,
        CancellationToken cancellationToken = default)
    {
        this.HandleConcurrencyToken();

        try
        {
            var result = -1;
            await entityFrameworkCoreLocking.OnSaveChangesAsync(this, async () =>
            {
                result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
            }).ConfigureAwait(false);
            return result;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            // Concurrency conflicts can occur when marking items as played/unplayed concurrently.
            // Strategy: Three-way merge of original → client → database values for intelligent conflict resolution.
            // Reference: https://learn.microsoft.com/en-us/ef/core/saving/concurrency#resolving-concurrency-conflicts
            logger.LogWarning(ex, "Concurrency exception detected, attempting three-way merge-based conflict resolution with exponential backoff.");

            var maxRetries = 3;
            var delay = TimeSpan.FromMilliseconds(100);

            for (var i = 0; i < maxRetries; i++)
            {
                try
                {
                    // Process each conflicted entry
                    var entriesToRemove = new List<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>();

                    foreach (var entry in ex.Entries)
                    {
                        try
                        {
                            // Step 1: Capture the client's pending changes before reload
                            // These are the values the client tried to save
                            var clientValues = entry.CurrentValues.Clone();

                            // Step 2: Capture the original values (what client loaded from DB)
                            // We need this BEFORE reload to see what changed on client side
                            var originalValues = entry.OriginalValues.Clone();

                            // Step 3: Reload the entity from database to get latest values
                            // This updates entry.CurrentValues with current database state
                            await entry.ReloadAsync(cancellationToken).ConfigureAwait(false);

                            // Check if the entity still exists in the database
                            if (entry.State == EntityState.Detached)
                            {
                                logger.LogInformation("Entity {EntityType} was deleted by another operation, skipping merge.", entry.Entity.GetType().Name);
                                entriesToRemove.Add(entry);
                            }
                            else
                            {
                                // Step 4: Perform three-way merge
                                // For each property, determine: client changed it? DB changed it? Apply accordingly
                                PerformThreeWayMerge(entry, originalValues, clientValues);

                                // Step 5: Mark as modified and increment concurrency token
                                entry.State = EntityState.Modified;

                                if (entry.Entity is IHasConcurrencyToken concurrencyEntity)
                                {
                                    concurrencyEntity.OnSavingChanges();
                                }

                                logger.LogDebug("Three-way merge completed for {EntityType}", entry.Entity.GetType().Name);
                            }
                        }
                        catch (Exception reloadEx)
                        {
                            logger.LogWarning(reloadEx, "Failed to merge entity {EntityType}, treating as deleted.", entry.Entity.GetType().Name);
                            entriesToRemove.Add(entry);
                        }
                    }

                    // Detach entries that were deleted or couldn't be merged
                    foreach (var entryToRemove in entriesToRemove)
                    {
                        this.Entry(entryToRemove.Entity).State = EntityState.Detached;
                    }

                    // If all entries were deleted, no point in retrying
                    if (entriesToRemove.Count == ex.Entries.Count && ex.Entries.Count > 0)
                    {
                        logger.LogInformation("All {Count} conflicted entities were deleted by other operations. Continuing without them.", ex.Entries.Count);
                        return 0; // Return 0 to indicate no rows were affected
                    }

                    // Retry saving changes after merging conflicts
                    var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
                    if (result >= 0)
                    {
                        logger.LogInformation("Concurrency merge retry {RetryCount} succeeded, saved {RowsAffected} row(s).", i + 1, result);
                    }

                    return result;
                }
                catch (DbUpdateConcurrencyException retryEx) when (i < maxRetries - 1)
                {
                    logger.LogWarning(retryEx, "Concurrency conflict persisted on merge retry {RetryCount}, retrying with backoff ({Delay}ms).", i + 1, delay.TotalMilliseconds);
                    await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
                    delay *= 2; // Exponential backoff
                }
            }

            // If all retries fail, rethrow the last exception
            throw;
        }
        catch (DbUpdateException ex)
        {
            // Check if it's a constraint violation (works across all database providers)
            var isConstraintViolation = ex.InnerException?.GetType().Name.Contains("Exception", StringComparison.Ordinal) == true &&
                                       (ex.Message.Contains("constraint", StringComparison.OrdinalIgnoreCase) ||
                                        ex.Message.Contains("duplicate", StringComparison.OrdinalIgnoreCase) ||
                                        ex.Message.Contains("unique", StringComparison.OrdinalIgnoreCase));

            if (isConstraintViolation)
            {
                // Log constraint violations as warnings (expected in some concurrent scenarios)
#pragma warning disable CA1848 // Use LoggerMessage delegates - exception type is dynamic
                logger.LogWarning(
                    ex,
                    "Database constraint violation: Attempted to insert or update data that violates a database constraint. " +
                    "This may indicate a concurrency issue or a bug in the update logic. " +
                    "Inner exception: {InnerExceptionType}",
                    ex.InnerException?.GetType().Name ?? "unknown");
#pragma warning restore CA1848
            }
            else
            {
                // Other DbUpdateExceptions are true errors
                SaveChangesError(logger, ex);
            }

            throw;
        }
        catch (Exception e)
        {
            SaveChangesError(logger, e);
            throw;
        }
    }

    /// <inheritdoc/>
    public override int SaveChanges(bool acceptAllChangesOnSuccess) // SaveChanges(bool) is beeing called by SaveChanges() with default to false.
    {
        this.HandleConcurrencyToken();

        try
        {
            var result = -1;
            entityFrameworkCoreLocking.OnSaveChanges(this, () =>
            {
                result = base.SaveChanges(acceptAllChangesOnSuccess);
            });
            return result;
        }
        catch (Exception e)
        {
            SaveChangesError(logger, e);
            throw;
        }
    }

    private void HandleConcurrencyToken()
    {
        foreach (var saveEntity in this.ChangeTracker.Entries()
                     .Where(e => e.State == EntityState.Modified)
                     .Select(entry => entry.Entity)
                     .OfType<IHasConcurrencyToken>())
        {
            saveEntity.OnSavingChanges();
        }
    }

    /// <summary>
    /// Performs a three-way merge to intelligently resolve concurrency conflicts.
    /// Merges original (loaded) → client (attempted) → database (current) values.
    ///
    /// Algorithm for each property:
    /// - If client changed it AND database didn't → use client value
    /// - If database changed it AND client didn't → use database value
    /// - If both changed it → conflict detected, use database value (safe default)
    /// - If neither changed it → no change needed
    /// </summary>
    /// <param name="entry">The conflicted entry from the change tracker.</param>
    /// <param name="originalValues">The original values when client loaded the entity.</param>
    /// <param name="clientValues">The values the client attempted to save.</param>
    private void PerformThreeWayMerge(
        Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry,
        Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues originalValues,
        Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues clientValues)
    {
        try
        {
            var properties = entry.Metadata.GetProperties();
            var mergedCount = 0;
            var conflictCount = 0;

            foreach (var property in properties)
            {
                // Skip concurrency tokens and key properties - these are handled separately
                if (property.IsPrimaryKey() || property.Name.Equals("RowVersion", StringComparison.Ordinal))
                {
                    continue;
                }

                try
                {
                    var originalValue = originalValues[property];
                    var clientValue = clientValues[property];
                    var databaseValue = entry.CurrentValues[property];

                    var originalEqualsClient = ValuesEqual(originalValue, clientValue);
                    var originalEqualsDatabase = ValuesEqual(originalValue, databaseValue);
                    var clientEqualsDatabase = ValuesEqual(clientValue, databaseValue);

                    if (originalEqualsClient && originalEqualsDatabase)
                    {
                        // No changes on either side - nothing to do
                        continue;
                    }

                    if (!originalEqualsClient && originalEqualsDatabase)
                    {
                        // Client changed, database didn't → Apply client change
                        entry.CurrentValues[property] = clientValue;
                        mergedCount++;
                        logger.LogDebug("Merged {Property}: client changed, database didn't. Applied client value.", property.Name);
                    }
                    else if (originalEqualsClient && !originalEqualsDatabase)
                    {
                        // Database changed, client didn't → Keep database value (already set)
                        mergedCount++;
                        logger.LogDebug("Merged {Property}: database changed, client didn't. Kept database value.", property.Name);
                    }
                    else if (!originalEqualsClient && !originalEqualsDatabase && !clientEqualsDatabase)
                    {
                        // Both sides changed AND they're different → Conflict!
                        // Strategy: Keep database value as it's more recent
                        conflictCount++;
                        logger.LogWarning("Conflict on {Property}: both client and database changed to different values. Keeping database value.", property.Name);
                    }
                    else if (!originalEqualsClient && !originalEqualsDatabase && clientEqualsDatabase)
                    {
                        // Both sides changed but ended up at same value → No conflict, already correct
                        mergedCount++;
                        logger.LogDebug("Merged {Property}: both changed to same value. No conflict.", property.Name);
                    }
                }
                catch (Exception propEx)
                {
                    logger.LogWarning(propEx, "Error merging property {Property}, skipping.", property.Name);
                }
            }

            if (conflictCount > 0)
            {
                logger.LogInformation("Three-way merge completed with {MergedCount} successful merges and {ConflictCount} conflicts (database values preserved).", mergedCount, conflictCount);
            }
            else
            {
                logger.LogInformation("Three-way merge completed successfully: {MergedCount} property merges applied.", mergedCount);
            }
        }
        catch (Exception mergeEx)
        {
            logger.LogWarning(mergeEx, "Error during three-way merge. Proceeding with current database values.");
        }
    }

    /// <summary>
    /// Compares two values for equality, handling nulls correctly.
    /// </summary>
    private static bool ValuesEqual(object? value1, object? value2)
    {
        if (ReferenceEquals(value1, value2))
        {
            return true;
        }

        if (value1 == null || value2 == null)
        {
            return false;
        }

        return value1.Equals(value2);
    }

    /// <summary>
    /// Gets the recommended batch size for bulk operations based on the number of items to process.
    /// Smaller batches during library scans reduce dead tuple generation in PostgreSQL.
    /// </summary>
    /// <param name="entityCount">The total number of entities being processed.</param>
    /// <returns>The recommended batch size for optimal performance.</returns>
    public int GetBatchSize(int entityCount)
    {
        // Smaller batches during media scans = fewer dead tuples
        // < 100 items: batch 10
        // 100-1000: batch 50
        // 1000-10000: batch 100
        // > 10000: batch 200
        return entityCount switch
        {
            < 100 => 10,
            < 1000 => 50,
            < 10000 => 100,
            _ => 200
        };
    }

    /// <inheritdoc />
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        jellyfinDatabaseProvider.OnModelCreating(modelBuilder);
        base.OnModelCreating(modelBuilder);

        ArgumentNullException.ThrowIfNull(modelBuilder);

        // Configuration for each entity is in its own class inside 'ModelConfiguration'.
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
    }

    /// <inheritdoc />
    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        jellyfinDatabaseProvider.ConfigureConventions(configurationBuilder);
        base.ConfigureConventions(configurationBuilder);
    }
}
