﻿// MIT License. Copyright (c) 2016 Maxim Kuzmin. Contacts: https://github.com/maxim-kuzmin

// Author: Maxim Kuzmin

using Makc2017.Core.Extensions.Tasking.OfTask;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNetCore.Mvc.Internal;
using Makc2017.Core.Web.Exceptions;
using System.Diagnostics;

namespace Makc2017.Core.Web.Middlewares
{
    /// <summary>
    /// Ядро. Веб. Промежуточные компоненты. Компонент обработки ошибок.
    /// </summary>
    public class CoreWebMiddlewareErrorHandling
    {
        #region Constants

        private const string HANDLED_EXCEPTION = "Microsoft.AspNetCore.Diagnostics.HandledException";

        #endregion Constants

        #region Properties

        private RequestDelegate Next { get; set; }

        private ILogger Logger { get; set; }

        private ExceptionHandlerOptions Options { get; set; }

        private DiagnosticSource DiagnosticSource { get; set; }

        #endregion Properties

        #region Constructors

        /// <summary>
        /// Конструктор.
        /// </summary>
        /// <param name="next">Следующий делегат обработки запроса.</param>
        /// <param name="loggerFactory">Фабрика логирования.</param>
        /// <param name="options">Опции.</param>
        /// <param name="diagnosticSource">Источник диагностики.</param>
        public CoreWebMiddlewareErrorHandling(
            RequestDelegate next,
            ILoggerFactory loggerFactory,
            IOptions<ExceptionHandlerOptions> options,
            DiagnosticSource diagnosticSource
            )
        {
            Next = next;
            Logger = loggerFactory.CreateLogger<CoreWebMiddlewareErrorHandling>();
            Options = options.Value;

            if (Options.ExceptionHandler == null)
            {
                Options.ExceptionHandler = Next;
            }

            DiagnosticSource = diagnosticSource;
        }

        #endregion Constructors

        #region Public methods

        /// <summary>
        /// Вызвать.
        /// </summary>
        /// <param name="context">HTTP-контекст.</param>
        /// <returns>Задача.</returns>
        public async Task Invoke(HttpContext context)
        {
            try
            {
                await Next.Invoke(context).WithCurrentCulture(false);

                switch (context.Response.StatusCode)
                {
                    case 403:
                    case 404:
                        throw new CoreWebExceptionHttp(context.Response.StatusCode);
                }
            }
            catch (Exception ex)
            {
                var isOk = false;

                var httpException = ex as CoreWebExceptionHttp;

                if (httpException == null)
                {
                    httpException = new CoreWebExceptionHttp(500, ex.Message, ex.InnerException);
                }

                if (context.Response.HasStarted)
                {
                    Logger.LogWarning(
                        "The response has already started, the error handler will not be executed."
                        );

                    throw;
                }

                var originalPath = context.Request.Path;

                if (Options.ExceptionHandlingPath.HasValue)
                {
                    context.Request.Path = Options.ExceptionHandlingPath.Value;
                }

                try
                {
                    context.Response.Clear();

                    var exceptionHandlerFeature = new ExceptionHandlerFeature()
                    {
                        Error = httpException,
                        Path = originalPath.Value,
                    };

                    context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
                    context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);

                    context.Response.StatusCode = httpException.StatusCode;
                    context.Response.OnStarting(ClearCacheHeaders, context.Response);

                    await Options.ExceptionHandler(context);

                    if (DiagnosticSource.IsEnabled(HANDLED_EXCEPTION) && httpException.StatusCode == 500)
                    {
                        DiagnosticSource.Write(HANDLED_EXCEPTION, new { httpContext = context, exception = ex });
                    }

                    isOk = true;
                }
                catch (Exception ex2)
                {
                    Logger.LogError(
                        0, 
                        ex2, 
                        "An exception was thrown attempting to execute the error handler."
                        );
                }
                finally
                {
                    context.Request.Path = originalPath;
                }

                if (!isOk) throw;
            }
        }

        #endregion Public methods

        private Task ClearCacheHeaders(object state)
        {
            var headers = ((HttpResponse)state).Headers;

            headers[HeaderNames.CacheControl] = "no-cache";
            headers[HeaderNames.Pragma] = "no-cache";
            headers[HeaderNames.Expires] = "-1";

            headers.Remove(HeaderNames.ETag);

            return TaskCache.CompletedTask;
        }
    }
}
