namespace MyOffice.Web.Auth;

using System.Security.Cryptography;
using DbContext;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
using OpenIddict.Validation.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;

public static class OpenIddictServiceCollectionExtensions
{
	public static IServiceCollection AddMyOfficeOpenIddict(
		this IServiceCollection services,
		IConfiguration configuration,
		IHostEnvironment environment
	)
	{
		services.AddOpenIddict()
			.AddCore(options =>
			{
				options.UseEntityFrameworkCore()
					.UseDbContext<AppDbContext>();
			})
			.AddServer(options =>
			{
				options.SetAuthorizationEndpointUris("/connect/authorize")
					.SetTokenEndpointUris("/connect/token")
					.SetUserInfoEndpointUris("/connect/userinfo")
					.SetEndSessionEndpointUris("/connect/logout");

				options.AllowAuthorizationCodeFlow()
					.AllowPasswordFlow()
					.AllowRefreshTokenFlow()
					.AllowCustomFlow(OpenIddictAuthConstants.ExternalGrantType);

				options.RegisterScopes(
					Scopes.OpenId,
					Scopes.Email,
					Scopes.Profile,
					Scopes.Roles,
					Scopes.OfflineAccess,
					OpenIddictAuthConstants.ApiScope,
					OpenIddictAuthConstants.RolesScope);

				ConfigureCryptography(options, configuration, environment);

				var aspNetCoreBuilder = options.UseAspNetCore()
					.EnableAuthorizationEndpointPassthrough()
					.EnableUserInfoEndpointPassthrough()
					.EnableEndSessionEndpointPassthrough();

				// Local HTTP only. In production nginx terminates TLS and
				// ForwardedHeaders restores https:// for OpenIddict.
				// Docker demo also serves plain HTTP (OpenIddict:AllowHttp / Environment=Docker).
				if (environment.IsDevelopment()
					|| IsDockerEnvironment(environment)
					|| configuration.GetValue("OpenIddict:AllowHttp", false))
				{
					aspNetCoreBuilder.DisableTransportSecurityRequirement();
				}

				// Password and external grants need custom handlers (separate registrations —
				// a second UseScopedHandler() on the same builder replaces the first).
				options.AddEventHandler<HandleTokenRequestContext>(builder =>
					builder.UseScopedHandler<PasswordGrantHandler>());

				options.AddEventHandler<HandleTokenRequestContext>(builder =>
					builder.UseScopedHandler<ExternalGrantHandler>());
			})
			.AddValidation(options =>
			{
				options.UseLocalServer();
				options.UseAspNetCore();
			});

		services.AddAuthentication(options =>
		{
			options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
			options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
			options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
		});

		services.AddHostedService<OpenIddictSeeder>();

		return services;
	}

	private static void ConfigureCryptography(
		OpenIddictServerBuilder options,
		IConfiguration configuration,
		IHostEnvironment environment
	)
	{
		// Dev: certs in the user profile. Docker/demo: ephemeral keys (no cert store in containers).
		if (string.IsNullOrWhiteSpace(configuration["OpenIddict:SigningKeyPath"])
			&& (environment.IsDevelopment()
				|| IsDockerEnvironment(environment)
				|| configuration.GetValue("OpenIddict:UseEphemeralKeys", false)))
		{
			if (IsDockerEnvironment(environment)
				|| configuration.GetValue("OpenIddict:UseEphemeralKeys", false))
			{
				options.AddEphemeralEncryptionKey()
					.AddEphemeralSigningKey();
			}
			else
			{
				options.AddDevelopmentEncryptionCertificate()
					.AddDevelopmentSigningCertificate();
			}

			return;
		}

		var contentRoot = configuration.GetValue<string>(WebHostDefaults.ContentRootKey)
			?? throw new InvalidOperationException("Content root path is not configured.");

		var signingPath = ResolveKeyPath(
			configuration["OpenIddict:SigningKeyPath"],
			contentRoot,
			"keys/signing.pem");
		var encryptionPath = ResolveKeyPath(
			configuration["OpenIddict:EncryptionKeyPath"],
			contentRoot,
			"keys/encryption.pem");

		if (!File.Exists(signingPath))
		{
			throw new InvalidOperationException(
				$"OpenIddict signing key not found at '{signingPath}'. " +
				"Set OpenIddict:SigningKeyPath or place keys/signing.pem under the content root.");
		}

		options.AddSigningKey(LoadRsaSecurityKey(signingPath));

		if (File.Exists(encryptionPath) &&
			!string.Equals(encryptionPath, signingPath, StringComparison.OrdinalIgnoreCase))
		{
			options.AddEncryptionKey(LoadRsaSecurityKey(encryptionPath));
		}
		else
		{
			// Prefer a dedicated encryption key. Fallback keeps older single-key setups working.
			options.AddEncryptionKey(LoadRsaSecurityKey(signingPath));
		}
	}

	private static bool IsDockerEnvironment(IHostEnvironment environment) =>
		string.Equals(environment.EnvironmentName, "Docker", StringComparison.OrdinalIgnoreCase);

	private static string ResolveKeyPath(string? configuredPath, string contentRoot, string defaultRelative)
	{
		if (!string.IsNullOrWhiteSpace(configuredPath))
		{
			return Path.IsPathRooted(configuredPath)
				? configuredPath
				: Path.GetFullPath(Path.Combine(contentRoot, configuredPath));
		}

		return Path.GetFullPath(Path.Combine(contentRoot, defaultRelative));
	}

	private static RsaSecurityKey LoadRsaSecurityKey(string path)
	{
		var privateKey = File.ReadAllText(path)
			.Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
			.Replace("-----END RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
			.Replace("-----BEGIN PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
			.Replace("-----END PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
			.Replace("\r", string.Empty, StringComparison.Ordinal)
			.Replace("\n", string.Empty, StringComparison.Ordinal);

		var rsa = RSA.Create();
		var keyBytes = Convert.FromBase64String(privateKey);

		try
		{
			rsa.ImportRSAPrivateKey(keyBytes, out _);
		}
		catch (CryptographicException)
		{
			rsa.ImportPkcs8PrivateKey(keyBytes, out _);
		}

		return new RsaSecurityKey(rsa);
	}
}
