using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Hosting;
using Nop.Core;
using Nop.Core.Configuration;
using Nop.Core.Domain.Seo;
using Nop.Services.Localization;
using Nop.Web.Framework.Mvc.Routing;
using Nop.Web.Framework.WebOptimizer;

namespace Nop.Web.Framework.UI;

/// <summary>
/// Represents the HTML helper implementation
/// </summary>
public partial class NopHtmlHelper : INopHtmlHelper
{
    #region Fields

    protected readonly AppSettings _appSettings;
    protected readonly HtmlEncoder _htmlEncoder;
    protected readonly IActionContextAccessor _actionContextAccessor;
    protected readonly IHtmlHelper _htmlHelper;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly INopAssetHelper _bundleHelper;
    protected readonly Lazy<ILocalizationService> _localizationService;
    protected readonly IStoreContext _storeContext;
    protected readonly IUrlHelperFactory _urlHelperFactory;
    protected readonly IWebHostEnvironment _webHostEnvironment;
    protected readonly SeoSettings _seoSettings;

    protected readonly Dictionary<ResourceLocation, List<ScriptReferenceMeta>> _scriptParts = new();
    protected readonly Dictionary<ResourceLocation, List<string>> _inlineScriptParts = new();
    protected readonly List<CssReferenceMeta> _cssParts = new();

    protected readonly List<string> _canonicalUrlParts = new();
    protected readonly List<string> _headCustomParts = new();
    protected readonly List<string> _metaDescriptionParts = new();
    protected readonly List<string> _metaKeywordParts = new();
    protected readonly List<string> _pageCssClassParts = new();
    protected readonly List<string> _titleParts = new();

    protected string _activeAdminMenuSystemName;
    protected string _editPageUrl;

    #endregion

    #region Ctor

    public NopHtmlHelper(AppSettings appSettings,
        HtmlEncoder htmlEncoder,
        IActionContextAccessor actionContextAccessor,
        IHtmlHelper htmlHelper,
        IHttpContextAccessor httpContextAccessor,
        INopAssetHelper bundleHelper,
        Lazy<ILocalizationService> localizationService,
        IStoreContext storeContext,
        IUrlHelperFactory urlHelperFactory,
        IWebHostEnvironment webHostEnvironment,
        SeoSettings seoSettings)
    {
        _appSettings = appSettings;
        _htmlEncoder = htmlEncoder;
        _actionContextAccessor = actionContextAccessor;
        _htmlHelper = htmlHelper;
        _httpContextAccessor = httpContextAccessor;
        _bundleHelper = bundleHelper;
        _localizationService = localizationService;
        _storeContext = storeContext;
        _urlHelperFactory = urlHelperFactory;
        _webHostEnvironment = webHostEnvironment;
        _seoSettings = seoSettings;
    }

    #endregion

    #region Utilities

    protected static string GetAssetKey(string[] keys, string suffix)
    {
        ArgumentNullException.ThrowIfNull(keys?.Length > 0 ? keys : null, nameof(keys));

        var hashInput = string.Join(',', keys);
        var input = MD5.HashData(Encoding.Unicode.GetBytes(hashInput));

        var key = string.Concat(WebEncoders.Base64UrlEncode(input));

        if (!string.IsNullOrEmpty(suffix))
            key += suffix;

        return key.ToLower();
    }

    /// <summary>
    /// Get URL of "src" parameter and check whether it is local
    /// </summary>
    /// <param name="src">Src</param>
    /// <returns>URL; check result</returns>
    protected virtual (string Url, bool IsLocal) GetSrcUrl(string src)
    {
        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);

        var isLocal = urlHelper.IsLocalUrl(src);
        var url = isLocal
            ? urlHelper.Content(src).RemoveApplicationPathFromRawUrl(_httpContextAccessor.HttpContext.Request.PathBase)
            : src;

        return (url, isLocal);
    }

    #endregion

    #region Methods

    /// <summary>
    /// Add title element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Title part</param>
    public virtual void AddTitleParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _titleParts.Add(part);
    }

    /// <summary>
    /// Append title element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Title part</param>
    public virtual void AppendTitleParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _titleParts.Insert(0, part);
    }

    /// <summary>
    /// Generate all title parts
    /// </summary>
    /// <param name="addDefaultTitle">A value indicating whether to insert a default title</param>
    /// <param name="part">Title part</param>
    /// <returns>A task that represents the asynchronous operation
    /// The task result contains generated HTML string</returns>
    public virtual async Task<IHtmlContent> GenerateTitleAsync(bool addDefaultTitle = true, string part = "")
    {
        AppendTitleParts(part);
        var store = await _storeContext.GetCurrentStoreAsync();
        var defaultTitle = await _localizationService.Value.GetLocalizedAsync(store, s => s.DefaultTitle);

        var specificTitle = string.Join(_seoSettings.PageTitleSeparator, _titleParts.AsEnumerable().Reverse().ToArray());
        string result;
        if (!string.IsNullOrEmpty(specificTitle))
        {
            if (addDefaultTitle)
            {
                //store name + page title
                switch (_seoSettings.PageTitleSeoAdjustment)
                {
                    case PageTitleSeoAdjustment.PagenameAfterStorename:
                        {
                            result = string.Join(_seoSettings.PageTitleSeparator, defaultTitle, specificTitle);
                        }
                        break;
                    case PageTitleSeoAdjustment.StorenameAfterPagename:
                    default:
                        {
                            result = string.Join(_seoSettings.PageTitleSeparator, specificTitle, defaultTitle);
                        }
                        break;
                }
            }
            else
            {
                //page title only
                result = specificTitle;
            }
        }
        else
        {
            //store name only
            result = defaultTitle;
        }

        return new HtmlString(_htmlEncoder.Encode(result ?? string.Empty));
    }

    /// <summary>
    /// Add meta description element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Meta description part</param>
    public virtual void AddMetaDescriptionParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _metaDescriptionParts.Add(part);
    }

    /// <summary>
    /// Append meta description element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Meta description part</param>
    public virtual void AppendMetaDescriptionParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _metaDescriptionParts.Insert(0, part);
    }

    /// <summary>
    /// Generate all description parts
    /// </summary>
    /// <param name="part">Meta description part</param>
    /// <returns>A task that represents the asynchronous operation
    /// The task result contains generated HTML string</returns>
    public virtual async Task<IHtmlContent> GenerateMetaDescriptionAsync(string part = "")
    {
        AppendMetaDescriptionParts(part);

        var metaDescription = string.Join(", ", _metaDescriptionParts.AsEnumerable().Reverse().ToArray());
        var result = !string.IsNullOrEmpty(metaDescription)
            ? metaDescription
            : await _localizationService.Value.GetLocalizedAsync(await _storeContext.GetCurrentStoreAsync(),
                s => s.DefaultMetaDescription);

        return new HtmlString(_htmlEncoder.Encode(result ?? string.Empty));
    }

    /// <summary>
    /// Add meta keyword element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Meta keyword part</param>
    public virtual void AddMetaKeywordParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _metaKeywordParts.Add(part);
    }

    /// <summary>
    /// Append meta keyword element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Meta keyword part</param>
    public virtual void AppendMetaKeywordParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _metaKeywordParts.Insert(0, part);
    }

    /// <summary>
    /// Generate all keyword parts
    /// </summary>
    /// <param name="part">Meta keyword part</param>
    /// <returns>A task that represents the asynchronous operation
    /// The task result contains generated HTML string</returns>
    public virtual async Task<IHtmlContent> GenerateMetaKeywordsAsync(string part = "")
    {
        AppendMetaKeywordParts(part);

        var metaKeyword = string.Join(", ", _metaKeywordParts.AsEnumerable().Reverse().ToArray());
        var result = !string.IsNullOrEmpty(metaKeyword)
            ? metaKeyword
            : await _localizationService.Value.GetLocalizedAsync(await _storeContext.GetCurrentStoreAsync(),
                s => s.DefaultMetaKeywords);

        return new HtmlString(_htmlEncoder.Encode(result ?? string.Empty));
    }

    /// <summary>
    /// Add script element
    /// </summary>
    /// <param name="location">A location of the script element</param>
    /// <param name="src">Script path (minified version)</param>
    /// <param name="debugSrc">Script path (full debug version). If empty, then minified version will be used</param>
    /// <param name="excludeFromBundle">A value indicating whether to exclude this script from bundling</param>
    public virtual void AddScriptParts(ResourceLocation location, string src, string debugSrc = "", bool excludeFromBundle = false)
    {
        if (!_scriptParts.ContainsKey(location))
            _scriptParts.Add(location, new List<ScriptReferenceMeta>());

        if (string.IsNullOrEmpty(src))
            return;

        if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment())
            src = debugSrc;

        var (url, isLocal) = GetSrcUrl(src);
        _scriptParts[location].Add(new ScriptReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = isLocal,
            Src = url
        });
    }

    /// <summary>
    /// Append script element
    /// </summary>
    /// <param name="location">A location of the script element</param>
    /// <param name="src">Script path (minified version)</param>
    /// <param name="debugSrc">Script path (full debug version). If empty, then minified version will be used</param>
    /// <param name="excludeFromBundle">A value indicating whether to exclude this script from bundling</param>
    public virtual void AppendScriptParts(ResourceLocation location, string src, string debugSrc = "", bool excludeFromBundle = false)
    {
        if (!_scriptParts.ContainsKey(location))
            _scriptParts.Add(location, new List<ScriptReferenceMeta>());

        if (string.IsNullOrEmpty(src))
            return;

        if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment())
            src = debugSrc;

        var (url, isLocal) = GetSrcUrl(src);
        _scriptParts[location].Insert(0, new ScriptReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = isLocal,
            Src = url
        });
    }

    /// <summary>
    /// Generate all script parts
    /// </summary>
    /// <param name="location">A location of the script element</param>
    /// <returns>Generated HTML string</returns>
    public virtual IHtmlContent GenerateScripts(ResourceLocation location)
    {
        if (!_scriptParts.TryGetValue(location, out var value) || value == null)
            return HtmlString.Empty;

        if (!_scriptParts.Any())
            return HtmlString.Empty;

        var result = new StringBuilder();
        var woConfig = _appSettings.Get<WebOptimizerConfig>();
        var pathBase = _httpContextAccessor.HttpContext.Request.PathBase;

        if (woConfig.EnableJavaScriptBundling && value.Any(item => !item.ExcludeFromBundle))
        {
            var sources = value.Where(item => !item.ExcludeFromBundle && item.IsLocal)
                .Select(item => item.Src)
                .Distinct().ToArray();

            var bundleKey = $"/js/{GetAssetKey(sources, woConfig.JavaScriptBundleSuffix)}.js";

            var bundleAsset = _bundleHelper.GetOrCreateJavaScriptAsset(bundleKey, sources);
            var route = _bundleHelper.CacheBusting(bundleAsset);

            result.AppendFormat("<script type=\"{0}\" src=\"{1}{2}\"></script>",
                MimeTypes.TextJavascript, pathBase, route);
        }

        var scripts = value.Where(item => !woConfig.EnableJavaScriptBundling || item.ExcludeFromBundle || !item.IsLocal)
            .Distinct();

        foreach (var item in scripts)
        {
            if (!item.IsLocal)
            {
                result.AppendFormat("<script type=\"{0}\" src=\"{1}\"></script>", MimeTypes.TextJavascript, item.Src);
                result.Append(Environment.NewLine);
                continue;
            }

            var asset = _bundleHelper.GetOrCreateJavaScriptAsset(item.Src);
            var route = _bundleHelper.CacheBusting(asset);

            result.AppendFormat("<script type=\"{0}\" src=\"{1}{2}\"></script>",
                MimeTypes.TextJavascript, pathBase, route);

            result.Append(Environment.NewLine);
        }

        return new HtmlString(result.ToString());
    }

    /// <summary>
    /// Add inline script element
    /// </summary>
    /// <param name="location">A location of the script element</param>
    /// <param name="script">Script</param>
    public virtual void AddInlineScriptParts(ResourceLocation location, string script)
    {
        if (!_inlineScriptParts.ContainsKey(location))
            _inlineScriptParts.Add(location, new());

        if (string.IsNullOrEmpty(script))
            return;

        if (_inlineScriptParts[location].Contains(script))
            return;

        _inlineScriptParts[location].Add(script);
    }

    /// <summary>
    /// Append inline script element
    /// </summary>
    /// <param name="location">A location of the script element</param>
    /// <param name="script">Script</param>
    public virtual void AppendInlineScriptParts(ResourceLocation location, string script)
    {
        if (!_inlineScriptParts.ContainsKey(location))
            _inlineScriptParts.Add(location, new());

        if (string.IsNullOrEmpty(script))
            return;

        if (_inlineScriptParts[location].Contains(script))
            return;

        _inlineScriptParts[location].Insert(0, script);
    }

    /// <summary>
    /// Generate all inline script parts
    /// </summary>
    /// <param name="location">A location of the script element</param>
    /// <returns>Generated HTML string</returns>
    public virtual IHtmlContent GenerateInlineScripts(ResourceLocation location)
    {
        if (!_inlineScriptParts.TryGetValue(location, out var value) || value == null)
            return HtmlString.Empty;

        if (!_inlineScriptParts.Any())
            return HtmlString.Empty;

        var result = new StringBuilder();
        foreach (var item in value)
        {
            result.Append(item);
            result.Append(Environment.NewLine);
        }
        return new HtmlString(result.ToString());
    }

    /// <summary>
    /// Add CSS element
    /// </summary>
    /// <param name="src">Script path (minified version)</param>
    /// <param name="debugSrc">Script path (full debug version). If empty, then minified version will be used</param>
    /// <param name="excludeFromBundle">A value indicating whether to exclude this style sheet from bundling</param>
    public virtual void AddCssFileParts(string src, string debugSrc = "", bool excludeFromBundle = false)
    {
        if (string.IsNullOrEmpty(src))
            return;

        if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment())
            src = debugSrc;

        var (url, isLocal) = GetSrcUrl(src);
        _cssParts.Add(new CssReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = isLocal,
            Src = url
        });
    }

    /// <summary>
    /// Append CSS element
    /// </summary>
    /// <param name="src">Script path (minified version)</param>
    /// <param name="debugSrc">Script path (full debug version). If empty, then minified version will be used</param>
    /// <param name="excludeFromBundle">A value indicating whether to exclude this style sheet from bundling</param>
    public virtual void AppendCssFileParts(string src, string debugSrc = "", bool excludeFromBundle = false)
    {
        if (string.IsNullOrEmpty(src))
            return;

        if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment())
            src = debugSrc;

        var (url, isLocal) = GetSrcUrl(src);
        _cssParts.Insert(0, new CssReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = isLocal,
            Src = url
        });
    }

    /// <summary>
    /// Generate all CSS parts
    /// </summary>
    /// <returns>Generated HTML string</returns>
    public virtual IHtmlContent GenerateCssFiles()
    {
        if (!_cssParts.Any())
            return HtmlString.Empty;

        var result = new StringBuilder();

        var woConfig = _appSettings.Get<WebOptimizerConfig>();
        var pathBase = _httpContextAccessor.HttpContext.Request.PathBase;

        if (woConfig.EnableCssBundling && _cssParts.Any(item => !item.ExcludeFromBundle))
        {
            var bundleSuffix = woConfig.CssBundleSuffix;

            if (CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft)
                bundleSuffix += ".rtl";

            var sources = _cssParts
                .Where(item => !item.ExcludeFromBundle && item.IsLocal)
                .Distinct()
                //remove the application path from the generated URL if exists
                .Select(item => item.Src).ToArray();

            var bundleKey = $"/css/{GetAssetKey(sources, bundleSuffix)}.css";

            var bundleAsset = _bundleHelper.GetOrCreateCssAsset(bundleKey, sources);
            var route = _bundleHelper.CacheBusting(bundleAsset);


            result.AppendFormat("<link rel=\"stylesheet\" type=\"{0}\" href=\"{1}{2}\" />",
                MimeTypes.TextCss, pathBase, route);
        }

        var styles = _cssParts
            .Where(item => !woConfig.EnableCssBundling || item.ExcludeFromBundle || !item.IsLocal)
            .Distinct();

        foreach (var item in styles)
        {
            if (!item.IsLocal)
            {
                result.AppendFormat("<link rel=\"stylesheet\" type=\"{0}\" href=\"{1}\" />", MimeTypes.TextCss, item.Src);
                result.Append(Environment.NewLine);
                continue;
            }

            var asset = _bundleHelper.GetOrCreateCssAsset(item.Src);
            var route = _bundleHelper.CacheBusting(asset);

            result.AppendFormat("<link rel=\"stylesheet\" type=\"{0}\" href=\"{1}{2}\" />",
                MimeTypes.TextCss, pathBase, route);
            result.AppendLine();
        }

        return new HtmlString(result.ToString());
    }

    /// <summary>
    /// Add canonical URL element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Canonical URL part</param>
    /// <param name="withQueryString">Whether to use canonical URLs with query string parameters</param>
    public virtual void AddCanonicalUrlParts(string part, bool withQueryString = false)
    {
        if (string.IsNullOrEmpty(part))
            return;

        if (withQueryString)
        {
            //add ordered query string parameters
            var queryParameters = _httpContextAccessor.HttpContext.Request.Query
                .OrderBy(parameter => parameter.Key)
                .ToDictionary(parameter => parameter.Key, parameter => parameter.Value.ToString());
            part = QueryHelpers.AddQueryString(part, queryParameters);
        }

        _canonicalUrlParts.Add(part);
    }

    /// <summary>
    /// Append canonical URL element to the <![CDATA[<head>]]>
    /// </summary>
    /// <param name="part">Canonical URL part</param>
    public virtual void AppendCanonicalUrlParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _canonicalUrlParts.Insert(0, part);
    }

    /// <summary>
    /// Generate all canonical URL parts
    /// </summary>
    /// <returns>Generated HTML string</returns>
    public virtual IHtmlContent GenerateCanonicalUrls()
    {
        var result = new StringBuilder();
        foreach (var canonicalUrl in _canonicalUrlParts)
        {
            result.AppendFormat("<link rel=\"canonical\" href=\"{0}\" />", canonicalUrl);
            result.Append(Environment.NewLine);
        }
        return new HtmlString(result.ToString());
    }

    /// <summary>
    /// Add any custom element to the <![CDATA[<head>]]> element
    /// </summary>
    /// <param name="part">The entire element. For example, <![CDATA[<meta name="msvalidate.01" content="123121231231313123123" />]]></param>
    public virtual void AddHeadCustomParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _headCustomParts.Add(part);
    }

    /// <summary>
    /// Append any custom element to the <![CDATA[<head>]]> element
    /// </summary>
    /// <param name="part">The entire element. For example, <![CDATA[<meta name="msvalidate.01" content="123121231231313123123" />]]></param>
    public virtual void AppendHeadCustomParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _headCustomParts.Insert(0, part);
    }

    /// <summary>
    /// Generate all custom elements
    /// </summary>
    /// <returns>Generated HTML string</returns>
    public virtual IHtmlContent GenerateHeadCustom()
    {
        //use only distinct rows
        var distinctParts = _headCustomParts.Distinct().ToList();
        if (!distinctParts.Any())
            return HtmlString.Empty;

        var result = new StringBuilder();
        foreach (var path in distinctParts)
        {
            result.Append(path);
            result.Append(Environment.NewLine);
        }
        return new HtmlString(result.ToString());
    }

    /// <summary>
    /// Add CSS class to the <![CDATA[<head>]]> element
    /// </summary>
    /// <param name="part">CSS class</param>
    public virtual void AddPageCssClassParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _pageCssClassParts.Add(part);
    }

    /// <summary>
    /// Append CSS class to the <![CDATA[<head>]]> element
    /// </summary>
    /// <param name="part">CSS class</param>
    public virtual void AppendPageCssClassParts(string part)
    {
        if (string.IsNullOrEmpty(part))
            return;

        _pageCssClassParts.Insert(0, part);
    }

    /// <summary>
    /// Generate all title parts
    /// </summary>
    /// <param name="part">CSS class</param>
    /// <returns>Generated string</returns>
    public virtual string GeneratePageCssClasses(string part = "")
    {
        AppendPageCssClassParts(part);

        var result = string.Join(" ", _pageCssClassParts.AsEnumerable().Reverse().ToArray());

        if (string.IsNullOrEmpty(result))
            return string.Empty;

        return _htmlEncoder.Encode(result);
    }

    /// <summary>
    /// Specify "edit page" URL
    /// </summary>
    /// <param name="url">URL</param>
    public virtual void AddEditPageUrl(string url)
    {
        _editPageUrl = url;
    }

    /// <summary>
    /// Get "edit page" URL
    /// </summary>
    /// <returns>URL</returns>
    public virtual string GetEditPageUrl()
    {
        return _editPageUrl;
    }

    /// <summary>
    /// Specify system name of admin menu item that should be selected (expanded)
    /// </summary>
    /// <param name="systemName">System name</param>
    public virtual void SetActiveMenuItemSystemName(string systemName)
    {
        _activeAdminMenuSystemName = systemName;
    }

    /// <summary>
    /// Get system name of admin menu item that should be selected (expanded)
    /// </summary>
    /// <returns>System name</returns>
    public virtual string GetActiveMenuItemSystemName()
    {
        return _activeAdminMenuSystemName;
    }

    /// <summary>
    /// Get the route name associated with the request rendering this page
    /// </summary>
    /// <param name="handleDefaultRoutes">A value indicating whether to build the name using engine information unless otherwise specified</param>
    /// <returns>Route name</returns>
    public virtual string GetRouteName(bool handleDefaultRoutes = false)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        var routeName = httpContext.GetEndpoint()?.Metadata.GetMetadata<RouteNameMetadata>()?.RouteName ?? string.Empty;

        if (!string.IsNullOrEmpty(routeName) && routeName != "areaRoute")
            return routeName;

        //then try to get a generic one (actually it's an action name, not the route)
        if (httpContext.GetRouteValue(NopRoutingDefaults.RouteValue.SeName) is not null &&
            httpContext.GetRouteValue(NopRoutingDefaults.RouteValue.Action) is string actionName)
        {
            return actionName;
        }

        if (handleDefaultRoutes)
        {
            return _actionContextAccessor.ActionContext.ActionDescriptor switch
            {
                ControllerActionDescriptor controllerAction => string.Concat(controllerAction.ControllerName, controllerAction.ActionName),
                CompiledPageActionDescriptor compiledPage => string.Concat(compiledPage.AreaName, compiledPage.ViewEnginePath.Replace("/", "")),
                PageActionDescriptor pageAction => string.Concat(pageAction.AreaName, pageAction.ViewEnginePath.Replace("/", "")),
                _ => _actionContextAccessor.ActionContext.ActionDescriptor.DisplayName?.Replace("/", "") ?? string.Empty
            };
        }

        return routeName;
    }

    /// <summary>
    /// Add JSON-LD to the <![CDATA[<head>]]> element
    /// </summary>
    /// <param name="jsonLd">The JSON-LD serialized model></param>
    public virtual void AddJsonLdParts(string jsonLd)
    {
        if (_seoSettings.MicrodataEnabled)
            AddHeadCustomParts("<script type=\"application/ld+json\">" + _htmlHelper.Raw(jsonLd) + "</script>");
    }

    #endregion

    #region Nested classes

    /// <summary>
    /// JS file meta data
    /// </summary>
    protected partial record ScriptReferenceMeta
    {
        /// <summary>
        /// A value indicating whether to exclude the script from bundling
        /// </summary>
        public bool ExcludeFromBundle { get; init; }

        /// <summary>
        /// A value indicating whether the src is local
        /// </summary>
        public bool IsLocal { get; init; }

        /// <summary>
        /// Src for production
        /// </summary>
        public string Src { get; init; }
    }

    /// <summary>
    /// CSS file meta data
    /// </summary>
    protected partial record CssReferenceMeta
    {
        /// <summary>
        /// A value indicating whether to exclude the script from bundling
        /// </summary>
        public bool ExcludeFromBundle { get; init; }

        /// <summary>
        /// Src for production
        /// </summary>
        public string Src { get; init; }

        /// <summary>
        /// A value indicating whether the Src is local
        /// </summary>
        public bool IsLocal { get; init; }
    }

    #endregion
}