using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata;

namespace Ryujinx.Ava.Utilities
{
    public static class TitleUpdatesHelper
    {
        private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());

        public static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
        {
            var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);

            if (!File.Exists(titleUpdatesJsonPath))
            {
                return [];
            }

            try
            {
                var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata);
                return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase);
            }
            catch
            {
                Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}");
                return [];
            }
        }

        public static void SaveTitleUpdatesJson(ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
        {
            var titleUpdateWindowData = new TitleUpdateMetadata
            {
                Selected = string.Empty,
                Paths = [],
            };

            foreach ((TitleUpdateModel update, bool isSelected) in updates)
            {
                titleUpdateWindowData.Paths.Add(update.Path);
                if (isSelected)
                {
                    if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected))
                    {
                        Logger.Error?.Print(LogClass.Application,
                            $"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}");
                        return;
                    }

                    titleUpdateWindowData.Selected = update.Path;
                }
            }

            var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
            JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
        }

        private static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
        {
            var result = new List<(TitleUpdateModel, bool IsSelected)>();

            IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
                ? IntegrityCheckLevel.ErrorOnInvalid
                : IntegrityCheckLevel.None;

            foreach (string path in titleUpdateMetadata.Paths)
            {
                if (!File.Exists(path))
                    continue;

                try
                {
                    using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs);

                    Dictionary<ulong, ContentMetaData> updates =
                        pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel);

                    if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content))
                        continue;

                    Nca patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program);
                    Nca controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control);

                    if (controlNca is null || patchNca is null)
                        continue;

                    ApplicationControlProperty controlData = new();

                    using UniqueRef<IFile> nacpFile = new();

                    controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
                        .OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
                    nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
                        .ThrowIfFailure();

                    var displayVersion = controlData.DisplayVersionString.ToString();
                    var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version,
                        displayVersion, path);

                    result.Add((update, path == titleUpdateMetadata.Selected));
                }
                catch (MissingKeyException exception)
                {
                    Logger.Warning?.Print(LogClass.Application,
                        $"Your key set is missing a key with the name: {exception.Name}");
                }
                catch (InvalidDataException)
                {
                    Logger.Warning?.Print(LogClass.Application,
                        $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Malformed File: {path}");
                }
                catch (IOException exception)
                {
                    Logger.Warning?.Print(LogClass.Application, exception.Message);
                }
                catch (Exception exception)
                {
                    Logger.Warning?.Print(LogClass.Application,
                        $"The file encountered was not of a valid type. File: '{path}' Error: {exception}");
                }
            }

            return result;
        }

        private static string PathToGameUpdatesJson(ulong applicationIdBase)
            => Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json");
    }
}
