using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Nop.Core.Http;

namespace Nop.Core;

/// <summary>
/// Represents a web helper
/// </summary>
public partial class WebHelper : IWebHelper
{
    #region Fields  

    protected readonly IActionContextAccessor _actionContextAccessor;
    protected readonly IHostApplicationLifetime _hostApplicationLifetime;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly IUrlHelperFactory _urlHelperFactory;
    protected readonly Lazy<IStoreContext> _storeContext;

    #endregion

    #region Ctor

    public WebHelper(IActionContextAccessor actionContextAccessor,
        IHostApplicationLifetime hostApplicationLifetime,
        IHttpContextAccessor httpContextAccessor,
        IUrlHelperFactory urlHelperFactory,
        Lazy<IStoreContext> storeContext)
    {
        _actionContextAccessor = actionContextAccessor;
        _hostApplicationLifetime = hostApplicationLifetime;
        _httpContextAccessor = httpContextAccessor;
        _urlHelperFactory = urlHelperFactory;
        _storeContext = storeContext;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Check whether current HTTP request is available
    /// </summary>
    /// <returns>True if available; otherwise false</returns>
    protected virtual bool IsRequestAvailable()
    {
        if (_httpContextAccessor?.HttpContext == null)
            return false;

        try
        {
            if (_httpContextAccessor.HttpContext?.Request == null)
                return false;
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    /// <summary>
    /// Is IP address specified
    /// </summary>
    /// <param name="address">IP address</param>
    /// <returns>Result</returns>
    protected virtual bool IsIpAddressSet(IPAddress address)
    {
        var rez = address != null && address.ToString() != IPAddress.IPv6Loopback.ToString();

        return rez;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Get URL referrer if exists
    /// </summary>
    /// <returns>URL referrer</returns>
    public virtual string GetUrlReferrer()
    {
        if (!IsRequestAvailable())
            return string.Empty;

        //URL referrer is null in some case (for example, in IE 8)
        return _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Referer];
    }

    /// <summary>
    /// Get IP address from HTTP context
    /// </summary>
    /// <returns>String of IP address</returns>
    public virtual string GetCurrentIpAddress()
    {
        if (!IsRequestAvailable() || _httpContextAccessor.HttpContext!.Connection.RemoteIpAddress is not { } remoteIp)
            return string.Empty;

        return (remoteIp.Equals(IPAddress.IPv6Loopback) ? IPAddress.Loopback : remoteIp).ToString();
    }

    /// <summary>
    /// Gets this page URL
    /// </summary>
    /// <param name="includeQueryString">Value indicating whether to include query strings</param>
    /// <param name="useSsl">Value indicating whether to get SSL secured page URL. Pass null to determine automatically</param>
    /// <param name="lowercaseUrl">Value indicating whether to lowercase URL</param>
    /// <returns>Page URL</returns>
    public virtual string GetThisPageUrl(bool includeQueryString, bool? useSsl = null, bool lowercaseUrl = false)
    {
        if (!IsRequestAvailable())
            return string.Empty;

        //get store location
        var storeLocation = GetStoreLocation(useSsl ?? IsCurrentConnectionSecured());

        //add local path to the URL
        var pageUrl = $"{storeLocation.TrimEnd('/')}{_httpContextAccessor.HttpContext.Request.Path}";

        //add query string to the URL
        if (includeQueryString)
            pageUrl = $"{pageUrl}{_httpContextAccessor.HttpContext.Request.QueryString}";

        //whether to convert the URL to lower case
        if (lowercaseUrl)
            pageUrl = pageUrl.ToLowerInvariant();

        return pageUrl;
    }

    /// <summary>
    /// Gets a value indicating whether current connection is secured
    /// </summary>
    /// <returns>True if it's secured, otherwise false</returns>
    public virtual bool IsCurrentConnectionSecured()
    {
        if (!IsRequestAvailable())
            return false;

        return _httpContextAccessor.HttpContext.Request.IsHttps;
    }

    /// <summary>
    /// Gets store host location
    /// </summary>
    /// <param name="useSsl">Whether to get SSL secured URL</param>
    /// <returns>Store host location</returns>
    public virtual string GetStoreHost(bool useSsl)
    {
        if (!IsRequestAvailable())
            return string.Empty;

        //try to get host from the request HOST header
        var hostHeader = _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Host];
        if (StringValues.IsNullOrEmpty(hostHeader))
            return string.Empty;

        //add scheme to the URL
        var storeHost = $"{(useSsl ? Uri.UriSchemeHttps : Uri.UriSchemeHttp)}{Uri.SchemeDelimiter}{hostHeader.FirstOrDefault()}";

        //ensure that host is ended with slash
        storeHost = $"{storeHost.TrimEnd('/')}/";

        return storeHost;
    }

    /// <summary>
    /// Gets store location
    /// </summary>
    /// <param name="useSsl">Whether to get SSL secured URL; pass null to determine automatically</param>
    /// <returns>Store location</returns>
    public virtual string GetStoreLocation(bool? useSsl = null)
    {
        var storeLocation = string.Empty;

        //get store host
        var storeHost = GetStoreHost(useSsl ?? IsCurrentConnectionSecured());
        if (!string.IsNullOrEmpty(storeHost))
        {
            //add application path base if exists
            storeLocation = IsRequestAvailable() ? $"{storeHost.TrimEnd('/')}{_httpContextAccessor.HttpContext.Request.PathBase}" : storeHost;
        }

        //if host is empty (it is possible only when HttpContext is not available), use URL of a store entity configured in admin area
        if (string.IsNullOrEmpty(storeHost))
            storeLocation = _storeContext.Value.GetCurrentStore()?.Url
                            ?? throw new Exception("Current store cannot be loaded");

        //ensure that URL is ended with slash
        storeLocation = $"{storeLocation.TrimEnd('/')}/";

        return storeLocation;
    }

    /// <summary>
    /// Returns true if the requested resource is one of the typical resources that needn't be processed by the cms engine.
    /// </summary>
    /// <returns>True if the request targets a static resource file.</returns>
    public virtual bool IsStaticResource()
    {
        if (!IsRequestAvailable())
            return false;

        string path = _httpContextAccessor.HttpContext.Request.Path;

        //a little workaround. FileExtensionContentTypeProvider contains most of static file extensions. So we can use it
        //source: https://github.com/aspnet/StaticFiles/blob/dev/src/Microsoft.AspNetCore.StaticFiles/FileExtensionContentTypeProvider.cs
        //if it can return content type, then it's a static file
        var contentTypeProvider = new FileExtensionContentTypeProvider();
        return contentTypeProvider.TryGetContentType(path, out var _);
    }

    /// <summary>
    /// Modify query string of the URL
    /// </summary>
    /// <param name="url">Url to modify</param>
    /// <param name="key">Query parameter key to add</param>
    /// <param name="values">Query parameter values to add</param>
    /// <returns>New URL with passed query parameter</returns>
    public virtual string ModifyQueryString(string url, string key, params string[] values)
    {
        if (string.IsNullOrEmpty(url))
            return string.Empty;

        if (string.IsNullOrEmpty(key))
            return url;

        //prepare URI object
        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
        var isLocalUrl = urlHelper.IsLocalUrl(url);

        var uriStr = url;
        if (isLocalUrl)
        {
            var pathBase = _httpContextAccessor.HttpContext.Request.PathBase;
            uriStr = $"{GetStoreLocation().TrimEnd('/')}{(url.StartsWith(pathBase) ? url.Replace(pathBase, "") : url)}";
        }

        var uri = new Uri(uriStr, UriKind.Absolute);

        //get current query parameters
        var queryParameters = QueryHelpers.ParseQuery(uri.Query);

        //and add passed one
        queryParameters[key] = string.Join(",", values);

        //add only first value
        //two the same query parameters? theoretically it's not possible.
        //but MVC has some ugly implementation for checkboxes and we can have two values
        //find more info here: http://www.mindstorminteractive.com/topics/jquery-fix-asp-net-mvc-checkbox-truefalse-value/
        //we do this validation just to ensure that the first one is not overridden
        var queryBuilder = new QueryBuilder(queryParameters
            .ToDictionary(parameter => parameter.Key, parameter => parameter.Value.FirstOrDefault()?.ToString() ?? string.Empty));

        //create new URL with passed query parameters
        url = $"{(isLocalUrl ? uri.LocalPath : uri.GetLeftPart(UriPartial.Path))}{queryBuilder.ToQueryString()}{uri.Fragment}";

        return url;
    }

    /// <summary>
    /// Remove query parameter from the URL
    /// </summary>
    /// <param name="url">Url to modify</param>
    /// <param name="key">Query parameter key to remove</param>
    /// <param name="value">Query parameter value to remove; pass null to remove all query parameters with the specified key</param>
    /// <returns>New URL without passed query parameter</returns>
    public virtual string RemoveQueryString(string url, string key, string value = null)
    {
        if (string.IsNullOrEmpty(url))
            return string.Empty;

        if (string.IsNullOrEmpty(key))
            return url;

        //prepare URI object
        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
        var isLocalUrl = urlHelper.IsLocalUrl(url);
        var uri = new Uri(isLocalUrl ? $"{GetStoreLocation().TrimEnd('/')}{url}" : url, UriKind.Absolute);

        //get current query parameters
        var queryParameters = QueryHelpers.ParseQuery(uri.Query)
            .SelectMany(parameter => parameter.Value, (parameter, queryValue) => new KeyValuePair<string, string>(parameter.Key, queryValue))
            .ToList();

        if (!string.IsNullOrEmpty(value))
        {
            //remove a specific query parameter value if it's passed
            queryParameters.RemoveAll(parameter => parameter.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)
                                                   && parameter.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
        }
        else
        {
            //or remove query parameter by the key
            queryParameters.RemoveAll(parameter => parameter.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase));
        }

        var queryBuilder = new QueryBuilder(queryParameters);

        //create new URL without passed query parameters
        url = $"{(isLocalUrl ? uri.LocalPath : uri.GetLeftPart(UriPartial.Path))}{queryBuilder.ToQueryString()}{uri.Fragment}";

        return url;
    }

    /// <summary>
    /// Gets query string value by name
    /// </summary>
    /// <typeparam name="T">Returned value type</typeparam>
    /// <param name="name">Query parameter name</param>
    /// <returns>Query string value</returns>
    public virtual T QueryString<T>(string name)
    {
        if (!IsRequestAvailable())
            return default;

        if (StringValues.IsNullOrEmpty(_httpContextAccessor.HttpContext.Request.Query[name]))
            return default;

        return CommonHelper.To<T>(_httpContextAccessor.HttpContext.Request.Query[name].ToString());
    }

    /// <summary>
    /// Restart application domain
    /// </summary>
    public virtual void RestartAppDomain()
    {
        _hostApplicationLifetime.StopApplication();
    }

    /// <summary>
    /// Gets a value that indicates whether the client is being redirected to a new location
    /// </summary>
    public virtual bool IsRequestBeingRedirected
    {
        get
        {
            var response = _httpContextAccessor.HttpContext.Response;
            //ASP.NET 4 style - return response.IsRequestBeingRedirected;
            int[] redirectionStatusCodes = [StatusCodes.Status301MovedPermanently, StatusCodes.Status302Found];

            return redirectionStatusCodes.Contains(response.StatusCode);
        }
    }

    /// <summary>
    /// Gets or sets a value that indicates whether the client is being redirected to a new location using POST
    /// </summary>
    public virtual bool IsPostBeingDone
    {
        get
        {
            if (_httpContextAccessor.HttpContext.Items[NopHttpDefaults.IsPostBeingDoneRequestItem] == null)
                return false;

            return Convert.ToBoolean(_httpContextAccessor.HttpContext.Items[NopHttpDefaults.IsPostBeingDoneRequestItem]);
        }

        set => _httpContextAccessor.HttpContext.Items[NopHttpDefaults.IsPostBeingDoneRequestItem] = value;
    }

    /// <summary>
    /// Gets current HTTP request protocol
    /// </summary>
    public virtual string GetCurrentRequestProtocol()
    {
        return IsCurrentConnectionSecured() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
    }

    /// <summary>
    /// Gets whether the specified HTTP request URI references the local host.
    /// </summary>
    /// <param name="req">HTTP request</param>
    /// <returns>True, if HTTP request URI references to the local host</returns>
    public virtual bool IsLocalRequest(HttpRequest req)
    {
        //source: https://stackoverflow.com/a/41242493/7860424
        var connection = req.HttpContext.Connection;
        if (IsIpAddressSet(connection.RemoteIpAddress))
        {
            //We have a remote address set up
            return IsIpAddressSet(connection.LocalIpAddress)
                //Is local is same as remote, then we are local
                ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress)
                //else we are remote if the remote IP address is not a loopback address
                : IPAddress.IsLoopback(connection.RemoteIpAddress);
        }

        return true;
    }

    /// <summary>
    /// Get the raw path and full query of request
    /// </summary>
    /// <param name="request">HTTP request</param>
    /// <returns>Raw URL</returns>
    public virtual string GetRawUrl(HttpRequest request)
    {
        //first try to get the raw target from request feature
        //note: value has not been UrlDecoded
        var rawUrl = request.HttpContext.Features.Get<IHttpRequestFeature>()?.RawTarget;

        //or compose raw URL manually
        if (string.IsNullOrEmpty(rawUrl))
            rawUrl = $"{request.PathBase}{request.Path}{request.QueryString}";

        return rawUrl;
    }

    /// <summary>
    /// Gets whether the request is made with AJAX 
    /// </summary>
    /// <param name="request">HTTP request</param>
    /// <returns>Result</returns>
    public virtual bool IsAjaxRequest(HttpRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);

        if (request.Headers == null)
            return false;

        return request.Headers.XRequestedWith == "XMLHttpRequest";
    }

    #endregion
}