using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// 
/// Migration to move extracted files to the new directories.
/// 
[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData))]
public class MigrateKeyframeData : IDatabaseMigrationRoutine
{
    private readonly IStartupLogger _logger;
    private readonly IApplicationPaths _appPaths;
    private readonly IDbContextFactory _dbProvider;
    private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
    /// 
    /// Initializes a new instance of the  class.
    /// 
    /// The startup logger for Startup UI intigration.
    /// Instance of the  interface.
    /// The EFCore db factory.
    public MigrateKeyframeData(
        IStartupLogger startupLogger,
        IApplicationPaths appPaths,
        IDbContextFactory dbProvider)
    {
        _logger = startupLogger;
        _appPaths = appPaths;
        _dbProvider = dbProvider;
    }
    private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
    /// 
    public void Perform()
    {
        const int Limit = 5000;
        int itemCount = 0, offset = 0;
        var sw = Stopwatch.StartNew();
        using var context = _dbProvider.CreateDbContext();
        var baseQuery = context.BaseItems.Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b.IsFolder).OrderBy(e => e.Id);
        var records = baseQuery.Count();
        _logger.LogInformation("Checking {Count} items for importable keyframe data.", records);
        context.KeyframeData.ExecuteDelete();
        using var transaction = context.Database.BeginTransaction();
        do
        {
            var results = baseQuery.Skip(offset).Take(Limit).Select(b => new Tuple(b.Id, b.Path)).ToList();
            foreach (var result in results)
            {
                if (TryGetKeyframeData(result.Item1, result.Item2, out var data))
                {
                    itemCount++;
                    context.KeyframeData.Add(data);
                }
            }
            offset += Limit;
            if (offset > records)
            {
                offset = records;
            }
            _logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
        } while (offset < records);
        context.SaveChanges();
        transaction.Commit();
        _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
        if (Directory.Exists(KeyframeCachePath))
        {
            Directory.Delete(KeyframeCachePath, true);
        }
    }
    private bool TryGetKeyframeData(Guid id, string? path, [NotNullWhen(true)] out KeyframeData? data)
    {
        data = null;
        if (!string.IsNullOrEmpty(path))
        {
            var cachePath = GetCachePath(KeyframeCachePath, path);
            if (TryReadFromCache(cachePath, out var keyframeData))
            {
                data = new()
                {
                    ItemId = id,
                    KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
                    TotalDuration = keyframeData.TotalDuration
                };
                return true;
            }
        }
        return false;
    }
    private string? GetCachePath(string keyframeCachePath, string filePath)
    {
        DateTime? lastWriteTimeUtc;
        try
        {
            lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
        }
        catch (IOException e)
        {
            _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
            return null;
        }
        ReadOnlySpan filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
        var prefix = filename[..1];
        return Path.Join(keyframeCachePath, prefix, filename);
    }
    private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
    {
        if (File.Exists(cachePath))
        {
            var bytes = File.ReadAllBytes(cachePath);
            cachedResult = JsonSerializer.Deserialize(bytes, _jsonOptions);
            return cachedResult is not null;
        }
        cachedResult = null;
        return false;
    }
}