using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Hangfire;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.CookiePolicy;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using OnlineSalesAutoCrop.CoreAPI.API.Swagger;
using OnlineSalesAutoCrop.CoreAPI.Configuration.DI;
using OnlineSalesAutoCrop.CoreAPI.Configurations;
using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.BackgroupJobs;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.Integrations;
using OnlineSalesAutoCrop.CoreAPI.SignalRHub;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.RateLimiting;
using System.Threading.Tasks;

namespace OnlineSalesAutoCrop.CoreAPI
{
	/// <summary>
	/// 
	/// </summary>
	public class Startup
	{
		private readonly AppSettings _appSettings;

		/// <summary>
		/// 
		/// </summary>
		public IConfiguration Configuration { get; }

		/// <summary>
		/// Initializes a new instance of the Startup class using the specified configuration settings. 
		/// </summary>
		/// <param name="configuration">The configuration settings used to initialize the application. Cannot be null.</param>
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
			_appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
		}


		/// <summary>
		///     
		/// </summary>
		/// <param name="services"></param>
		public void ConfigureServices(IServiceCollection services)
		{
			try
			{
				if (_appSettings.IsValid())
				{
					#region AppSettings & MenuSettings

					services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
					services.Configure<MenuSettings>(Configuration.GetSection("MenuSettings"));

                    #endregion

                    #region Controllers

                    services.AddControllers(options =>
					{
						options.Filters.Add(new ProducesAttribute("application/json"));
					});

                    services.Configure<ApiBehaviorOptions>(options =>
                    {
                        options.InvalidModelStateResponseFactory = context =>
                        {
                            var errors = context.ModelState
                                .Where(kvp => kvp.Value?.Errors.Count > 0)
                                .SelectMany(kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage))
                                .ToList();

                            var response = new MobileResponseBase<object>
                            {
                                ReturnStatus = StatusCodes.Status400BadRequest,
                                ReturnMessage =new List<string>() { errors.Count > 0 ? errors[0] : string.Empty },
                                Data = null
                            };

                            return new BadRequestObjectResult(response);
                        };
                    });

                    #endregion

                    #region API versioning

                    services.AddApiVersioning(options =>
					{
						options.ReportApiVersions = true;
						options.DefaultApiVersion = new ApiVersion(1, 0);
						options.AssumeDefaultVersionWhenUnspecified = true;
						options.ApiVersionReader = new UrlSegmentApiVersionReader();
					})
					.AddMvc()                       // API versioning extensions for MVC Core
					.AddApiExplorer();              // API version-aware API Explorer extensions

					#endregion

					#region CORS

					string[] origins = _appSettings.CorsOrigins.Split(',');
					string[] methods = _appSettings.CorsMethods.Split(',');
					string[] headers = _appSettings.CorsHeaders.Split(',');
					services.AddCors(options =>
					{
						options.DefaultPolicyName = "CORSPolicies";
						options.AddDefaultPolicy(builder =>
							{
								builder.WithOrigins(origins)
									.SetIsOriginAllowedToAllowWildcardSubdomains()
									.WithMethods(methods)
									.WithHeaders(headers)
									.AllowCredentials();
							});
					});

					#endregion

					#region Caching

					if (_appSettings.CacheType == EnumCacheType.Redis && !string.IsNullOrEmpty(_appSettings.CacheUrl))
					{
						services.AddStackExchangeRedisCache(options =>
						{
							options.Configuration = _appSettings.CacheUrl;
						});
					}
					else if (_appSettings.CacheType == EnumCacheType.SqlServer && !string.IsNullOrEmpty(_appSettings.CacheUrl) && !string.IsNullOrEmpty(_appSettings.CacheTableName) && !string.IsNullOrEmpty(_appSettings.CacheSchemaName))
					{
						services.AddDistributedSqlServerCache(options =>
						{
							options.ConnectionString = _appSettings.CacheUrl;
							options.TableName = _appSettings.CacheTableName;
							options.SchemaName = _appSettings.CacheSchemaName;
							options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
						});
					}
					else
					{
						services.AddMemoryCache();
					}

					#endregion

					#region Cookie Settings

					services.Configure<CookiePolicyOptions>(options =>
					{
						options.ConsentCookieValue = "true";
						options.CheckConsentNeeded = context => true;
						options.MinimumSameSitePolicy = (SameSiteMode)_appSettings.CookieSameSite;
						if (_appSettings.CookieHttpOnly)
						{
							options.HttpOnly = HttpOnlyPolicy.Always;
						}
					});

					#endregion

					#region Session

					services.AddSession(options =>
					{
						options.Cookie.Name = "OnlineSalesAutoCrop.Net9.Session.Id";
						options.Cookie.HttpOnly = _appSettings.CookieHttpOnly;
						options.Cookie.SameSite = (SameSiteMode)_appSettings.CookieSameSite;
						options.IdleTimeout = TimeSpan.FromMinutes(_appSettings.CookieLifeTime);
					});

					#endregion

					#region JWT Authentication

					var key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
					services.AddAuthentication(options =>
					{
						options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
						options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
					})
					.AddJwtBearer(options =>
					{
						options.SaveToken = true;
						options.RequireHttpsMetadata = false;
						options.TokenValidationParameters = new TokenValidationParameters
						{
							ClockSkew = TimeSpan.Zero,
							ValidateIssuerSigningKey = true,
							ValidIssuer = _appSettings.JwtIssuer,
							ValidAudience = _appSettings.JwtAudience,
							ValidateIssuer = _appSettings.JwtValidateIssuer,
							IssuerSigningKey = new SymmetricSecurityKey(key),
							ValidateAudience = _appSettings.JwtValidateAudience
						};

						options.Events = new JwtBearerEvents
						{
							OnAuthenticationFailed = context =>
							{
								if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
									context.Response.Headers.Append("Token-Expired", "true");

								return Task.CompletedTask;
							}
						};
					});

					#endregion

					#region Mvc

					services.AddMvc();

					#endregion

					#region Forwarded Headers

					services.Configure<ForwardedHeadersOptions>(options =>
					{
						options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
					});

					#endregion

					#region Antiforgery

					services.AddAntiforgery(options =>
					{
						options.SuppressXFrameOptionsHeader = false;
						options.HeaderName = "OnlineSalesAutoCrop.X-XSRF-TOKEN";
						options.Cookie.Name = "OnlineSalesAutoCrop.X-XSRF-ANTIFORGERY";
						options.Cookie.HttpOnly = _appSettings.CookieHttpOnly;
						options.Cookie.SameSite = (SameSiteMode)_appSettings.CookieSameSite;
					});

					#endregion

					#region SignalR

					services.AddSignalR();
					services.AddResponseCompression(opt =>
					{
						opt.MimeTypes = ["application/octet-stream"];
					});

					#endregion

					#region Rate Limit

					if (!string.IsNullOrEmpty(_appSettings.RLName))
					{
						services.AddRateLimiter(options =>
						{
							options.AddPolicy(_appSettings.RLName, httpContext =>
								RateLimitPartition.GetFixedWindowLimiter(
									partitionKey: httpContext.GetIpAddress(),
									factory: _ => new FixedWindowRateLimiterOptions
									{
										QueueLimit = _appSettings.RLQueueLimit,
										PermitLimit = _appSettings.RLPermitLimit,
										AutoReplenishment = _appSettings.RLAutoReplenishment,
										Window = TimeSpan.FromSeconds(_appSettings.RLWindowInSecond),
										QueueProcessingOrder = (QueueProcessingOrder)_appSettings.RLQueueProcessingOrder
									}));

							options.OnRejected = (context, cancellationToken) =>
							{
								if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
								{
									context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString();
								}
								context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
								context.HttpContext.Response.WriteAsync(text: $"Too many requests (more than {_appSettings.RLPermitLimit} calls in {_appSettings.RLWindowInSecond} seconds). Please try again later.", cancellationToken: cancellationToken);
								return new ValueTask();
							};
						});
					}

					#endregion

					#region Hangfire

					if (!string.IsNullOrEmpty(_appSettings.HangfireDb))
					{
						services.AddHangfire(config =>
						{
							config.UseFilter(new TimeRestrictionServerFilter(startHour: 1, endHour: 7));
							config.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
							config.UseSimpleAssemblyNameTypeSerializer();
							config.UseRecommendedSerializerSettings();

                            config.UseSqlServerStorage(_appSettings.HangfireDb, new SqlServerStorageOptions
							{
								DisableGlobalLocks = true,
								QueuePollInterval = TimeSpan.Zero,
								UseRecommendedIsolationLevel = true,
								CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
								SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5)
							});
						});
                        services.AddHangfireServer(options =>
                        {
                            options.Queues = new[] { "sap_order_send", "default" }; // merge with any queues you already have
                        }); ;
					}


                    #endregion

                    #region SWAGGER

                    if (_appSettings.Swagger.Enabled)
					{
						services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
						services.AddSwaggerGen(options =>
						{
							options.OperationFilter<SwaggerDefaultValues>();
							options.IncludeXmlComments(filePath: XmlCommentsFilePath);
						});
					}

					#endregion

					#region Business Service  

					services.ConfigureBusinessServices();

                    #endregion


                    #region AutoCrop Integration Service

                    services.AddHttpClient<IIntegrationHttpService, IntegrationHttpService>((sp, client) =>
                    {
						var opt = _appSettings.AutoCropSAPApi;

                        client.BaseAddress = new Uri(opt.BaseUrl);
  
                        var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{opt.UserName}:{opt.Password}"));
                        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);

                        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                        client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
                    })
					.AddStandardResilienceHandler(); // retry + timeout + circuit breaker, bundled

                    #endregion
                }
            }
			catch
			{
				//Nothimg to do here as we are handling all exceptions in Configure method using global exception handler
			}
		}

		/// <summary>
		///     
		/// </summary>
		/// <param name="app"></param>
		/// <param name="env"></param>
		/// <param name="provider"></param>
		/// <param name="loggerFactory"></param>
		/// <param name="logger"></param>
		public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, ILoggerFactory loggerFactory, ILogger<Startup> logger)
		{
			logger.LogTrace("Startup::Configure");
			try
			{
				if (env.IsDevelopment())
				{
					app.UseDeveloperExceptionPage();
				}
				else
				{
					#region Exception Handling in Production mode

					app.UseExceptionHandler(a => a.Run(async context =>
					{
						IExceptionHandlerPathFeature feature = context.Features.Get<IExceptionHandlerPathFeature>();
						Exception exception = feature.Error;
						HttpStatusCode code = HttpStatusCode.InternalServerError;

						if (exception is ArgumentNullException)
							code = HttpStatusCode.BadRequest;
						else if (exception is ArgumentException)
							code = HttpStatusCode.BadRequest;
						else if (exception is UnauthorizedAccessException)
							code = HttpStatusCode.Unauthorized;
						else if (exception is WebException && (exception as WebException).Response is HttpWebResponse response)
							code = response.StatusCode;

						logger.LogError($"GLOBAL ERROR HANDLER::HTTP:{code}::{exception.Message}");

						var result = JsonConvert.SerializeObject(exception, Formatting.Indented);

						context.Response.Clear();
						context.Response.ContentType = "application/json";
						await context.Response.WriteAsync(result);
					}));

					app.UseHsts();
					app.UseHttpsRedirection();

					#endregion
				}

				#region CORS

				app.UseCors("CORSPolicies");

				#endregion

				#region Use Static Files

				app.UseStaticFiles();

				#endregion

				#region Routing

				app.UseRouting();

				#endregion

				#region Session

				app.UseSession();

				#endregion

				#region For Authentication

				app.UseAuthentication();

				#endregion

				#region For Authorization

				app.UseAuthorization();

				#endregion

				#region X-Frame-Options

				app.Use(async (context, next) =>
				{
					if (_appSettings.CookieHttpOnly)
					{
						string tokens = context.Request.Cookies["OnlineSalesAutoCrop.X-XSRF-TOKEN"];
						if (!string.IsNullOrEmpty(tokens))
							context.Request.Headers.Append("OnlineSalesAutoCrop.X-XSRF-TOKEN", tokens);
					}
					await next();
				});

				#endregion

				#region RateLimiter

				if (!string.IsNullOrEmpty(_appSettings.RLName))
				{
					app.UseRateLimiter();
				}

				#endregion

				#region Logger

				if (!string.IsNullOrEmpty(_appSettings.LoggerPath))
				{
					loggerFactory.AddFile(pathFormat: $@"{_appSettings.LoggerPath}\Log.txt", minimumLevel: (LogLevel)_appSettings.LoggerMinLevel);
				}

				#endregion

				#region Controllers and SignalR 

				app.UseEndpoints(endpoints =>
				{
					if (!string.IsNullOrEmpty(_appSettings.RLName))
					{
						endpoints.MapControllers().RequireRateLimiting(_appSettings.RLName);
					}
					else
					{
						endpoints.MapControllers();
					}
					endpoints.MapHub<NotificationHub>("/signalRHub");
				});

				#endregion

				#region Forwarded Headers

				app.UseForwardedHeaders();

				#endregion

				#region Request Localization

				app.UseRequestLocalization();

				#endregion

				#region Hangfire

				if (!string.IsNullOrEmpty(_appSettings.HangfireDb))
				{
					app.UseHangfireDashboard();

					RecurringJobOptions options = new() { TimeZone = TimeZoneInfo.Local, MisfireHandling = MisfireHandlingMode.Relaxed };

                    RecurringJob.AddOrUpdate<IBackgroundJobService>(
						"send-sap-orders",
						service => service.ExecuteSendingOrderAsync(CancellationToken.None),
						"*/1 * * * *");
                }

				#endregion

				#region Swagger

				if (_appSettings.IsValid() && _appSettings.Swagger.Enabled)
				{
					app.UseSwagger();
					app.UseSwaggerUI(options =>
					{
						foreach (var description in provider.ApiVersionDescriptions)
						{
							string sjp = string.IsNullOrWhiteSpace(options.RoutePrefix) ? "." : "..";
							options.SwaggerEndpoint($"{sjp}/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
						}
					});
				}

				#endregion

				app.Run(async (context) =>
				{
					context.Response.ContentType = "text/html";
					await context.Response.WriteAsync("<b>Ease Taskforce Api is Running</b>");
				});
			}
			catch (Exception ex)
			{
				logger.LogError(ex);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		static string XmlCommentsFilePath
		{
			get
			{
				var basePath = AppContext.BaseDirectory;
				var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
				return Path.Combine(basePath, fileName);
			}
		}
	}
}
