using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.SRFPlay.Api.Models;
using Jellyfin.Plugin.SRFPlay.Configuration;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using Jellyfin.Plugin.SRFPlay.Utilities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.SRFPlay.Services;

/// <summary>
/// Removes stale plugin-owned resume points from every user's "Continue Watching" row.
/// </summary>
public class ResumeCleanupService : IResumeCleanupService
{
    /// <summary>
    /// Provider ID key stamped onto every item built from the SRG API.
    /// </summary>
    private const string SrfProviderId = "SRF";

    private readonly ILogger<ResumeCleanupService> _logger;
    private readonly ILibraryManager _libraryManager;
    private readonly IUserManager _userManager;
    private readonly IUserDataManager _userDataManager;

    /// <summary>
    /// Initializes a new instance of the <see cref="ResumeCleanupService"/> class.
    /// </summary>
    /// <param name="loggerFactory">The logger factory.</param>
    /// <param name="libraryManager">The library manager.</param>
    /// <param name="userManager">The user manager.</param>
    /// <param name="userDataManager">The user data manager.</param>
    public ResumeCleanupService(
        ILoggerFactory loggerFactory,
        ILibraryManager libraryManager,
        IUserManager userManager,
        IUserDataManager userDataManager)
    {
        _logger = loggerFactory.CreateLogger<ResumeCleanupService>();
        _libraryManager = libraryManager;
        _userManager = userManager;
        _userDataManager = userDataManager;
    }

    /// <inheritdoc />
    public ResumeCleanupResult Cleanup(bool clearAll, CancellationToken cancellationToken)
    {
        var result = new ResumeCleanupResult();
        var config = Plugin.Instance?.Configuration;

        if (config == null)
        {
            _logger.LogWarning("Plugin configuration not available - skipping resume point cleanup");
            return result;
        }

        if (!clearAll && !config.CleanUpResumePoints)
        {
            _logger.LogDebug("Resume point cleanup is disabled in configuration");
            return result;
        }

        var channelIds = GetPluginChannelIds();

        foreach (var user in _userManager.Users)
        {
            cancellationToken.ThrowIfCancellationRequested();
            result.UsersChecked++;

            var query = new InternalItemsQuery(user)
            {
                IsResumable = true,
                Recursive = true,
                EnableTotalRecordCount = false,
                DtoOptions = new DtoOptions(false)
            };

            foreach (var item in _libraryManager.GetItemList(query))
            {
                cancellationToken.ThrowIfCancellationRequested();

                if (!IsPluginItem(item, channelIds))
                {
                    continue;
                }

                result.Inspected++;

                var userData = _userDataManager.GetUserData(user, item);
                if (userData == null || userData.PlaybackPositionTicks <= 0)
                {
                    continue;
                }

                var reason = clearAll ? "clear all" : GetStaleReason(item, userData, config);
                if (reason == null)
                {
                    continue;
                }

                if (ClearResumePoint(item, user, userData, reason))
                {
                    result.Cleared++;
                    result.ClearedByReason.TryGetValue(reason, out var count);
                    result.ClearedByReason[reason] = count + 1;
                }
            }
        }

        _logger.LogInformation(
            "Resume point cleanup finished: {Cleared} of {Inspected} SRF Play resume points cleared across {Users} user(s)",
            result.Cleared,
            result.Inspected,
            result.UsersChecked);

        return result;
    }

    /// <inheritdoc />
    public int ClearResumePoint(BaseItem item, IEnumerable<User> users, string reason)
    {
        ArgumentNullException.ThrowIfNull(item);
        ArgumentNullException.ThrowIfNull(users);

        var cleared = 0;

        foreach (var user in users)
        {
            var userData = _userDataManager.GetUserData(user, item);
            if (userData == null || userData.PlaybackPositionTicks <= 0)
            {
                continue;
            }

            if (ClearResumePoint(item, user, userData, reason))
            {
                cleared++;
            }
        }

        return cleared;
    }

    /// <inheritdoc />
    public bool IsPluginItem(BaseItem item)
    {
        return IsPluginItem(item, GetPluginChannelIds());
    }

    /// <inheritdoc />
    public int ClearLiveStreamResumePoint(BaseItem item, IEnumerable<User> users)
    {
        if (item == null || Plugin.Instance?.Configuration.ClearLiveStreamResumePoints != true)
        {
            return 0;
        }

        // Only the URN heuristic here - the item is already known, and re-querying the channel
        // list on every playback stop would be wasteful.
        if (!UrnHelper.IsLivestreamUrn(item.ProviderIds.GetValueOrDefault(SrfProviderId)))
        {
            return 0;
        }

        return ClearResumePoint(item, users, "livestream stopped");
    }

    /// <summary>
    /// Determines whether an item belongs to one of this plugin's channels.
    /// </summary>
    /// <param name="item">The item to test.</param>
    /// <param name="channelIds">The set of channel IDs owned by this plugin.</param>
    /// <returns>True if the item is owned by this plugin.</returns>
    private static bool IsPluginItem(BaseItem item, HashSet<Guid> channelIds)
    {
        if (item == null)
        {
            return false;
        }

        // API-sourced content carries the URN as a provider ID. Recordings do not, so fall back
        // to the owning channel, which also covers any item shape added later.
        return item.ProviderIds.ContainsKey(SrfProviderId)
            || (!item.ChannelId.Equals(Guid.Empty) && channelIds.Contains(item.ChannelId));
    }

    /// <summary>
    /// Resolves the IDs of the channel entries created by this plugin, one per business unit.
    /// </summary>
    /// <returns>The set of channel IDs, empty if the channels have not been scanned yet.</returns>
    private HashSet<Guid> GetPluginChannelIds()
    {
        // SrgChannelBase names every channel "{unit} Play".
        var names = Enum.GetNames<BusinessUnit>()
            .Select(unit => unit + " Play")
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        var channels = _libraryManager.GetItemList(new InternalItemsQuery
        {
            IncludeItemTypes = new[] { BaseItemKind.Channel },
            EnableTotalRecordCount = false,
            DtoOptions = new DtoOptions(false)
        });

        return channels
            .Where(channel => channel.Name != null && names.Contains(channel.Name))
            .Select(channel => channel.Id)
            .ToHashSet();
    }

    /// <summary>
    /// Decides whether a resume point is stale, and why.
    /// </summary>
    /// <param name="item">The item the resume point belongs to.</param>
    /// <param name="userData">The user data holding the resume position.</param>
    /// <param name="config">The plugin configuration supplying the thresholds.</param>
    /// <returns>A short reason string, or null when the resume point should be kept.</returns>
    private static string? GetStaleReason(BaseItem item, UserItemData userData, PluginConfiguration config)
    {
        // A livestream has no meaningful position to return to: by the time the user comes back
        // the broadcast has moved on or ended. These are the entries that pile up the fastest.
        if (config.ClearLiveStreamResumePoints
            && UrnHelper.IsLivestreamUrn(item.ProviderIds.GetValueOrDefault(SrfProviderId)))
        {
            return "livestream";
        }

        if (config.ResumePointMaxAgeDays > 0
            && userData.LastPlayedDate.HasValue
            && userData.LastPlayedDate.Value.ToUniversalTime() < DateTime.UtcNow.AddDays(-config.ResumePointMaxAgeDays))
        {
            return "older than " + config.ResumePointMaxAgeDays.ToString(CultureInfo.InvariantCulture) + " days";
        }

        if (config.ResumePointMinPositionSeconds > 0
            && userData.PlaybackPositionTicks <= TimeSpan.FromSeconds(config.ResumePointMinPositionSeconds).Ticks)
        {
            return "barely started";
        }

        // Runtime is unknown for live and for some API items; without it there is nothing to
        // compare the position against, so leave the resume point alone.
        if (config.ResumePointCompletedPercent > 0
            && item.RunTimeTicks.HasValue
            && item.RunTimeTicks.Value > 0)
        {
            var watchedPercent = userData.PlaybackPositionTicks * 100.0 / item.RunTimeTicks.Value;
            if (watchedPercent >= config.ResumePointCompletedPercent)
            {
                return "watched to the end";
            }
        }

        return null;
    }

    /// <summary>
    /// Zeroes the resume position, which removes the item from "Continue Watching" while
    /// leaving it unwatched so it can still be played again from the start.
    /// </summary>
    /// <param name="item">The item to clear.</param>
    /// <param name="user">The user to clear it for.</param>
    /// <param name="userData">The user data to update.</param>
    /// <param name="reason">A short reason, logged for traceability.</param>
    /// <returns>True if the resume point was cleared.</returns>
    private bool ClearResumePoint(BaseItem item, User user, UserItemData userData, string reason)
    {
        try
        {
            userData.PlaybackPositionTicks = 0;

            _userDataManager.SaveUserData(
                user,
                item,
                userData,
                UserDataSaveReason.UpdateUserData,
                CancellationToken.None);

            _logger.LogInformation(
                "Cleared resume point for '{Name}' (user {User}, reason: {Reason})",
                item.Name,
                user.Username,
                reason);

            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to clear resume point for '{Name}'", item.Name);
            return false;
        }
    }
}
