// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Platform;
using osu.Game.Online.API;

namespace osu.Game.Beatmaps
{
    /// <summary>
    /// A component which handles population of online IDs for beatmaps using a two part lookup procedure.
    /// </summary>
    public class BeatmapUpdaterMetadataLookup : IDisposable
    {
        private readonly IOnlineBeatmapMetadataSource apiMetadataSource;
        private readonly IOnlineBeatmapMetadataSource localCachedMetadataSource;

        public BeatmapUpdaterMetadataLookup(IAPIProvider api, Storage storage)
            : this(new APIBeatmapMetadataSource(api), new LocalCachedBeatmapMetadataSource(storage))
        {
        }

        internal BeatmapUpdaterMetadataLookup(IOnlineBeatmapMetadataSource apiMetadataSource, IOnlineBeatmapMetadataSource localCachedMetadataSource)
        {
            this.apiMetadataSource = apiMetadataSource;
            this.localCachedMetadataSource = localCachedMetadataSource;
        }

        /// <summary>
        /// Queue an update for a beatmap set.
        /// </summary>
        /// <remarks>
        /// This may happen during initial import, or at a later stage in response to a user action or server event.
        /// </remarks>
        /// <param name="beatmapSet">The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed).</param>
        /// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param>
        public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch)
        {
            var lookupResults = new List<OnlineBeatmapMetadata?>();

            foreach (var beatmapInfo in beatmapSet.Beatmaps)
            {
                // note that these lookups DO NOT ACTUALLY FULLY GUARANTEE that the beatmap is what it claims it is,
                // i.e. the correctness of this lookup should be treated as APPROXIMATE AT WORST.
                // this is because the beatmap filename is used as a fallback in some scenarios where the MD5 of the beatmap may mismatch.
                // this is considered to be an acceptable casualty so that things can continue to work as expected for users in some rare scenarios
                // (stale beatmap files in beatmap packs, beatmap mirror desyncs).
                // however, all this means that other places such as score submission ARE EXPECTED TO VERIFY THE MD5 OF THE BEATMAP AGAINST THE ONLINE ONE EXPLICITLY AGAIN.
                //
                // additionally note that the online ID stored to the map is EXPLICITLY NOT USED because some users in a silly attempt to "fix" things for themselves on stable
                // would reuse online IDs of already submitted beatmaps, which means that information is pretty much expected to be bogus in a nonzero number of beatmapsets.
                if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res))
                    continue;

                if (res == null)
                {
                    beatmapInfo.ResetOnlineInfo();
                    lookupResults.Add(null); // mark lookup failure
                    continue;
                }

                lookupResults.Add(res);

                beatmapInfo.OnlineID = res.BeatmapID;
                beatmapInfo.OnlineMD5Hash = res.MD5Hash;
                beatmapInfo.LastOnlineUpdate = res.LastUpdated;

                Debug.Assert(beatmapInfo.BeatmapSet != null);
                beatmapInfo.BeatmapSet.OnlineID = res.BeatmapSetID;

                // Some metadata should only be applied if there's no local changes.
                if (beatmapInfo.MatchesOnlineVersion)
                {
                    beatmapInfo.Status = res.BeatmapStatus;
                    beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
                }
            }

            if (beatmapSet.Beatmaps.All(b => b.MatchesOnlineVersion)
                && lookupResults.All(r => r != null)
                && lookupResults.Select(r => r!.BeatmapSetID).Distinct().Count() == 1)
            {
                var representative = lookupResults.First()!;

                beatmapSet.Status = representative.BeatmapSetStatus ?? BeatmapOnlineStatus.None;
                beatmapSet.DateRanked = representative.DateRanked;
                beatmapSet.DateSubmitted = representative.DateSubmitted;
            }
        }

        /// <summary>
        /// Attempts to retrieve the <see cref="OnlineBeatmapMetadata"/> for the given <paramref name="beatmapInfo"/>.
        /// </summary>
        /// <param name="beatmapInfo">The beatmap to perform the online lookup for.</param>
        /// <param name="preferOnlineFetch">Whether online sources should be preferred for the lookup.</param>
        /// <param name="result">The result of the lookup. Can be <see langword="null"/> if no matching beatmap was found (or the lookup failed).</param>
        /// <returns>
        /// <see langword="true"/> if any of the metadata sources were available and returned a valid <paramref name="result"/>.
        /// <see langword="false"/> if none of the metadata sources were available, or if there was insufficient data to return a valid <paramref name="result"/>.
        /// </returns>
        /// <remarks>
        /// There are two cases wherein this method will return <see langword="false"/>:
        /// <list type="bullet">
        /// <item>If neither the local cache or the API are available to query.</item>
        /// <item>If the API is not available to query, and a positive match was not made in the local cache.</item>
        /// </list>
        /// In either case, the online ID read from the .osu file will be preserved, which may not necessarily be what we want.
        /// TODO: reconsider this if/when a better flow for queueing online retrieval is implemented.
        /// </remarks>
        private bool tryLookup(BeatmapInfo beatmapInfo, bool preferOnlineFetch, out OnlineBeatmapMetadata? result)
        {
            bool useLocalCache = !apiMetadataSource.Available || !preferOnlineFetch;
            if (useLocalCache && localCachedMetadataSource.TryLookup(beatmapInfo, out result))
                return true;

            if (apiMetadataSource.TryLookup(beatmapInfo, out result))
                return true;

            result = null;
            return false;
        }

        public void Dispose()
        {
            apiMetadataSource.Dispose();
            localCachedMetadataSource.Dispose();
        }
    }
}