using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Source.Common.Xml;

namespace StellaOps.Feedser.Source.Common.Http;

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Registers a named HTTP client configured for a source connector with allowlisted hosts and sensible defaults.
    /// </summary>
    public static IHttpClientBuilder AddSourceHttpClient(this IServiceCollection services, string name, Action<SourceHttpClientOptions> configure)
        => services.AddSourceHttpClient(name, (_, options) => configure(options));

    public static IHttpClientBuilder AddSourceHttpClient(this IServiceCollection services, string name, Action<IServiceProvider, SourceHttpClientOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentNullException.ThrowIfNull(configure);

        services.AddOptions<SourceHttpClientOptions>(name).Configure<IServiceProvider>((options, sp) =>
        {
            configure(sp, options);
            SourceHttpClientConfigurationBinder.Apply(sp, name, options);
        });

        return services
            .AddHttpClient(name)
            .ConfigureHttpClient((sp, client) =>
            {
                var options = sp.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>().Get(name);

                if (options.BaseAddress is not null)
                {
                    client.BaseAddress = options.BaseAddress;
                }

                client.Timeout = options.Timeout;
                client.DefaultRequestHeaders.UserAgent.Clear();
                client.DefaultRequestHeaders.UserAgent.ParseAdd(options.UserAgent);
                client.DefaultRequestVersion = options.RequestVersion;
                client.DefaultVersionPolicy = options.VersionPolicy;

                foreach (var header in options.DefaultRequestHeaders)
                {
                    client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
                }
            })
            .ConfigurePrimaryHttpMessageHandler((sp) =>
            {
                var options = sp.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>().Get(name).Clone();
                var handler = new SocketsHttpHandler
                {
                    AllowAutoRedirect = options.AllowAutoRedirect,
                    AutomaticDecompression = DecompressionMethods.All,
                    EnableMultipleHttp2Connections = options.EnableMultipleHttp2Connections,
                };
                options.ConfigureHandler?.Invoke(handler);
                ApplyProxySettings(handler, options);

                if (options.ServerCertificateCustomValidation is not null)
                {
                    handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) =>
                    {
                        X509Certificate2? certToValidate = certificate as X509Certificate2;
                        X509Certificate2? disposable = null;
                        if (certToValidate is null && certificate is not null)
                        {
                            disposable = X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
                            certToValidate = disposable;
                        }

                        try
                        {
                            return options.ServerCertificateCustomValidation(certToValidate, chain, sslPolicyErrors);
                        }
                        finally
                        {
                            disposable?.Dispose();
                        }
                    };
                }
                else if (options.TrustedRootCertificates.Count > 0 && handler.SslOptions.RemoteCertificateValidationCallback is null)
                {
                    var trustedRoots = new X509Certificate2Collection();
                    foreach (var certificate in options.TrustedRootCertificates)
                    {
                        trustedRoots.Add(certificate);
                    }

                    handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) =>
                    {
                        if (errors == SslPolicyErrors.None)
                        {
                            return true;
                        }

                        if (certificate is null)
                        {
                            return false;
                        }

                        X509Certificate2? certToValidate = certificate as X509Certificate2;
                        X509Certificate2? disposable = null;
                        try
                        {
                            if (certToValidate is null)
                            {
                                disposable = X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
                                certToValidate = disposable;
                            }

                            using var customChain = new X509Chain();
                            customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
                            customChain.ChainPolicy.CustomTrustStore.Clear();
                            customChain.ChainPolicy.CustomTrustStore.AddRange(trustedRoots);
                            customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
                            customChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

                            if (chain is not null)
                            {
                                foreach (var element in chain.ChainElements)
                                {
                                    customChain.ChainPolicy.ExtraStore.Add(element.Certificate);
                                }
                            }

                            return certToValidate is not null && customChain.Build(certToValidate);
                        }
                        finally
                        {
                            disposable?.Dispose();
                        }
                    };
                }

                return handler;
            })
            .AddHttpMessageHandler(sp =>
            {
                var options = sp.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>().Get(name).Clone();
                return new AllowlistedHttpMessageHandler(options);
            });
    }

    /// <summary>
    /// Registers shared helpers used by source connectors.
    /// </summary>
    public static IServiceCollection AddSourceCommon(this IServiceCollection services)
    {
        ArgumentNullException.ThrowIfNull(services);

        services.AddSingleton<Json.JsonSchemaValidator>();
        services.AddSingleton<Json.IJsonSchemaValidator>(sp => sp.GetRequiredService<Json.JsonSchemaValidator>());
        services.AddSingleton<XmlSchemaValidator>();
        services.AddSingleton<IXmlSchemaValidator>(sp => sp.GetRequiredService<XmlSchemaValidator>());
        services.AddSingleton<Fetch.IJitterSource, Fetch.CryptoJitterSource>();
        services.AddSingleton<Fetch.RawDocumentStorage>();
        services.AddSingleton<Fetch.SourceFetchService>();

        return services;
    }

    private static void ApplyProxySettings(SocketsHttpHandler handler, SourceHttpClientOptions options)
    {
        if (options.ProxyAddress is null)
        {
            return;
        }

        var proxy = new WebProxy(options.ProxyAddress)
        {
            BypassProxyOnLocal = options.ProxyBypassOnLocal,
            UseDefaultCredentials = options.ProxyUseDefaultCredentials,
        };

        if (options.ProxyBypassList.Count > 0)
        {
            proxy.BypassList = options.ProxyBypassList.ToArray();
        }

        if (!options.ProxyUseDefaultCredentials
            && !string.IsNullOrWhiteSpace(options.ProxyUsername))
        {
            proxy.Credentials = new NetworkCredential(
                options.ProxyUsername,
                options.ProxyPassword ?? string.Empty);
        }

        handler.Proxy = proxy;
        handler.UseProxy = true;
    }
}
