// ***********************************************************************
// 
//     Heath
// 
// ***********************************************************************
using Lib.Common.LoggingAPI.Service.Constants;
using Lib.Common.LoggingAPI.Service.DataTransferObjects.Logger;
using Microsoft.AspNetCore.Http;
using Microsoft.IO;
using Serilog;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Lib.Common.LoggingAPI.Service.Middleware.HttpLogger
{
    /// 
    /// Handles all logging scenarios.
    /// 
    public static class HttpLogger
    {
        /// 
        /// The JSON serializer options for logging methods.
        /// 
        public static JsonSerializerOptions serializerOptions = new JsonSerializerOptions
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            Converters = {
                new JsonStringEnumConverter( JsonNamingPolicy.CamelCase),
            },
        };
        /// 
        /// Logs an error message.
        /// 
        /// The generic message parameter.
        /// The HTTP context.
        /// The message.
        /// The service identifier.
        public static void LogError(this ILogger logger, HttpContext context, TMessage message, string? serviceId)
        {
            var logMessage = CreateErrorLog(context, message, serviceId);
            logger.Error(logMessage);
        }
        /// 
        /// Logs an information message.
        /// 
        /// The generic message parameter.
        /// The HTTP context.
        /// The message.
        /// The service identifier.
        public static void LogInfo(this ILogger logger, HttpContext context, TMessage message, string? serviceId)
        {
            var logMessage = CreateInfoLog(context, message, serviceId);
            logger.Information(logMessage);
        }
        /// 
        /// Logs an incoming HTTP request.
        /// 
        /// The logger.
        /// The HTTP context.
        /// The recyclable mmory stream manager.
        /// The service identifier.
        /// A  representing the asynchronous operation.
        public static async Task LogRequest(
            this ILogger logger,
            HttpContext context,
            RecyclableMemoryStreamManager recyclableMemoryStreamManager,
            string? serviceId)
        {
            context.Request.EnableBuffering();
            await using var requestStream = recyclableMemoryStreamManager.GetStream();
            await context.Request.Body.CopyToAsync(requestStream);
            var logMessage = CreateRequestLog(
                context,
                ReadStream(requestStream),
                serviceId);
            logger.Information(logMessage);
            context.Request.Body.Position = 0;
        }
        /// 
        /// Logs an outcome HTTP response.
        /// 
        /// The logger.
        /// The HTTP context.
        /// The recyclable mmory stream manager.
        /// The request delegate process.
        /// The service identifier.
        /// A  representing the asynchronous operation.
        /// 
        public static async Task LogResponse(
            this ILogger logger,
            HttpContext context,
            RecyclableMemoryStreamManager recyclableMemoryStreamManager,
            RequestDelegate requestProcess,
            string? serviceId)
        {
            var originalBodyStream = context.Response.Body;
            await using var responseBody = recyclableMemoryStreamManager.GetStream();
            context.Response.Body = responseBody;
            await requestProcess(context);
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var text = await new StreamReader(context.Response.Body).ReadToEndAsync();
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var logMessage = CreateResponseLog(context, text, serviceId);
            logger.Information(logMessage);
            await responseBody.CopyToAsync(originalBodyStream);
        }
        /// 
        /// Creates an error log.
        /// 
        /// The generic message.
        /// The HTTP context.
        /// The error message.
        /// The service identifier.
        /// A  representig the error log.
        private static string CreateErrorLog(HttpContext context, TMessage message, string? serviceId)
            => CreateLog(context, LogSeverity.Error, LogOperation.Error, message, serviceId);
        /// 
        /// Creates an info log.
        /// 
        /// The generic message.
        /// The HTTP context.
        /// The info message.
        /// The service identifier.
        /// A  representig the info log.
        private static string CreateInfoLog(HttpContext context, TMessage message, string? serviceId)
            => CreateLog(context, LogSeverity.Info, LogOperation.Info, message, serviceId);
        /// 
        /// Creates a request log.
        /// 
        /// The HTTP context.
        /// The request body.
        /// The service identifier.
        /// A  representig the request log.
        private static string CreateRequestLog(HttpContext context, string? requestBody, string? serviceId)
            => CreateLog(context, LogSeverity.Info, LogOperation.ClientRequest, requestBody, serviceId);
        /// 
        /// Creates a response log.
        /// 
        /// The HTTP context.
        /// The response body.
        /// The service identifier.
        /// A  representig the response log.
        private static string CreateResponseLog(HttpContext context, string? responseBody, string? serviceId)
            => CreateLog(context, LogSeverity.Info, LogOperation.ClientResponse, responseBody, serviceId);
        /// 
        /// Creates a generic log.
        /// 
        /// The HTTP context.
        /// The log severity.
        /// The log operation.
        /// The log message
        /// The service identifier.
        /// A  representing a generic log.
        private static string CreateLog(
            HttpContext context,
            LogSeverity severity,
            LogOperation operation,
            TMessage message,
            string? serviceId)
        {
            var tokenHeader = context.Request.Headers[Headers.Authorization].FirstOrDefault()?.Split(" ").Last();
            var tokenHandler = new JwtSecurityTokenHandler();
            var token = (tokenHeader is not null) ? tokenHandler.ReadJwtToken(tokenHeader) : null;
            var log = new LogDetail
            {
                Severity = severity,
                Target = new LogTarget
                {
                    Method = context.Request.Method,
                    Host = context.Request.Host.Host,
                    Route = context.Request.Path,
                },
                Email = token?.Claims.FirstOrDefault(c => c.Type == Claims.Email)?.Value,
                User = token?.Claims.FirstOrDefault(c => c.Type == Claims.Name)?.Value,
                UserId = token?.Claims.FirstOrDefault(c => c.Type == Claims.Id)?.Value,
                Environment = Environment.GetEnvironmentVariable(EnvironmentVariables.Stage),
                Operation = operation,
                RequestId = context.Request.Headers[DisplayNames.RequestId],
                ServiceId = serviceId,
                XForwardedFor = context.Request.Headers[DisplayNames.XForwardedForHeader],
                Timestamp = DateTime.Now,
                Message = message,
            };
            var serializedLog = JsonSerializer.Serialize(log, serializerOptions);
            return serializedLog
                .Replace("\\u0022", "\"")
                .Replace("\"{", "{")
                .Replace("}\"", "}")
                .Replace("\\u0027", "'")
                .Replace("\\\u0027", "'")
                .Replace("\n", "");
        }
        /// 
        /// Reads the stream.
        /// 
        /// The stream to be read.
        /// A  representig the request body.
        private static string? ReadStream(Stream stream)
        {
            const int readChunkBufferLength = 4096;
            stream.Seek(0, SeekOrigin.Begin);
            using var textWriter = new StringWriter();
            using var reader = new StreamReader(stream);
            var readChunk = new char[readChunkBufferLength];
            int readChunkLength;
            do
            {
                readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength);
                textWriter.Write(readChunk, 0, readChunkLength);
            } while (readChunkLength > 0);
            var stringItem = textWriter.ToString();
            return stringItem != string.Empty ? stringItem : null;
        }
    }
}