namespace MyOffice.Web;

using System.IO;
using System.Linq;
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.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Console;
using System.Text.Json.Serialization;

using Identity;
using Identity.Domain;
using Identity.ExternalProviders;
using Identity.Repositories;
using Auth;
using Core.Identity;
using Core.Extensions;
using Data.Repositories.Account;
using Data.Repositories.Currency;
using Data.Repositories.Item;
using Data.Repositories.Users;
using DbContext;
using Services.Users;
using Shared;
using Infrastructure;
using Models.Account;
using MyOffice.Services.Currency;
using Services.Account;
using Services.Item;
using MyOffice.Services.Dashboard;
using MyOffice.Services.Identity;
using MyOffice.Services.Account.Domain;
using MyOffice.Web.Infrastructure.Attributes;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using MyOffice.Web.Infrastructure.Filters;

//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: Test back
//TODO: Test front
//TODO: Test DB ???
//TODO: Email confirmation
//TODO: Password recovery
//TODO: Account sharing
//TODO: /dashboard/dashboard -> /dashboard/main or /dashboard
//TODO: SPA load categories twice menu + setting (cache)
//TODO: SPA update account category -> update menu
//TODO: Response model from base type
//TODO: XXXResult -> XXXStatus
//TODO: SPA isAllowDelete -> allowDelete (remove is)
//TODO: Repository auto registration
//TODO: Services auto registration
//TODO: openid-configuration failed - lock login
//TODO: BUG some times after login redirect to dashboard but exists return url
//TODO: automapper -> Extension ToModel() ToDbo() FromModel() FromDbo()
//TODO: SPA all http requests -> services
//TODO: Items, select category -> save url to allow refresh
//TODO: Accounts, select category -> save url to allow refresh

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

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

		app.Run();
	}

	private static void AddServices(WebApplicationBuilder builder, string[] args)
	{
		#region Loging

		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"));

		#endregion Loging

		builder.Services.Configure<LoggerFilterOptions>(options =>
		{
			options.AddFilter("OpenIddict", LogLevel.Warning);
		});

		// Shared JSON after appsettings.*, then restore higher-priority sources
		builder.Configuration.AddSharedAppSettings(builder.Environment.EnvironmentName);
		builder.Configuration.AddEnvironmentVariables();
		builder.Configuration.AddCommandLine(args);

		#region 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);
		// Before OpenIddictSeeder so the schema exists when clients/scopes are registered.
		builder.Services.AddHostedService<DatabaseInitializerHostedService>();

		#endregion Database

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

		InitializeGlobalSettings(builder);

		AddIdentityServices(builder);

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

		AddAutoMapper(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;
				options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
				options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
			});

		builder.Services.AddCors();

		builder.Services.AddControllersWithViews(options =>
		{
			options.Conventions.Add(new DefaultFromBodyBindingConvention());
			options.Filters.Add(typeof(GlobalModelStateValidatorAttribute));
			options.Filters.Add(typeof(ResponseFilter));
		});

		builder.Services.Configure<MvcOptions>(options =>
		{
		});

		builder.Services.Configure<ApiBehaviorOptions>(x => {
		});

		builder.Services.AddProblemDetails();
		builder.Services.AddExceptionHandler<UnhandledExceptionHandler>();
		builder.Services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();
	}

	private static void InitializeGlobalSettings(WebApplicationBuilder builder)
	{
		var frontEndHost = builder.Configuration.GetValue<string>("FrontEnd:Host");
		if (frontEndHost.IsMissing())
		{
			frontEndHost = "http://localhost:4300";
			if (!builder.Environment.IsDevelopment())
			{
				throw new InvalidOperationException(
					"FrontEnd:Host must be set outside Development (do not derive it from Referer).");
			}
		}

		frontEndHost = frontEndHost!.TrimEnd('/');
		builder.Services.Configure<GlobalSettings>(options => options.Host = frontEndHost);
		builder.Services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<GlobalSettings>>().Value);
	}

	private static void AddIdentityServices(WebApplicationBuilder builder)
	{
		builder.Services.AddHttpContextAccessor();

		// add identity
		builder.Services
			.AddIdentity<ApplicationUser<Guid>, ApplicationRole>()
			.AddUserStore<UserStore>()
			.AddRoleStore<RoleStore>()
			.AddUserManager<AppUserManager>()
			.AddSignInManager<SignInManager<ApplicationUser<Guid>>>()
			.AddDefaultTokenProviders();

		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;

			// Email+password accounts can sign in immediately after register
			options.SignIn.RequireConfirmedAccount = false;
			options.SignIn.RequireConfirmedEmail = false;

			// 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;
		});

		builder.Services.Configure<ForwardedHeadersOptions>(options =>
		{
			options.ForwardedHeaders =
				ForwardedHeaders.XForwardedFor
				| ForwardedHeaders.XForwardedProto
				| ForwardedHeaders.XForwardedHost;
			// Trust nginx in Docker; headers are only present behind the proxy.
			options.KnownNetworks.Clear();
			options.KnownProxies.Clear();
		});

		builder.Services.AddMyOfficeOpenIddict(builder.Configuration, builder.Environment);
	}

	private static void AddAutoMapper(WebApplicationBuilder builder)
	{
		// AutoMapper 15+ dual license: runs without a key (warning logs only).
		// Set AutoMapper:LicenseKey (or AUTOMAPPER_LICENSE_KEY) from https://automapper.io — free Community tier if under $5M revenue.
		var licenseKey = builder.Configuration["AutoMapper:LicenseKey"];

		builder.Services.AddAutoMapper(
			c =>
			{
				if (!string.IsNullOrWhiteSpace(licenseKey))
				{
					c.LicenseKey = licenseKey;
				}

				c.AllowNullCollections = true;
				c.AllowNullDestinationValues = true;
			},
			typeof(AccountDto).Assembly,
			typeof(AccountViewModel).Assembly
		);
	}

	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<IAccountAccessInviteRepository, AccountAccessInviteRepository>();
		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>();
		
		builder.Services.AddScoped<IVerificationCodeRepository, VerificationCodeRepository>();
		builder.Services.AddScoped<IEmailTemplateRepository, EmailTemplateRepository>();
	}

	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 void Configure(WebApplication app)
	{
		// nginx (prod) terminates TLS and forwards X-Forwarded-Proto/Host
		app.UseForwardedHeaders();

		// Configure the HTTP request pipeline.
		if (app.Environment.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
			IdentityModelEventSource.ShowPII = true;
		}
		else
		{
			app.UseExceptionHandler();
			app.UseStatusCodePages();
		}

		// Explicit routing so CORS runs before endpoint matching (preflight / OPTIONS).
		app.UseRouting();
		ConfigureCors(app);

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

		var logger = app.Logger;
		var globalSettings = app.Services.GetRequiredService<GlobalSettings>();
		logger.LogInformation("FrontEnd host: {Host}", globalSettings.Host);

		// send index.html / static files for non-API routes
		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)
			{
				await next();
				return;
			}

			if (coreRoutes.Any(x => path.StartsWithSegments(x, StringComparison.OrdinalIgnoreCase)))
			{
				await next();
				return;
			}

			if (!_staticFilesCache.TryGetValue(path.Value, out var physicalFileInfo))
			{
				var segments = path.Value.Split('/', '\\');
				var fileName = segments.LastOrDefault();
				var fileInfo = string.IsNullOrEmpty(fileName)
					? null
					: new FileInfo(Path.Combine(wwwrootPath, fileName));

				if (fileInfo is null || !fileInfo.Exists)
				{
					// SPA deep link fallback
					var indexInfo = new FileInfo(Path.Combine(wwwrootPath, "index.html"));
					if (!indexInfo.Exists)
					{
						context.Response.StatusCode = StatusCodes.Status404NotFound;
						return;
					}

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

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

		app.UseAuthentication();
		app.UseAuthorization();

		MapRoutes(app);

		app.MapControllers();
	}

	private static void ConfigureCors(WebApplication app)
	{
		var allowedOrigins = (app.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [])
			.Where(x => !string.IsNullOrWhiteSpace(x))
			.Select(x => x.Trim().TrimEnd('/'))
			.Distinct(StringComparer.OrdinalIgnoreCase)
			.ToList();

		var frontEndHost = app.Configuration.GetValue<string>("FrontEnd:Host")?.Trim().TrimEnd('/');
		if (!string.IsNullOrWhiteSpace(frontEndHost)
			&& !allowedOrigins.Contains(frontEndHost, StringComparer.OrdinalIgnoreCase))
		{
			allowedOrigins.Add(frontEndHost);
		}

		var isDocker = string.Equals(
			app.Environment.EnvironmentName,
			"Docker",
			StringComparison.OrdinalIgnoreCase);

		app.Logger.LogInformation(
			"CORS origins: {Origins}",
			allowedOrigins.Count > 0 ? string.Join(", ", allowedOrigins) : "(none)");

		app.UseCors(policy =>
		{
			if (allowedOrigins.Count > 0)
			{
				policy.WithOrigins(allowedOrigins.ToArray())
					.AllowAnyHeader()
					.AllowAnyMethod();
			}
			else if (app.Environment.IsDevelopment() || isDocker)
			{
				// Demo / local: SPA and API are on different host ports.
				policy.AllowAnyOrigin()
					.AllowAnyHeader()
					.AllowAnyMethod();
			}
		});
	}
}