using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Theme;
using API.Entities;
using API.Entities.Enums.Theme;
using API.Extensions;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR;
using Flurl.Http;
using HtmlAgilityPack;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using MarkdownDeep;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace API.Services.Tasks;
#nullable enable
internal class GitHubContent
{
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("path")]
    public string Path { get; set; }
    [JsonProperty("type")]
    public string Type { get; set; }
    [JsonPropertyName("download_url")]
    [JsonProperty("download_url")]
    public string DownloadUrl { get; set; }
    [JsonProperty("sha")]
    public string Sha { get; set; }
}
/// 
/// The readme of the Theme repo
/// 
internal class ThemeMetadata
{
    public string Author { get; set; }
    public string AuthorUrl { get; set; }
    public string Description { get; set; }
    public Version LastCompatible { get; set; }
}
public interface IThemeService
{
    Task GetContent(int themeId);
    Task UpdateDefault(int themeId);
    /// 
    /// Browse theme repo for themes to download
    /// 
    /// 
    Task> GetDownloadableThemes();
    Task DownloadRepoTheme(DownloadableSiteThemeDto dto);
    Task DeleteTheme(int siteThemeId);
    Task CreateThemeFromFile(string tempFile, string username);
    Task SyncThemes();
}
public class ThemeService : IThemeService
{
    private readonly IDirectoryService _directoryService;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventHub _eventHub;
    private readonly ILogger _logger;
    private readonly Markdown _markdown = new();
    private readonly IMemoryCache _cache;
    private readonly MemoryCacheEntryOptions _cacheOptions;
    private const string GithubBaseUrl = "https://api.github.com";
    /// 
    /// Used for refreshing metadata around themes
    /// 
    private const string GithubReadme = "https://raw.githubusercontent.com/Kareadita/Themes/main/README.md";
    public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork,
        IEventHub eventHub, ILogger logger, IMemoryCache cache)
    {
        _directoryService = directoryService;
        _unitOfWork = unitOfWork;
        _eventHub = eventHub;
        _logger = logger;
        _cache = cache;
        _cacheOptions = new MemoryCacheEntryOptions()
            .SetSize(1)
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
    }
    /// 
    /// Given a themeId, return the content inside that file
    /// 
    /// 
    /// 
    public async Task GetContent(int themeId)
    {
        var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId) ?? throw new KavitaException("theme-doesnt-exist");
        var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName);
        if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile))
            throw new KavitaException("theme-doesnt-exist");
        return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile);
    }
    public async Task> GetDownloadableThemes()
    {
        const string cacheKey = "browse";
        // Avoid a duplicate Dark issue some users faced during migration
        var existingThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos())
            .GroupBy(k => k.Name)
            .ToDictionary(g => g.Key, g => g.First());
        if (_cache.TryGetValue(cacheKey, out List? themes) && themes != null)
        {
            foreach (var t in themes)
            {
                t.AlreadyDownloaded = existingThemes.ContainsKey(t.Name);
            }
            return themes;
        }
        // Fetch contents of the Native Themes directory
        var themesContents = await GetDirectoryContent("Native%20Themes");
        // Filter out directories
        var themeDirectories = themesContents.Where(c => c.Type == "dir").ToList();
        // Get the Readme and augment the theme data
        var themeMetadata = await GetReadme();
        var themeDtos = new List();
        foreach (var themeDir in themeDirectories)
        {
            var themeName = themeDir.Name.Trim();
            // Fetch contents of the theme directory
            var themeContents = await GetDirectoryContent(themeDir.Path);
            // Find css and preview files
            var cssFile = themeContents.FirstOrDefault(c => c.Name.EndsWith(".css"));
            var previewUrls = GetPreviewUrls(themeContents);
            if (cssFile == null) continue;
            var cssUrl = cssFile.DownloadUrl;
            var dto = new DownloadableSiteThemeDto()
            {
                Name = themeName,
                CssUrl = cssUrl,
                CssFile = cssFile.Name,
                PreviewUrls = previewUrls,
                Sha = cssFile.Sha,
                Path = themeDir.Path,
            };
            if (themeMetadata.TryGetValue(themeName, out var metadata))
            {
                dto.Author = metadata.Author;
                dto.LastCompatibleVersion = metadata.LastCompatible.ToString();
                dto.IsCompatible = BuildInfo.Version <= metadata.LastCompatible;
                dto.AlreadyDownloaded = existingThemes.ContainsKey(themeName);
                dto.Description = metadata.Description;
            }
            themeDtos.Add(dto);
        }
        _cache.Set(cacheKey, themeDtos, _cacheOptions);
        return themeDtos;
    }
    private static List GetPreviewUrls(IEnumerable themeContents)
    {
        return themeContents
            .Where(c => Parser.IsImage(c.Name) )
            .Select(p => p.DownloadUrl)
            .ToList();
    }
    private static async Task> GetDirectoryContent(string path)
    {
        var json = await $"{GithubBaseUrl}/repos/Kareadita/Themes/contents/{path}"
            .WithHeader("Accept", "application/vnd.github+json")
            .WithHeader("User-Agent", "Kavita")
            .GetStringAsync();
        return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json);
    }
    /// 
    /// Returns a map of all Native Themes names mapped to their metadata
    /// 
    /// 
    private async Task> GetReadme()
    {
        // Try and delete a Readme file if it already exists
        var existingReadmeFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, "README.md");
        if (_directoryService.FileSystem.File.Exists(existingReadmeFile))
        {
            _directoryService.DeleteFiles([existingReadmeFile]);
        }
        var tempDownloadFile = await GithubReadme.DownloadFileAsync(_directoryService.TempDirectory);
        // Read file into Markdown
        var htmlContent  = _markdown.Transform(await _directoryService.FileSystem.File.ReadAllTextAsync(tempDownloadFile));
        var htmlDoc = new HtmlDocument();
        htmlDoc.LoadHtml(htmlContent);
        // Find the table of Native Themes
        var tableContent = htmlDoc.DocumentNode
            .SelectSingleNode("//h2[contains(text(),'Native Themes')]/following-sibling::p").InnerText;
        // Initialize dictionary to store theme metadata
        var themes = new Dictionary();
        // Split the table content by rows
        var rows = tableContent.Split("\r\n").Select(row => row.Trim()).Where(row => !string.IsNullOrWhiteSpace(row)).ToList();
        // Parse each row in the Native Themes table
        foreach (var row in rows.Skip(2))
        {
            var cells = row.Split('|').Skip(1).Select(cell => cell.Trim()).ToList();
            // Extract information from each cell
            var themeName = cells[0];
            var authorName = cells[1];
            var description = cells[2];
            var compatibility = Version.Parse(cells[3]);
            // Create ThemeMetadata object
            var themeMetadata = new ThemeMetadata
            {
                Author = authorName,
                Description = description,
                LastCompatible = compatibility
            };
            // Add theme metadata to dictionary
            themes.Add(themeName, themeMetadata);
        }
        return themes;
    }
    private async Task DownloadSiteTheme(DownloadableSiteThemeDto dto)
    {
        if (string.IsNullOrEmpty(dto.Sha))
        {
            throw new ArgumentException("SHA cannot be null or empty for already downloaded themes.");
        }
        _directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory);
        var existingTempFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory,
            _directoryService.FileSystem.FileInfo.New(dto.CssUrl).Name);
        _directoryService.DeleteFiles([existingTempFile]);
        var tempDownloadFile = await dto.CssUrl.DownloadFileAsync(_directoryService.TempDirectory);
        // Validate the hash on the downloaded file
        // if (!_fileService.ValidateSha(tempDownloadFile, dto.Sha))
        // {
        //     throw new KavitaException("Cannot download theme, hash does not match");
        // }
        _directoryService.CopyFileToDirectory(tempDownloadFile, _directoryService.SiteThemeDirectory);
        var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, dto.CssFile);
        return finalLocation;
    }
    public async Task DownloadRepoTheme(DownloadableSiteThemeDto dto)
    {
        // Validate we don't have a collision with existing or existing doesn't already exist
        var existingThemes = _directoryService.ScanFiles(_directoryService.SiteThemeDirectory, string.Empty);
        if (existingThemes.Any(f => Path.GetFileName(f) == dto.CssFile))
        {
            // This can happen if you delete then immediately download (to refresh). We should just delete the old file and download. Users can always rollback their version with github directly
            _directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile));
        }
        var finalLocation = await DownloadSiteTheme(dto);
        // Create a new entry and note that this is downloaded
        var theme = new SiteTheme()
        {
            Name = dto.Name,
            NormalizedName = dto.Name.ToNormalized(),
            FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation),
            Provider = ThemeProvider.Custom,
            IsDefault = false,
            GitHubPath = dto.Path,
            Description = dto.Description,
            PreviewUrls = string.Join('|', dto.PreviewUrls),
            Author = dto.Author,
            ShaHash = dto.Sha,
            CompatibleVersion = dto.LastCompatibleVersion,
        };
        _unitOfWork.SiteThemeRepository.Add(theme);
        await _unitOfWork.CommitAsync();
        // Inform about the new theme
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
            MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name,
                ProgressEventType.Ended));
        return theme;
    }
    public async Task SyncThemes()
    {
        var themes = await _unitOfWork.SiteThemeRepository.GetThemes();
        var themeMetadata = await GetReadme();
        foreach (var theme in themes)
        {
            await SyncTheme(theme, themeMetadata);
        }
        _logger.LogInformation("Sync Themes complete");
    }
    /// 
    /// If the Theme is from the Theme repo, see if there is a new version that is compatible
    /// 
    /// 
    /// The Readme information
    private async Task SyncTheme(SiteTheme? theme, IDictionary themeMetadata)
    {
        // Given a theme, first validate that it is applicable
        if (theme == null || theme.Provider == ThemeProvider.System || string.IsNullOrEmpty(theme.GitHubPath))
        {
            _logger.LogInformation("Cannot Sync {ThemeName} as it is not valid", theme?.Name);
            return;
        }
        if (new Version(theme.CompatibleVersion) > BuildInfo.Version)
        {
            _logger.LogDebug("{ThemeName} theme supports a more up-to-date version ({Version}) of Kavita. Please update", theme.Name, theme.CompatibleVersion);
            return;
        }
        var themeContents = await GetDirectoryContent(theme.GitHubPath);
        var cssFile = themeContents.FirstOrDefault(c => c.Name.EndsWith(".css"));
        if (cssFile == null) return;
        // Update any metadata
        if (themeMetadata.TryGetValue(theme.Name, out var metadata))
        {
            theme.Description = metadata.Description;
            theme.Author = metadata.Author;
            theme.CompatibleVersion = metadata.LastCompatible.ToString();
            theme.PreviewUrls = string.Join('|', GetPreviewUrls(themeContents));
        }
        var hasUpdated = cssFile.Sha != theme.ShaHash;
        if (hasUpdated)
        {
            _logger.LogDebug("Theme {ThemeName} is out of date, updating", theme.Name);
            var tempLocation = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName);
            _directoryService.DeleteFiles([tempLocation]);
            var location = await cssFile.DownloadUrl.DownloadFileAsync(_directoryService.TempDirectory);
            if (_directoryService.FileSystem.File.Exists(location))
            {
                _directoryService.CopyFileToDirectory(location, _directoryService.SiteThemeDirectory);
                _logger.LogInformation("Updated Theme on disk for {ThemeName}", theme.Name);
            }
        }
        await _unitOfWork.CommitAsync();
        if (hasUpdated)
        {
            await _eventHub.SendMessageAsync(MessageFactory.SiteThemeUpdated,
                MessageFactory.SiteThemeUpdatedEvent(theme.Name));
        }
        // Send an update to refresh metadata around the themes
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
            MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name,
                ProgressEventType.Ended));
        _logger.LogInformation("Theme Sync complete");
    }
    /// 
    /// Deletes a SiteTheme. The CSS file will be moved to temp/ to allow user to recover data
    /// 
    /// 
    public async Task DeleteTheme(int siteThemeId)
    {
        // Validate no one else is using this theme
        var inUse = await _unitOfWork.SiteThemeRepository.IsThemeInUse(siteThemeId);
        if (inUse)
        {
            throw new KavitaException("errors.delete-theme-in-use");
        }
        var siteTheme = await _unitOfWork.SiteThemeRepository.GetTheme(siteThemeId);
        if (siteTheme == null) return;
        await RemoveTheme(siteTheme);
    }
    /// 
    /// This assumes a file is already in temp directory and will be used for
    /// 
    /// 
    /// 
    public async Task CreateThemeFromFile(string tempFile, string username)
    {
        if (!_directoryService.FileSystem.File.Exists(tempFile))
        {
            _logger.LogInformation("Unable to create theme from manual upload as file not in temp");
            throw new KavitaException("errors.theme-manual-upload");
        }
        var filename = _directoryService.FileSystem.FileInfo.New(tempFile).Name;
        var themeName = Path.GetFileNameWithoutExtension(filename);
        if (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null)
        {
            throw new KavitaException("errors.theme-already-in-use");
        }
        _directoryService.CopyFileToDirectory(tempFile, _directoryService.SiteThemeDirectory);
        var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, filename);
        // Create a new entry and note that this is downloaded
        var theme = new SiteTheme()
        {
            Name = Path.GetFileNameWithoutExtension(filename),
            NormalizedName = themeName.ToNormalized(),
            FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation),
            Provider = ThemeProvider.Custom,
            IsDefault = false,
            Description = $"Manually uploaded via UI by {username}",
            PreviewUrls = string.Empty,
            Author = username,
        };
        _unitOfWork.SiteThemeRepository.Add(theme);
        await _unitOfWork.CommitAsync();
        // Inform about the new theme
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
            MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name,
                ProgressEventType.Ended));
        return theme;
    }
    /// 
    /// Removes the theme and any references to it from Pref and sets them to the default at the time.
    /// This commits to DB.
    /// 
    /// 
    private async Task RemoveTheme(SiteTheme theme)
    {
        _logger.LogInformation("Removing {ThemeName}. File can be found in temp/ until nightly cleanup", theme.Name);
        var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id);
        var defaultTheme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
        foreach (var pref in prefs)
        {
            pref.Theme = defaultTheme;
            _unitOfWork.UserRepository.Update(pref);
        }
        try
        {
            // Copy the theme file to temp for nightly removal (to give user time to reclaim if made a mistake)
            var existingLocation =
                _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName);
            var newLocation =
                _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName);
            _directoryService.CopyFileToDirectory(existingLocation, newLocation);
            _directoryService.DeleteFiles([existingLocation]);
        }
        catch (Exception) { /* Swallow */ }
        _unitOfWork.SiteThemeRepository.Remove(theme);
        await _unitOfWork.CommitAsync();
    }
    /// 
    /// Updates the themeId to the default theme, all others are marked as non-default
    /// 
    /// 
    /// 
    /// If theme does not exist
    public async Task UpdateDefault(int themeId)
    {
        try
        {
            var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
            if (theme == null) throw new KavitaException("theme-doesnt-exist");
            foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes())
            {
                siteTheme.IsDefault = (siteTheme.Id == themeId);
                _unitOfWork.SiteThemeRepository.Update(siteTheme);
            }
            if (!_unitOfWork.HasChanges()) return;
            await _unitOfWork.CommitAsync();
        }
        catch (Exception)
        {
            await _unitOfWork.RollbackAsync();
            throw;
        }
    }
}