using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EasyCaching.Core;
using Flurl.Http;
using Kavita.API.Database;
using Kavita.API.Services;
using Kavita.API.Services.Plus;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Extensions;
using Kavita.Models.Constants;
using Kavita.Models.DTOs.KavitaPlus.License;
using Kavita.Models.Entities.Enums;
using Microsoft.Extensions.Logging;

namespace Kavita.Services.Plus;

internal class RegisterLicenseResponseDto
{
    public string EncryptedLicense { get; set; }
    public bool Successful { get; set; }
    public string ErrorMessage { get; set; }
}

public class LicenseService(
    IEasyCachingProviderFactory cachingProviderFactory,
    IUnitOfWork unitOfWork,
    ILogger<LicenseService> logger,
    IVersionUpdaterService versionUpdaterService)
    : ILicenseService
{
    private readonly TimeSpan _licenseCacheTimeout = TimeSpan.FromHours(8);
    public const string Cron = "0 */9 * * *";
    /// <summary>
    /// Cache key for if license is valid or not
    /// </summary>
    public const string CacheKey = "license";
    private const string LicenseInfoCacheKey = "license-info";


    /// <summary>
    /// Performs license lookup to API layer
    /// </summary>
    /// <param name="license"></param>
    /// <returns></returns>
    private async Task<bool> IsLicenseValid(string license)
    {
        if (string.IsNullOrWhiteSpace(license)) return false;
        try
        {
            var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check")
                .WithKavitaPlusHeaders(license)
                .PostJsonAsync(new LicenseValidDto()
                {
                    License = license,
                    InstallId = HashUtil.ServerToken()
                })
                .ReceiveString();
            return bool.Parse(response);
        }
        catch (Exception e)
        {
            logger.LogError(e, "An error happened during the request to Kavita+ API");
            throw;
        }
    }

    /// <summary>
    /// Register the license with KavitaPlus
    /// </summary>
    /// <param name="license"></param>
    /// <param name="email"></param>
    /// <returns></returns>
    private async Task<string> RegisterLicense(string license, string email, string? discordId)
    {
        if (string.IsNullOrWhiteSpace(license) || string.IsNullOrWhiteSpace(email)) return string.Empty;
        try
        {
            var response = await (Configuration.KavitaPlusApiUrl + "/api/license/register")
                .WithKavitaPlusHeaders(license)
                .PostJsonAsync(new EncryptLicenseDto()
                {
                    License = license.Trim(),
                    InstallId = HashUtil.ServerToken(),
                    EmailId = email.Trim(),
                    DiscordId = discordId?.Trim()
                })
                .ReceiveJson<RegisterLicenseResponseDto>();

            if (response.Successful)
            {
                return response.EncryptedLicense;
            }

            logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response.ErrorMessage);
            throw new KavitaException(response.ErrorMessage);
        }
        catch (FlurlHttpException e)
        {
            logger.LogError(e, "An error happened during the request to Kavita+ API");
            return string.Empty;
        }
    }


    /// <summary>
    /// Checks licenses and updates cache
    /// </summary>
    /// <param name="forceCheck">Skip what's in cache</param>
    /// <param name="ct"></param>
    /// <returns></returns>
    public async Task<bool> HasActiveLicense(bool forceCheck = false, CancellationToken ct = default)
    {
        var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
        if (!forceCheck)
        {
            var cacheValue = await provider.GetAsync<bool>(CacheKey, ct);
            if (cacheValue.HasValue) return cacheValue.Value;
        }

        var result = false;
        try
        {
            var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct);
            result = await IsLicenseValid(serverSetting.Value);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "There was an issue connecting to Kavita+");
        }
        finally
        {
            await provider.FlushAsync(ct);
            await provider.SetAsync(CacheKey, result, _licenseCacheTimeout, ct);
        }

        return result;
    }

    /// <summary>
    /// Checks if the sub is active and caches the result. This should not be used too much over cache as it will skip backend caching.
    /// </summary>
    /// <param name="license"></param>
    /// <param name="ct"></param>
    /// <returns></returns>
    public async Task<bool> HasActiveSubscription(string? license, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(license)) return false;
        try
        {
            var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check-sub")
                .WithKavitaPlusHeaders(license)
                .PostJsonAsync(new LicenseValidDto()
                {
                    License = license,
                    InstallId = HashUtil.ServerToken()
                }, cancellationToken: ct)
                .ReceiveString();

            var result =  bool.Parse(response);

            var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
            await provider.FlushAsync(ct);
            await provider.SetAsync(CacheKey, result, _licenseCacheTimeout, ct);

            return result;
        }
        catch (Exception e)
        {
            logger.LogError(e, "An error happened during the request to Kavita+ API");
            return false;
        }
    }

    public async Task RemoveLicense(CancellationToken ct = default)
    {
        var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct);
        serverSetting.Value = string.Empty;
        unitOfWork.SettingsRepository.Update(serverSetting);
        await unitOfWork.CommitAsync(ct);

        var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
        await provider.RemoveAsync(CacheKey, ct);


    }

    public async Task AddLicense(string license, string email, string? discordId, CancellationToken ct = default)
    {
        var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct);
        var lic = await RegisterLicense(license, email, discordId);
        if (string.IsNullOrWhiteSpace(lic))
            throw new KavitaException("unable-to-register-k+");
        serverSetting.Value = lic;
        unitOfWork.SettingsRepository.Update(serverSetting);
        await unitOfWork.CommitAsync(ct);
    }


    /// <summary>
    /// Removes this installId from Kavita+, essentially allowing the user to re-register an instance
    /// </summary>
    /// <param name="license"></param>
    /// <param name="email"></param>
    /// <param name="ct"></param>
    /// <returns></returns>
    /// <exception cref="KavitaException"></exception>
    public async Task<bool> ResetLicense(string license, string email, CancellationToken ct = default)
    {
        try
        {
            var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct);
            var response = await (Configuration.KavitaPlusApiUrl + "/api/license/reset")
                .WithKavitaPlusHeaders(encryptedLicense.Value)
                .PostJsonAsync(new ResetLicenseDto()
                {
                    License = license.Trim(),
                    InstallId = HashUtil.ServerToken(),
                    EmailId = email
                }, cancellationToken: ct)
                .ReceiveString();

            if (string.IsNullOrEmpty(response))
            {
                var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
                await provider.RemoveAsync(CacheKey, ct);
                return true;
            }

            logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response);
            throw new KavitaException(response);
        }
        catch (FlurlHttpException e)
        {
            logger.LogError(e, "An error happened during the request to Kavita+ API");
        }

        return false;
    }

    /// <summary>
    /// Fetches information about the license from Kavita+. If there is no license or an exception, will return null and can be assumed it is not active
    /// </summary>
    /// <param name="forceCheck"></param>
    /// <param name="ct"></param>
    /// <returns></returns>
    public async Task<LicenseInfoDto?> GetLicenseInfo(bool forceCheck = false, CancellationToken ct = default)
    {
        // Check if there is a license
        var hasLicense =
            !string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct))
                .Value);

        if (!hasLicense) return null;

        // Check the cache
        var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LicenseInfo);
        if (!forceCheck)
        {
            var cacheValue = await licenseInfoProvider.GetAsync<LicenseInfoDto>(LicenseInfoCacheKey, ct);
            if (cacheValue.HasValue) return cacheValue.Value;
        }

        try
        {
            var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct);
            var response = await (Configuration.KavitaPlusApiUrl + "/api/license/info")
                .WithKavitaPlusHeaders(encryptedLicense.Value)
                .GetJsonAsync<LicenseInfoDto>(cancellationToken: ct);

            // This indicates a mismatch on installId or no active subscription
            if (response == null) return null;

            // Ensure that current version is within the 3 version limit. Don't count Nightly releases or Hotfixes
            var releases = await versionUpdaterService.GetAllReleases(ct: ct);
            response.IsValidVersion = releases
                .Where(r => !r.UpdateTitle.Contains("Hotfix")) // We don't care about Hotfix releases
                .Where(r => !r.IsPrerelease) // Ensure we don't take current nightlies within the current/last stable
                .Take(3)
                .All(r => new Version(r.UpdateVersion) <= BuildInfo.Version);

            response.HasLicense = hasLicense;
            response.InstallId = HashUtil.ServerToken();

            // Cache if the license is valid here as well
            var licenseProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
            await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout, ct);

            // default: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking
            if (response is {IsCancelled: true, IsActive: false})
            {
                //logger.LogWarning("Kavita+ License is no longer active, removing Server registration");
            }

            // Cache the license info if IsActive and ExpirationDate > DateTime.UtcNow + 2
            if (response.IsActive && response.ExpirationDate > DateTime.UtcNow.AddDays(2))
            {
                await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout, ct);
            }


            return response;
        }
        catch (FlurlHttpException e)
        {
            logger.LogError(e, "An error happened during the request to Kavita+ API");
        }

        return null;
    }

    /// <summary>
    /// Attempts to resend a welcome email to the registered user. The sub does not need to be active.
    /// </summary>
    /// <param name="ct"></param>
    /// <returns></returns>
    public async Task<bool> ResendWelcomeEmail(CancellationToken ct = default)
    {
        try
        {
            var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct);
            if (string.IsNullOrEmpty(encryptedLicense.Value)) return false;

            var httpResponse = await (Configuration.KavitaPlusApiUrl + "/api/license/resend-welcome-email")
                .WithKavitaPlusHeaders(encryptedLicense.Value)
                .PostAsync(cancellationToken: ct);

            var response = await httpResponse.GetStringAsync();

            if (response == null) return false;


            return response == "true";
        }
        catch (FlurlHttpException e)
        {
            logger.LogError(e, "An error happened during the request to Kavita+ API");
        }

        return false;
    }
}
