namespace MyOffice.Web;

using System.IO;
using System.Linq;
using Core.Extensions;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Logging;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Identity;
using Identity.Domain;
using Identity.ExternalProviders;
using Identity.Repositories;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Configuration;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Core.Identity;
using Data.Repositories.Account;
using Data.Repositories.Currency;
using Data.Repositories.Item;
using Data.Repositories.Users;
using DbContext;
using Services.Users;
using Shared;
using Identity.Configure;
using Infrastructure;
using Microsoft.Extensions.Logging.Console;
using IdentityServer4.Services;
using Models.Account;
using MyOffice.Services.Currency;
using Services.Account;
using Services.Item;
using MyOffice.Services.Dashboard;
using Services.Account.Domain;
using MyOffice.Services.Identity;
using AutoMapper;
using Models.Motion;

//TODO: Data.Model only Repository and Service, response  <-> mapper <-> web <-> mapper <-> service <-> repository
//TODO: Project management
//TODO: Model names. Controller = xxxRequest/xxxResponse. Service xxxInput/xxxOutput.
//TODO: Validate string as Guid when id
//TODO: Logging

public class Program
{
	public static void Main(string[] args)
	{
		var builder = WebApplication.CreateBuilder(args);
		AddServices(builder);

		var app = builder.Build();
		Configure(app);

		app.Run();
	}

	private static void AddServices(WebApplicationBuilder builder)
	{
		builder.Services.AddLogging(logging =>
			logging.AddSimpleConsole(options =>
			{
				//options.SingleLine = true;
				options.TimestampFormat = "[HH:mm:ss] ";
				options.ColorBehavior = LoggerColorBehavior.Enabled;
			})
		);
		//File Logger
		builder.Logging.AddFile(builder.Configuration.GetSection("Logging"));

		// shared configuration
		builder.Configuration.AddConfiguration(SharedConfiguration.CreateConfigurationContainer());

		// Database
		var databaseProvider = builder.Configuration["DatabaseProvider"];
		if (databaseProvider == null)
			throw new NullReferenceException($"Configuration DatabaseProvider {databaseProvider}");

		var connectionString = builder.Configuration.GetConnectionString(databaseProvider);
		if (connectionString == null)
			throw new NullReferenceException($"Configuration ConnectionString {databaseProvider}");

		var connectionConfiguration = new ConnectionConfiguration(databaseProvider, connectionString);
		RepositoryInitializer.Initialize(builder.Services, connectionConfiguration);

		// Configurations
		builder.Services.Configure<ExternalProvidersConfig>(
			builder.Configuration.GetSection("ExternalProviders")
		);

		InitializeGlobalSettings(builder);

		AddIdentityServices(builder);

		builder.Services.AddScoped<IContextProvider, ContextProvider>();

		AddMapping(builder);

		AddRepositories(builder);

		AddBusinessServices(builder);

		builder.Services
			.AddControllers()
			.AddJsonOptions(options =>
			{
				options.AllowInputFormatterExceptionMessages = builder.Environment.IsDevelopment();
				options.JsonSerializerOptions.MaxDepth = 0;
				options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
				options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
			});

		// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
		//builder.Services.AddEndpointsApiExplorer();
		//builder.Services.AddSwaggerGen();

		builder.Services.AddCors();
	}

	private static void InitializeGlobalSettings(WebApplicationBuilder builder)
	{
		builder.Services.Configure<GlobalSettings>(_ => { });
		builder.Services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<GlobalSettings>>().Value);
	}

	private static void AddIdentityServices(WebApplicationBuilder builder)
	{
		// add identity
		builder.Services
			.AddIdentity<ApplicationUser<Guid>, ApplicationRole>()
			.AddUserStore<UserStore>()
			.AddRoleStore<RoleStore>()
			.AddUserManager<AppUserManager>()
			.AddDefaultTokenProviders();

		builder.Services.AddScoped<ICorsPolicyService, CorsPolicyService>();
		builder.Services.AddScoped<IPasswordHasher<ApplicationUser<Guid>>, PasswordHasher>();

		// Identity Services
		builder.Services.AddScoped<IUserStore<ApplicationUser<Guid>>, UserStore>();
		builder.Services.AddScoped<IRoleStore<ApplicationRole>, RoleStore>();

		// External providers
		builder.Services.AddScoped<IExternalProviderValidator, ExternalProviderValidatorAuth0>();
		builder.Services.AddScoped<IExternalProviderValidator, ExternalProviderValidatorGoogle>();

		// Configure Identity options and password complexity here
		builder.Services.Configure<IdentityOptions>(options =>
		{
			// User settings
			options.User.RequireUniqueEmail = true;

			// Password settings
			options.Password.RequireDigit = true;
			options.Password.RequiredLength = 8;
			options.Password.RequireNonAlphanumeric = true;
			options.Password.RequireUppercase = true;
			options.Password.RequireLowercase = true;

			// Lockout settings
			options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
			options.Lockout.MaxFailedAccessAttempts = 10;
		});

		// Adds IdentityServer.
		builder.Services
			.AddIdentityServer(o =>
			{
				o.UserInteraction = new UserInteractionOptions()
				{
					LoginUrl = "/authentication/signin",
					LoginReturnUrlParameter = "returnUrl"
				};
				//o.IssuerUri = "http://localhost:9300";
			})
			.AddSigningCredential(CreateSigningCredential(builder))
			.AddPersistedGrantStore<PersistedGrantStore>()
			.AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
			.AddInMemoryApiScopes(IdentityServerConfig.GetApiScopes())
			.AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
			.AddInMemoryClients(IdentityServerConfig.GetClients())
			.AddAspNetIdentity<ApplicationUser<Guid>>()
			.AddProfileService<ProfileService>()
			.AddExtensionGrantValidator<ExtensionGrantValidator>();

		var authentication = builder.Services
			.AddAuthentication(options =>
			{
				options.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
				options.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
				options.DefaultChallengeScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
			});

		var frontEndHost = builder.Configuration.GetValue<string>("FrontEnd:Host");
		var isDynamicFrontEndHost = frontEndHost.IsMissing();
		if (isDynamicFrontEndHost)
		{
			builder.Services.ConfigureOptions<ConfigureIdentityServerOptions>();
			authentication.AddIdentityServerAuthentication();
		}
		else
		{
			authentication.AddIdentityServerAuthentication(options =>
			{
				options.RequireHttpsMetadata = false;
				options.ApiName = IdentityServerConfig.ApiName;
				options.Authority = frontEndHost;
			});
		}
	}

	private static void AddMapping(WebApplicationBuilder builder)
	{
		builder.Services.AddSingleton(provider => new MapperConfiguration(cfg =>
		{
			cfg.AddProfile(new AccountServiceProfile());
			cfg.AddProfile(new ViewModelProfile());
			cfg.AddProfile(new AccountViewModelProfile(provider.CreateScope().ServiceProvider.GetService<IContextProvider>()!));

			cfg.AddProfile(new AccountControllerProfile());
		}).CreateMapper());
	}

	private static void AddRepositories(WebApplicationBuilder builder)
	{
		builder.Services.AddScoped<IUserRepository, UserRepository>();
		builder.Services.AddScoped<IUserExternalRepository, UserExternalRepository>();

		builder.Services.AddScoped<ICurrencyGlobalRepository, CurrencyGlobalRepository>();
		builder.Services.AddScoped<ICurrencyRepository, CurrencyRepository>();
		builder.Services.AddScoped<ICurrencyRateRepository, CurrencyRateRepository>();

		builder.Services.AddScoped<IAccountCategoryRepository, AccountCategoryRepository>();
		builder.Services.AddScoped<IAccountRepository, AccountRepository>();
		builder.Services.AddScoped<IAccountAccessRepository, AccountAccessRepository>();
		builder.Services.AddScoped<IAccountAccountCategoryRepository, AccountAccountCategoryRepository>();
		
		builder.Services.AddScoped<IItemCategoryRepository, ItemCategoryRepository>();
		builder.Services.AddScoped<IItemRepository, ItemRepository>();
		builder.Services.AddScoped<IItemGlobalRepository, ItemGlobalRepository>();
		builder.Services.AddScoped<IMotionRepository, MotionRepository>();
	}

	private static void AddBusinessServices(WebApplicationBuilder builder)
	{
		builder.Services.AddScoped<UserService, UserService>();
		builder.Services.AddScoped<CurrencyService, CurrencyService>();
		builder.Services.AddScoped<AccountService, AccountService>();
		builder.Services.AddScoped<ItemService, ItemService>();
		builder.Services.AddScoped<DashboardService, DashboardService>();
	}

	private static readonly ConcurrentDictionary<string, PhysicalFileInfo> _staticFilesCache = new();

	private static bool _firstRequest = true;
	private static readonly object LockFirstRequest = new();

	private static void Configure(WebApplication app)
	{
		// Configure the HTTP request pipeline.
		if (app.Environment.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
			IdentityModelEventSource.ShowPII = true;
		}
		else
		{
			IdentityModelEventSource.ShowPII = true;
			app.UseExceptionHandler("/Error");
		}

		app.UseDefaultFiles();
		app.UseStaticFiles();

		var logger = app.Logger;

		// configuration on first request
		app.Use(async (ctx, next) =>
		{
			var path = ctx.Request.Path;

			GlobalSettings? globalSettings = null;

			if (_firstRequest)
			{
				// lock
				lock (LockFirstRequest)
				{
					// recheck
					if (_firstRequest)
					{
						_firstRequest = false;

						globalSettings = ctx.RequestServices.GetRequiredService<GlobalSettings>();

						var configuration = ctx.RequestServices.GetRequiredService<IConfiguration>();
						var frontEndHost = configuration.GetValue<string>("FrontEnd:Host");
						var isDynamicFrontEndHost = frontEndHost.IsMissing();
						if (!isDynamicFrontEndHost)
						{
							globalSettings.Host = frontEndHost;
						}
						else
						{
							// set real frontend host
							globalSettings.Host = GetFrontendHost(ctx, logger);
						}

						// update identity server
						var options = ctx.RequestServices.GetRequiredService<IdentityServerOptions>();
						options.IssuerUri = globalSettings.Host;

						// update identity server client redirect uri
						var clients = ctx.RequestServices.GetRequiredService<IEnumerable<Client>>();
						var client = clients.FirstOrDefault()!;
						client.RedirectUris = new List<string>
						{
							$"{globalSettings.Host}/silent-refresh.html"
						};
					}
				}
			}

			// 
			if (path.Value.EqualsIgnoreCase("/.well-known/openid-configuration"))
			{
				globalSettings ??= ctx.RequestServices.GetRequiredService<GlobalSettings>();
				logger.LogInformation($"OC Host: {globalSettings.Host}");
				ctx.SetIdentityServerOrigin(globalSettings.Host);
				var options = ctx.RequestServices.GetRequiredService<IdentityServerOptions>();
				options.IssuerUri = globalSettings.Host;
			}

			await next();
		});

		// send index.html to any routes except coreRoutes
		var coreRoutes = new[]
		{
			// API routes
			new PathString("/api"),
			// Identity server routes
			new PathString("/.well-known"),
			new PathString("/connect"),
			new PathString("/silent-refresh.html")
		};

		var webRootPath = app.Configuration.GetValue<string>(WebHostDefaults.ContentRootKey);
		var wwwrootPath = Path.Combine(webRootPath, "wwwroot");
		app.Use(async (context, next) =>
		{
			var path = context.Request.Path;

			if (path.Value == null) return;
			if (coreRoutes.Any(x => path.StartsWithSegments(x, StringComparison.OrdinalIgnoreCase)))
			{
				await next();
			}
			else
			{
				if (!_staticFilesCache.TryGetValue(path.Value, out var physicalFileInfo))
				{
					var segments = path.Value.Split('/', '\\');
					var fileInfo = new FileInfo(Path.Combine(wwwrootPath, segments.LastOrDefault()!));
					if (!fileInfo.Exists)
					{
						return;
					}

					physicalFileInfo = new PhysicalFileInfo(fileInfo);
					_staticFilesCache.TryAdd(path.Value, physicalFileInfo);
				}

				await context.Response.SendFileAsync(physicalFileInfo);
				await context.Response.CompleteAsync();
			}
		});

		app.UseCors(builder => builder
			.AllowAnyOrigin()
			.AllowAnyHeader()
			.AllowAnyMethod());

		app.UseIdentityServer();
		app.UseAuthorization();

		app.MapControllers();
	}

	private static string GetFrontendHost(HttpContext ctx, ILogger logger)
	{
		// internet <-> nginx (https) <-> site(http)
		var host = $"{ctx.Request.Headers["Referer"]}";
		host = host.EndsWith("/") ? host.Substring(0, host.Length - 1) : host;

		return host;
	}

	private static SigningCredentials CreateSigningCredential(WebApplicationBuilder builder)
	{
		var rsa = RSA.Create();

		var webRootPath = builder.Configuration.GetValue<string>(WebHostDefaults.ContentRootKey);
		var privateKey = File.ReadAllText(Path.Combine(webRootPath, "rsa_key.pem"));
		privateKey = privateKey
			.Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty)
			.Replace("-----END RSA PRIVATE KEY-----", string.Empty)
			.Replace(Environment.NewLine, string.Empty);

		var privateKeyBytes = Convert.FromBase64String(privateKey);
		rsa.ImportRSAPrivateKey(privateKeyBytes, out int _);

		var rsaSecurityKey = new RsaSecurityKey(rsa);
		var credentials = new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256);

		return credentials;
	}
}