@using System.Diagnostics.CodeAnalysis
@using System.Net
@namespace Oqtane.UI
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState
@inject NavigationManager NavigationManager
@inject INavigationInterception NavigationInterception
@inject ISyncService SyncService
@inject ISiteService SiteService
@inject IPageService PageService
@inject IUserService UserService
@inject IUrlMappingService UrlMappingService
@inject ILogService LogService
@inject IJSRuntime JSRuntime
@implements IHandleAfterRender
@implements IDisposable

@if (!string.IsNullOrEmpty(_error))
{
	<ModuleMessage Message="@_error" Type="@MessageType.Warning" />
}

@DynamicComponent

@code {
    private string _absoluteUri;
    private bool _isInternalNavigation = false;
    private bool _navigationInterceptionEnabled;
    private PageState _pagestate;
    private string _error = "";    

    [Parameter]
    public string RenderMode { get; set; }

    [Parameter]
    public string Runtime { get; set; }

    [CascadingParameter]
    PageState PageState { get; set; }

    [Parameter]
    public Action<PageState> OnStateChange { get; set; }

    private RenderFragment DynamicComponent { get; set; }

    protected override void OnInitialized()
    {
        _absoluteUri = NavigationManager.Uri;
        NavigationManager.LocationChanged += LocationChanged;

        DynamicComponent = builder =>
        {
            if (PageState != null && !PageState.Refresh)
            {
                builder.OpenComponent(0, typeof(ThemeBuilder));
                builder.CloseComponent();
            }
        };
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= LocationChanged;
    }

    protected override async Task OnParametersSetAsync()
    {
        if (PageState == null || PageState.Refresh)
        {
            await Refresh();
        }
    }

    private async void LocationChanged(object sender, LocationChangedEventArgs args)
    {
        _absoluteUri = args.Location;
        _isInternalNavigation = true;
        await Refresh();
    }

    Task IHandleAfterRender.OnAfterRenderAsync()
    {
        if (!_navigationInterceptionEnabled)
        {
            _navigationInterceptionEnabled = true;
            return NavigationInterception.EnableNavigationInterceptionAsync();
        }
        return Task.CompletedTask;
    }

    [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
    private async Task Refresh()
    {
        Site site = null;
        Page page = null;
        User user = null;
        var editmode = false;
        var refresh = false;
        var lastsyncdate = DateTime.MinValue;
        var visitorId = -1;
        _error = "";

        Route route = new Route(_absoluteUri, SiteState.Alias.Path);
        int moduleid = int.Parse(route.ModuleId);
        var action = route.Action; 

        var querystring = Utilities.ParseQueryString(route.Query);
        var returnurl = "";
        if (querystring.ContainsKey("returnurl"))
        {
            returnurl = WebUtility.UrlDecode(querystring["returnurl"]);
            if (!returnurl.StartsWith("/"))
            {
                // urls which are not relative are vulnerable to open redirects or XSS
                returnurl = "";
                querystring["returnurl"] = "";
            }
        }

        // reload the client application from the server if there is a forced reload
        if (querystring.ContainsKey("reload"))
        {
            if (querystring.ContainsKey("reload") && querystring["reload"] == "post")
            {
                // post back so that the cookies are set correctly - required on any change to the principal 
                var interop = new Interop(JSRuntime);
                var fields = new { returnurl = "/" + NavigationManager.ToBaseRelativePath(_absoluteUri) };
                string url = Utilities.TenantUrl(SiteState.Alias, "/pages/external/");
                await interop.SubmitForm(url, fields);
                return;
            }
            else
            {
                NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", "").Replace("&reload", ""), true);
                return;				
            }
        }

        // the refresh parameter is used to refresh the client-side PageState
        if (querystring.ContainsKey("refresh"))
        {
            refresh = true;
        }

        if (PageState != null)
        {
            editmode = PageState.EditMode;
            lastsyncdate = PageState.LastSyncDate;
            visitorId = PageState.VisitorId;
        }
        if (PageState?.Page.Path != route.PagePath)
        {
            editmode = false; // reset edit mode when navigating to different page
        }
        if (querystring.ContainsKey("edit") && querystring["edit"] == "true")
        {
            editmode = true; // querystring can set edit mode
        }

        // get user
        if (PageState == null || PageState.Refresh || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
        {
            var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
            // verify user is authenticated for current site
            if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey))
            {
                user = await UserService.GetUserAsync(authState.User.Identity.Name, SiteState.Alias.SiteId);
                if (user != null)
                {
                    user.IsAuthenticated = authState.User.Identity.IsAuthenticated;
                }
            }
        }
        else
        {
            user = PageState.User;
        }

        // process any sync events
        var sync = await SyncService.GetSyncEventsAsync(lastsyncdate);
        lastsyncdate = sync.SyncDate;
        if (sync.SyncEvents.Any())
        {
            // reload client application if server was restarted
            if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.Host))
            {
                NavigationManager.NavigateTo(_absoluteUri, true);
                return;
            }
            // reload client application if site runtime/rendermode was modified
            if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId))
            {
                NavigationManager.NavigateTo(_absoluteUri, true);
                return;
            }
            // reload client application if current user auth information has changed
            if (user != null && sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.User && item.EntityId == user.UserId))
            {
                NavigationManager.NavigateTo(_absoluteUri, true);
                return;
            }
            // refresh PageState when site information has changed
            if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Refresh && item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId))
            {
                refresh = true;
            }
        }

        // get site
        if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
        {
            site = await SiteService.GetSiteAsync(SiteState.Alias.SiteId);
            refresh = true;
        }
        else
        {
            site = PageState.Site;
        }

        if (site != null)
        {
            if (Runtime == Runtimes.Hybrid && !site.Hybrid)
            {
                _error = "Hybrid Integration Is Not Enabled For This Site";
                return;
            }

            if (PageState == null || refresh || PageState.Page.Path != route.PagePath)
            {
                page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
            }
            else
            {
                page = PageState.Page;
            }

            if (page == null && route.PagePath == "") // naked path refers to site home page
            {
                if (site.HomePageId != null)
                {
                    page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
                }
                if (page == null)
                {
                    // fallback to use the first page in the collection
                    page = site.Pages.FirstOrDefault();
                }
            }
            if (page == null)
            {
                page = await PageService.GetPageAsync(route.PagePath, site.SiteId);
            }
            else
            {
                // look for personalized page
                if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))
                {
                    var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId);
                    if (personalized != null)
                    {
                        // redirect to the personalized page
                        NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, personalized.Path, ""), false);
                    }
                }
            }

            if (page != null)
            {
                // check if user is authorized to view page
                if (UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)))
                {
                    // load additional metadata for current page
                    page = ProcessPage(page, site, user, SiteState.Alias);

                    // load additional metadata for modules
                    (page, site.Modules) = ProcessModules(page, site.Modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);

                    // populate page state (which acts as a client-side cache for subsequent requests)
                    _pagestate = new PageState
                    {
                        Alias = SiteState.Alias,
                        Site = site,
                        Page = page,
                        User = user,
                        Uri = new Uri(_absoluteUri, UriKind.Absolute),
                        Route = route,
                        QueryString = querystring,
                        UrlParameters = route.UrlParameters,
                        ModuleId = moduleid,
                        Action = action,
                        EditMode = editmode,
                        LastSyncDate = lastsyncdate,
                        RenderMode = RenderMode,
                        Runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime),
						VisitorId = visitorId,
						RemoteIPAddress = SiteState.RemoteIPAddress,
						ReturnUrl = returnurl,
                        IsInternalNavigation = _isInternalNavigation,
                        RenderId = Guid.NewGuid(),
                        Refresh = false
                    };

                    OnStateChange?.Invoke(_pagestate);
                    await ScrollToFragment(_pagestate.Uri);
                }
                else
                {
                    // Need to redirect 404 as page doesnot exist in a Permission or Timeframe
                    if (route.PagePath != "404")
                    {
                        // redirect to 404 page
                        NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
                    }
                }
            }
            else // page not found
            {
                // look for url mapping
                var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
                if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
                {
                    var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query;
                    NavigationManager.NavigateTo(url, false);
                }
                else // not mapped
                {
                    if (user == null)
                    {
                        // redirect to login page if user not logged in as they may need to be authenticated
                        NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery)));
                    }
                    else
                    {
                        if (route.PagePath != "404")
                        {
                            // redirect to 404 page
                            NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
                        }
                        else
                        {
                            // redirect to home page as a fallback
                            NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));
                        }
                    }
                }
            }
        }
        else
        {
            // site does not exist
        }
    }

    private Page ProcessPage(Page page, Site site, User user, Alias alias)
    {
        try
        {
            page.Panes = new List<string>();
            page.Resources = new List<Resource>();

            // validate theme
            if (string.IsNullOrEmpty(page.ThemeType))
            {
                page.ThemeType = site.DefaultThemeType;
            }
            var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
            Type themetype = Type.GetType(page.ThemeType);
            if (themetype == null || theme == null)
            {
                // fallback to default Oqtane theme
                page.ThemeType = Constants.DefaultTheme;
                themetype = Type.GetType(Constants.DefaultTheme);
                theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
            }

            string panes = "";
            if (themetype != null && theme != null)
            {
                // get resources for theme (ITheme)
                page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName));

                var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
                if (themeobject != null)
                {
                    if (!string.IsNullOrEmpty(themeobject.Panes))
                    {
                        panes = themeobject.Panes;
                    }
                    // get resources for theme control
                    page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace);
                }
            }

            if (!string.IsNullOrEmpty(panes))
            {
                page.Panes = panes.Replace(";", ",").Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
                if (!page.Panes.Contains(PaneNames.Default) && !page.Panes.Contains(PaneNames.Admin))
                {
                    _error = "The Current Theme Does Not Contain A Default Or Admin Pane";
                }
            }
            else
            {
                page.Panes.Add(PaneNames.Admin);
                _error = "The Current Theme Does Not Contain Any Panes";
            }
        }
        catch
        {
            // error loading theme or layout
        }

        return page;
    }

    private (Page Page, List<Module> Modules) ProcessModules(Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias)
    {
        var paneindex = new Dictionary<string, int>();

        foreach (Module module in modules)
        {
            // initialize module control properties
            module.SecurityAccessLevel = SecurityAccessLevel.Host;
            module.ControlTitle = "";
            module.Actions = "";
            module.UseAdminContainer = false;
            module.PaneModuleIndex = -1;
            module.PaneModuleCount = 0;

            if (module.PageId == page.PageId || module.ModuleId == moduleid)
            {
                var typename = Constants.ErrorModule;

                if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime)))
                {
                    typename = module.ModuleDefinition.ControlTypeTemplate;

                    // handle default action
                    if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
                    {
                        action = module.ModuleDefinition.DefaultAction;
                    }

                    // check if the module defines custom action routes
                    if (module.ModuleDefinition.ControlTypeRoutes != "")
                    {
                        foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(';', StringSplitOptions.RemoveEmptyEntries))
                        {
                            if (route.StartsWith(action + "="))
                            {
                                typename = route.Replace(action + "=", "");
                            }
                        }
                    }

                    // get module resources
                    page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName));
                }

                // ensure component exists and implements IModuleControl
                module.ModuleType = "";
                if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase))
                {
                    typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action);
                }
                else
                {
                    typename = typename.Replace(Constants.ActionToken, action);
                }
                Type moduletype = Type.GetType(typename, false, true); // case insensitive
                if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
                {
                    module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name
                }

                // get additional metadata from IModuleControl interface
                if (moduletype != null && module.ModuleType != "")
                {
                    // retrieve module component resources
                    var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                    module.RenderMode = moduleobject.RenderMode;

                    page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules",  moduletype.Namespace);
                    if (action.ToLower() == "settings" && module.ModuleDefinition != null)
                    {
                        // settings components are embedded within a framework settings module
                        moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true);
                        if (moduletype != null)
                        {
                            moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                            page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
                        }
                    }

                    // additional metadata needed for admin components
                    if (module.ModuleId == moduleid && action != "")
                    {
                        module.ControlTitle = moduleobject.Title;
                        module.SecurityAccessLevel = moduleobject.SecurityAccessLevel;
                        module.Actions = moduleobject.Actions;
                        module.UseAdminContainer = moduleobject.UseAdminContainer;
                    }
                }

                // validate that module's pane exists in current page
                if (page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1)
                {
                    // fallback to default pane if it exists
                    if (page.Panes.FindIndex(item => item.Equals(PaneNames.Default, StringComparison.OrdinalIgnoreCase)) != -1)
                    {
                        module.Pane = PaneNames.Default;
                    }
                    else // otherwise admin pane (legacy)
                    {
                        module.Pane = PaneNames.Admin;						
                    }
                }

                // calculate module position within pane
                if (paneindex.ContainsKey(module.Pane.ToLower()))
                {
                    paneindex[module.Pane.ToLower()] += 1;
                }
                else
                {
                    paneindex.Add(module.Pane.ToLower(), 0);
                }

                module.PaneModuleIndex = paneindex[module.Pane.ToLower()];

                // container fallback
                if (string.IsNullOrEmpty(module.ContainerType))
                {
                    module.ContainerType = defaultcontainertype;
                }
            }
        }

        foreach (Module module in modules.Where(item => item.PageId == page.PageId))
        {
            if (paneindex.ContainsKey(module.Pane.ToLower()))
            {
                module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1;
            }
        }

        return (page, modules);
    }

    private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name)
    {
        if (resources != null)
        {
            foreach (var resource in resources)
            {
                if (resource.Level != ResourceLevel.Site)
                {
                    if (resource.Url.StartsWith("~"))
                    {
                        resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
                    }
                    if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
                    {
                        resource.Url = alias.BaseUrl + resource.Url;
                    }

                    // ensure resource does not exist already
                    if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
                    {
                        pageresources.Add(new Resource
                        {
                            ResourceType = resource.ResourceType,
                            Url = resource.Url,
                            Integrity = resource.Integrity,
                            CrossOrigin = resource.CrossOrigin,
                            Bundle = resource.Bundle,
                            Location = resource.Location,
                            ES6Module = resource.ES6Module,
                            Content = resource.Content,
                            Level = level,
                            Namespace = name
                        });
                    }
                }
			}
		}
		return pageresources;
	}

	private async Task ScrollToFragment(Uri uri)
	{
		var fragment = uri.Fragment;
		if (fragment.StartsWith('#'))
		{
			// handle text fragment (https://example.org/#test:~:text=foo)
			var id = fragment.Substring(1);
			var index = id.IndexOf(":~:", StringComparison.Ordinal);
			if (index > 0)
			{
				id = id.Substring(0, index);
			}

			if (!string.IsNullOrEmpty(id))
			{
				var interop = new Interop(JSRuntime);
				await interop.ScrollToId(id);
			}
        }
    }
}
