using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace SelectPdf.Samples
{
    public interface IViewRenderService
    {
        Task<string> RenderToStringAsync<T>(string pageName, string pageLayout, T model)
            where T : PageModel;
    }

    public sealed class ViewRenderService : IViewRenderService
    {
        private readonly IRazorViewEngine _razorViewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IRazorPageActivator _pageActivator;
        private readonly IModelMetadataProvider _modelMetadataProvider;

        public ViewRenderService(
            IRazorViewEngine razorViewEngine,
            ITempDataProvider tempDataProvider,
            IHttpContextAccessor httpContextAccessor,
            IRazorPageActivator pageActivator,
            IModelMetadataProvider modelMetadataProvider)
        {
            _razorViewEngine = razorViewEngine;
            _tempDataProvider = tempDataProvider;
            _httpContextAccessor = httpContextAccessor;
            _pageActivator = pageActivator;
            _modelMetadataProvider = modelMetadataProvider;
        }

        public async Task<string> RenderToStringAsync<T>(string pageName, string pageLayout, T model)
            where T : PageModel
        {
            var httpContext = _httpContextAccessor.HttpContext
                ?? throw new InvalidOperationException("No active HttpContext is available.");

            var actionContext = CreateActionContext(httpContext);

            await using var writer = new StringWriter();

            var pageResult = _razorViewEngine.FindPage(actionContext, pageName);
            if (pageResult.Page is null)
            {
                throw new InvalidOperationException($"The Razor Page '{pageName}' could not be found.");
            }

            if (pageResult.Page is not Page page)
            {
                throw new InvalidOperationException(
                    $"The Razor page '{pageName}' was found, but it is not a concrete {nameof(Page)} instance.");
            }

            page.Layout = pageLayout;

            var view = new RazorView(
                _razorViewEngine,
                _pageActivator,
                new List<IRazorPage> { page },
                page,
                HtmlEncoder.Default,
                new DiagnosticListener(nameof(ViewRenderService)));

            var viewData = new ViewDataDictionary<T>(
                _modelMetadataProvider,
                new ModelStateDictionary())
            {
                Model = model
            };

            var viewContext = new ViewContext(
                actionContext,
                view,
                viewData,
                new TempDataDictionary(httpContext, _tempDataProvider),
                writer,
                new HtmlHelperOptions());

            page.PageContext = new PageContext(actionContext)
            {
                ViewData = viewContext.ViewData
            };

            page.ViewContext = viewContext;

            _pageActivator.Activate(page, viewContext);

            await page.ExecuteAsync();

            return writer.ToString();
        }

        private static ActionContext CreateActionContext(HttpContext httpContext)
        {
            var routeData = httpContext.GetRouteData() ?? new RouteData();

            var actionDescriptor =
                httpContext.GetEndpoint()?.Metadata.GetMetadata<ActionDescriptor>()
                ?? new ActionDescriptor();

            return new ActionContext(httpContext, routeData, actionDescriptor);
        }
    }
}