using Flurl;
using Flurl.Http;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WowUp.Common.Enums;
using WowUp.Common.Models;
using WowUp.Common.Models.Addons;
using WowUp.Common.Models.Curse;
using WowUp.Common.Services.Contracts;
using WowUp.WPF.AddonProviders.Contracts;
using WowUp.WPF.AddonProviders.Curse;
using WowUp.WPF.Entities;
using WowUp.WPF.Extensions;
using WowUp.WPF.Models.Curse;
using WowUp.WPF.Models.WowUp;
using WowUp.WPF.Services.Contracts;
using WowUp.WPF.Utilities;

namespace WowUp.WPF.AddonProviders
{
    public class CurseAddonProvider : ICurseAddonProvider
    {
        private const string ApiUrl = "https://addons-ecs.forgesvc.net/api/v2";
        private const string ClassicGameVersionFlavor = "wow_classic";
        private const string RetailGameVersionFlavor = "wow_retail";

        private readonly ICacheService _cacheService;
        private readonly IAnalyticsService _analyticsService;

        public string Name => "Curse";

        public CurseAddonProvider(
            IAnalyticsService analyticsService,
            ICacheService cacheService)
        {
            _analyticsService = analyticsService;
            _cacheService = cacheService;
        }

        public async Task Scan(
            WowClientType clientType,
            AddonChannelType addonChannelType,
            IEnumerable<AddonFolder> addonFolders)
        {
            var addonDirectory = addonFolders.FirstOrDefault()?.Directory.Parent.FullName;
            var scanResults = await GetScanResults(addonFolders);

            await MapAddonFolders(scanResults, clientType);

            var addonIds = scanResults
                .Where(sr => sr.ExactMatch != null)
                .Select(sr => sr.ExactMatch.Id.ToString())
                .Distinct();

            //var addonIdStr = string.Join(",", addonIds);

            var addonResults = await GetAllIds(addonIds);

            foreach (var addonFolder in addonFolders)
            {
                var scanResult = scanResults.First(sr => sr.AddonFolder.Name == addonFolder.Name);
                if (scanResult.ExactMatch == null)
                {
                    continue;
                }

                scanResult.SearchResult = addonResults.FirstOrDefault(ar => ar.Id == scanResult.ExactMatch.Id);
                if (scanResult.SearchResult == null)
                {
                    continue;
                }

                try
                {
                    addonFolder.MatchingAddon = GetAddon(clientType, addonChannelType, scanResult);
                }
                catch (Exception ex)
                {
                    _analyticsService.Track(ex, $"Failed to create addon for result {scanResult.FolderScanner.Fingerprint}");
                }
            }
        }

        public bool IsValidAddonUri(Uri addonUri)
        {
            return string.IsNullOrEmpty(addonUri.Host) == false &&
                addonUri.Host.EndsWith("curseforge.com") &&
                addonUri.LocalPath.StartsWith("/wow/addons");
        }

        public async Task<AddonSearchResult> GetById(
            string addonId,
            WowClientType clientType)
        {
            var result = await GetSearchResult(addonId);
            if (result == null)
            {
                return null;
            }

            var latestFiles = GetLatestFiles(result, clientType);
            if (!latestFiles.Any())
            {
                return null;
            }

            return GetAddonSearchResult(result, latestFiles);
        }

        public async Task<CurseSearchResult> GetSearchResult(string addonId)
        {
            var url = $"{ApiUrl}/addon/{addonId}";

            return await _cacheService.GetCache(url, async () =>
            {
                return await url
                    .WithHeaders(HttpUtilities.DefaultHeaders)
                    .GetJsonAsync<CurseSearchResult>();
            });
        }

        public async Task<IEnumerable<PotentialAddon>> Search(string query, WowClientType clientType)
        {
            var searchResults = new List<PotentialAddon>();

            var response = await GetSearchResults(query);
            foreach (var result in response)
            {
                var latestFiles = GetLatestFiles(result, clientType);
                if (!latestFiles.Any())
                {
                    continue;
                }

                searchResults.Add(GetPotentialAddon(result));
            }

            return searchResults;
        }

        /// <summary>
        /// This is a basic method, curse api does not search via slug, so you have to get lucky basically.
        /// Could pre-make a map for slug to addon if wanted.
        /// </summary>
        /// <param name="addonUri"></param>
        /// <param name="clientType"></param>
        /// <returns></returns>
        public async Task<PotentialAddon> Search(Uri addonUri, WowClientType clientType)
        {
            var addonSlug = addonUri.LocalPath.Split('/').Last();
            var response = await GetSearchResults(addonSlug);
            var result = response.FirstOrDefault(res => res.Slug == addonSlug);
            if (result == null)
            {
                return null;
            }

            var latestFiles = GetLatestFiles(result, clientType);
            if (!latestFiles.Any())
            {
                return null;
            }

            return GetPotentialAddon(result);
        }

        public async Task<IEnumerable<AddonSearchResult>> Search(
            string addonName,
            string folderName,
            WowClientType clientType,
            string nameOverride = null)
        {
            var results = new List<AddonSearchResult>();

            var matches = await Search(addonName, folderName, clientType);

            foreach (var match in matches)
            {
                var latestFiles = GetLatestFiles(match, clientType);
                var searchResult = GetAddonSearchResult(match, latestFiles);
                if (searchResult != null)
                {
                    results.Add(searchResult);
                }
            }

            return results;
        }

        private async Task<List<CurseSearchResult>> Search(
            string addonName,
            string folderName,
            WowClientType clientType)
        {
            var response = await GetSearchResults(addonName);
            var matches = FilterResults(response, addonName, folderName, clientType);

            return matches.Where(m => GetLatestFiles(m, clientType).Any()).ToList();
        }

        public async Task<IList<AddonSearchResult>> GetAll(WowClientType clientType, IEnumerable<string> addonIds)
        {
            var addonResults = new List<AddonSearchResult>();
            if (!addonIds.Any())
            {
                return addonResults;
            }

            var results = await GetAllIds(addonIds);

            foreach (var result in results)
            {
                var latestFiles = GetLatestFiles(result, clientType);
                if (!latestFiles.Any())
                {
                    continue;
                }

                var searchResult = GetAddonSearchResult(result, latestFiles);
                if (searchResult != null)
                {
                    addonResults.Add(searchResult);
                }
            }

            return addonResults;
        }

        public async Task<IList<PotentialAddon>> GetFeaturedAddons(WowClientType clientType)
        {
            var featured = await GetFeaturedAddonList();

            featured = FilterClientType(featured, clientType);

            return featured.Select(f => GetPotentialAddon(f)).ToList();
        }

        public void OnPostInstall(Addon addon)
        {
        }

        public async Task<List<CurseScanResult>> GetScanResults(IEnumerable<AddonFolder> addonFolders)
        {
            var scanResults = new ConcurrentBag<CurseScanResult>();
            await addonFolders.ForEachAsync(3, async addonFolder =>
            {
                var scanner = await new CurseFolderScanner(addonFolder.Directory).ScanFolder();

                var scanResult = new CurseScanResult
                {
                    FolderScanner = scanner,
                    AddonFolder = addonFolder
                };

                scanResults.Add(scanResult);
            });

            return scanResults.ToList();
        }

        private Addon GetAddon(
            WowClientType clientType,
            AddonChannelType addonChannelType,
            AddonFolder addonFolder,
            CurseSearchResult searchResult)
        {
            if (addonFolder == null || searchResult == null)
            {
                return null;
            }

            var currentVersion = addonFolder.Toc.Version;
            var latestFile = GetLatestFiles(searchResult, clientType).First();
            var authors = GetAuthor(searchResult);

            return new Addon
            {
                Author = authors,
                Name = searchResult.Name,
                ChannelType = addonChannelType,
                AutoUpdateEnabled = false,
                ClientType = clientType,
                DownloadUrl = latestFile.DownloadUrl,
                ExternalUrl = searchResult.WebsiteUrl,
                ExternalId = searchResult.Id.ToString(),
                FolderName = addonFolder.Name,
                GameVersion = latestFile.GameVersion.FirstOrDefault(),
                InstalledAt = DateTime.Now,
                InstalledFolders = string.Join(",", GetFolderNames(latestFile)),
                InstalledVersion = currentVersion,
                IsIgnored = false,
                LatestVersion = latestFile.DisplayName,
                ProviderName = Name,
                ThumbnailUrl = GetThumbnailUrl(searchResult)
            };
        }

        private Addon GetAddon(
            WowClientType clientType,
            AddonChannelType addonChannelType,
            CurseScanResult scanResult)
        {
            var currentVersion = scanResult.ExactMatch.File;
            var authors = string.Join(", ", scanResult.SearchResult.Authors.Select(author => author.Name));
            var folderList = string.Join(",", scanResult.ExactMatch.File.Modules.Select(m => m.Foldername));
            var latestVersion = GetLatestFiles(scanResult.SearchResult, clientType).First();

            return new Addon
            {
                Author = string.Join(", ", scanResult.SearchResult.Authors.Select(author => author.Name)),
                Name = scanResult.SearchResult.Name,
                ChannelType = addonChannelType,
                AutoUpdateEnabled = false,
                ClientType = clientType,
                DownloadUrl = latestVersion.DownloadUrl,
                ExternalUrl = scanResult.SearchResult.WebsiteUrl,
                ExternalId = scanResult.SearchResult.Id.ToString(),
                FolderName = scanResult.AddonFolder.Name,
                GameVersion = currentVersion.GameVersion.FirstOrDefault(),
                InstalledAt = DateTime.Now,
                InstalledFolders = folderList,
                InstalledVersion = currentVersion.DisplayName,
                IsIgnored = false,
                LatestVersion = latestVersion.DisplayName,
                ProviderName = Name,
                ThumbnailUrl = GetThumbnailUrl(scanResult.SearchResult)
            };
        }

        private async Task MapAddonFolders(List<CurseScanResult> scanResults, WowClientType clientType)
        {
            //var fingerprintStr = string.Join(",", scanResults.Select(sf => sf.FolderScanner.Fingerprint));
            var fingerprintResponse = await GetAddonsByFingerprints(scanResults.Select(sf => sf.FolderScanner.Fingerprint));

            foreach (var scanResult in scanResults)
            {
                // Curse can deliver the wrong result sometimes, ensure the result matches the client type
                scanResult.ExactMatch = fingerprintResponse.ExactMatches
                    .FirstOrDefault(exactMatch =>
                        IsClientType(exactMatch.File.GameVersionFlavor, clientType) &&
                        HasMatchingFingerprint(scanResult, exactMatch));

                // If the addon does not have an exact match, check the partial matches.
                if (scanResult.ExactMatch == null)
                {
                    scanResult.ExactMatch = fingerprintResponse.PartialMatches
                        .FirstOrDefault(partialMatch => partialMatch.File.Modules.Any(module => module.Fingerprint == scanResult.FolderScanner.Fingerprint));
                }
            }
        }

        private bool HasMatchingFingerprint(CurseScanResult scanResult, CurseMatch exactMatch)
        {
            return exactMatch.File.Modules.Any(m => m.Fingerprint == scanResult.FolderScanner.Fingerprint);
        }

        private IList<CurseSearchResult> FilterClientType(IEnumerable<CurseSearchResult> results, WowClientType clientType)
        {
            var clientTypeStr = GetClientTypeString(clientType);

            return results
                .Where(r => r.LatestFiles.Any(f => DoesFileMatchClientType(f, clientTypeStr)))
                .ToList();
        }

        private bool DoesFileMatchClientType(CurseFile file, string clientTypeStr)
        {
            return file.ReleaseType == CurseReleaseType.Release &&
                file.GameVersionFlavor == clientTypeStr &&
                file.IsAlternate == false;
        }

        private AddonChannelType GetChannelType(CurseReleaseType releaseType)
        {
            return releaseType switch
            {
                CurseReleaseType.Alpha => AddonChannelType.Alpha,
                CurseReleaseType.Beta => AddonChannelType.Beta,
                _ => AddonChannelType.Stable,
            };
        }

        private AddonSearchResult GetAddonSearchResult(CurseSearchResult result, IEnumerable<CurseFile> latestFiles)
        {
            try
            {
                var thumbnailUrl = GetThumbnailUrl(result);
                var id = result.Id;
                var name = result.Name;
                var author = GetAuthor(result);

                var searchResultFiles = latestFiles.Select(lf => new AddonSearchResultFile
                {
                    ChannelType = GetChannelType(lf.ReleaseType),
                    Version = lf.DisplayName,
                    DownloadUrl = lf.DownloadUrl,
                    Folders = GetFolderNames(lf),
                    GameVersion = GetGameVersion(lf)
                });

                return new AddonSearchResult
                {
                    Author = author,
                    ExternalId = id.ToString(),
                    Name = name,
                    ThumbnailUrl = thumbnailUrl,
                    ExternalUrl = result.WebsiteUrl,
                    ProviderName = Name,
                    Files = searchResultFiles
                };
            }
            catch (Exception ex)
            {
                _analyticsService.Track(ex, "GetAddonSearchResult");
                return null;
            }
        }

        private IList<CurseSearchResult> FilterResults(
            IList<CurseSearchResult> results,
            string addonName,
            string folderName,
            WowClientType clientType)
        {
            var clientTypeStr = GetClientTypeString(clientType);

            return results.
                Where(r =>
                    r.Name == addonName ||
                    r.LatestFiles.Any(f => f.ReleaseType == CurseReleaseType.Release &&
                        f.GameVersionFlavor == clientTypeStr &&
                        f.IsAlternate == false &&
                        f.Modules.Any(m => m.Foldername == folderName)))
                .ToList();
        }

        private IList<string> GetFolderNames(CurseFile file)
        {
            return file.Modules.Select(m => m.Foldername).ToList();
        }

        private string GetAuthor(CurseSearchResult result)
        {
            var authorNames = result.Authors.Select(a => a.Name);
            return string.Join(", ", authorNames);
        }

        private string GetGameVersion(CurseFile file)
        {
            return file.GameVersion.FirstOrDefault();
        }

        private IEnumerable<CurseFile> GetLatestFiles(CurseSearchResult result, WowClientType clientType)
        {
            var clientTypeStr = GetClientTypeString(clientType);

            return result.LatestFiles
                .Where(f => f.IsAlternate == false && f.GameVersionFlavor == clientTypeStr)
                .OrderByDescending(f => f.Id);
        }

        private string GetThumbnailUrl(CurseSearchResult result)
        {
            return result.Attachments
                .FirstOrDefault(f => f.IsDefault && !string.IsNullOrEmpty(f.ThumbnailUrl))?.ThumbnailUrl;
        }

        private async Task<IList<CurseSearchResult>> GetAllIds(IEnumerable<string> addonIds)
        {
            var url = $"{ApiUrl}/addon";

            try
            {
                return await url
                    .WithHeaders(HttpUtilities.DefaultHeaders)
                    .PostJsonAsync(addonIds.Select(id => Convert.ToInt32(id)).ToArray())
                    .ReceiveJson<List<CurseSearchResult>>();
            }
            catch (Exception ex)
            {
                _analyticsService.Track(ex, "GetAllIds");

                return new List<CurseSearchResult>();
            }
        }

        private async Task<IList<CurseSearchResult>> GetSearchResults(string query)
        {
            var url = $"{ApiUrl}/addon/search";

            try
            {
                return await url
                    .SetQueryParams(new { gameId = 1, searchFilter = query })
                    .WithHeaders(HttpUtilities.DefaultHeaders)
                    .GetJsonAsync<IList<CurseSearchResult>>();
            }
            catch (Exception ex)
            {
                _analyticsService.Track(ex, "GetSearchResults");

                return new List<CurseSearchResult>();
            }
        }

        private async Task<IList<CurseSearchResult>> GetFeaturedAddonList()
        {
            var url = $"{ApiUrl}/addon/featured";

            try
            {
                var body = new
                {
                    GameId = 1,
                    featuredCount = 6,
                    popularCount = 50,
                    updatedCount = 0
                };

                var response = await _cacheService.GetCache(url, async () =>
                {
                    return await url
                        .WithHeaders(HttpUtilities.DefaultHeaders)
                        .PostJsonAsync(body)
                        .ReceiveJson<CurseGetFeaturedResponse>();
                });

                return response.Popular.ToList();
            }
            catch (Exception ex)
            {
                _analyticsService.Track(ex, "GetSearchResults");
                return new List<CurseSearchResult>();
            }
        }

        private async Task<CurseFingerprintsResponse> GetAddonsByFingerprints(IEnumerable<long> fingerprints)
        {
            var url = $"{ApiUrl}/fingerprint";

            return await url
                .WithHeaders(HttpUtilities.DefaultHeaders)
                .PostJsonAsync(fingerprints)
                .ReceiveJson<CurseFingerprintsResponse>();
        }

        private PotentialAddon GetPotentialAddon(CurseSearchResult searchResult)
        {
            return new PotentialAddon
            {
                ProviderName = Name,
                Name = searchResult.Name,
                DownloadCount = (int)searchResult.DownloadCount,
                ThumbnailUrl = GetThumbnailUrl(searchResult),
                ExternalId = searchResult.Id.ToString(),
                ExternalUrl = searchResult.WebsiteUrl,
                Author = GetAuthor(searchResult)
            };
        }

        private bool IsClientType(string gameVesionFlavor, WowClientType clientType)
        {
            switch (clientType)
            {
                case WowClientType.Classic:
                case WowClientType.ClassicPtr:
                    return ClassicGameVersionFlavor == gameVesionFlavor;
                case WowClientType.Retail:
                case WowClientType.RetailPtr:
                case WowClientType.Beta:
                    return RetailGameVersionFlavor == gameVesionFlavor;
                default:
                    return false;
            }
        }

        private string GetClientTypeString(WowClientType clientType)
        {
            switch (clientType)
            {
                case WowClientType.Classic:
                case WowClientType.ClassicPtr:
                    return ClassicGameVersionFlavor;
                case WowClientType.Retail:
                case WowClientType.RetailPtr:
                case WowClientType.Beta:
                    return RetailGameVersionFlavor;
                default:
                    return string.Empty;
            }
        }
    }
}
