using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace StellaOps.Feedser.Source.Common.Http;

internal static class SourceHttpClientConfigurationBinder
{
    private const string FeedserSection = "feedser";
    private const string HttpClientsSection = "httpClients";
    private const string SourcesSection = "sources";
    private const string HttpSection = "http";
    private const string AllowInvalidKey = "allowInvalidCertificates";
    private const string TrustedRootPathsKey = "trustedRootPaths";
    private const string ProxySection = "proxy";
    private const string ProxyAddressKey = "address";
    private const string ProxyBypassOnLocalKey = "bypassOnLocal";
    private const string ProxyBypassListKey = "bypassList";
    private const string ProxyUseDefaultCredentialsKey = "useDefaultCredentials";
    private const string ProxyUsernameKey = "username";
    private const string ProxyPasswordKey = "password";
    private const string OfflineRootKey = "offlineRoot";
    private const string OfflineRootEnvironmentVariable = "FEEDSER_OFFLINE_ROOT";

    public static void Apply(IServiceProvider services, string clientName, SourceHttpClientOptions options)
    {
        var configuration = services.GetService(typeof(IConfiguration)) as IConfiguration;
        if (configuration is null)
        {
            return;
        }

        var loggerFactory = services.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
        var logger = loggerFactory?.CreateLogger("SourceHttpClientConfiguration");

        var hostEnvironment = services.GetService(typeof(IHostEnvironment)) as IHostEnvironment;

        var processed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var section in EnumerateCandidateSections(configuration, clientName))
        {
            if (section is null || !section.Exists() || !processed.Add(section.Path))
            {
                continue;
            }

            ApplySection(section, configuration, hostEnvironment, clientName, options, logger);
        }
    }

    private static IEnumerable<IConfigurationSection> EnumerateCandidateSections(IConfiguration configuration, string clientName)
    {
        var names = BuildCandidateNames(clientName);
        foreach (var name in names)
        {
            var httpClientSection = GetSection(configuration, FeedserSection, HttpClientsSection, name);
            if (httpClientSection is not null && httpClientSection.Exists())
            {
                yield return httpClientSection;
            }

            var sourceHttpSection = GetSection(configuration, FeedserSection, SourcesSection, name, HttpSection);
            if (sourceHttpSection is not null && sourceHttpSection.Exists())
            {
                yield return sourceHttpSection;
            }
        }
    }

    private static IEnumerable<string> BuildCandidateNames(string clientName)
    {
        yield return clientName;

        if (clientName.StartsWith("source.", StringComparison.OrdinalIgnoreCase) && clientName.Length > "source.".Length)
        {
            yield return clientName["source.".Length..];
        }

        var noDots = clientName.Replace('.', '_');
        if (!string.Equals(noDots, clientName, StringComparison.OrdinalIgnoreCase))
        {
            yield return noDots;
        }
    }

    private static IConfigurationSection? GetSection(IConfiguration configuration, params string[] pathSegments)
    {
        IConfiguration? current = configuration;
        foreach (var segment in pathSegments)
        {
            if (current is null)
            {
                return null;
            }

            current = current.GetSection(segment);
        }

        return current as IConfigurationSection;
    }

    private static void ApplySection(
        IConfigurationSection section,
        IConfiguration rootConfiguration,
        IHostEnvironment? hostEnvironment,
        string clientName,
        SourceHttpClientOptions options,
        ILogger? logger)
    {
        var allowInvalid = section.GetValue<bool?>(AllowInvalidKey);
        if (allowInvalid == true)
        {
            options.AllowInvalidServerCertificates = true;
            var previous = options.ServerCertificateCustomValidation;
            options.ServerCertificateCustomValidation = (certificate, chain, errors) =>
            {
                if (allowInvalid == true)
                {
                    return true;
                }

                return previous?.Invoke(certificate, chain, errors) ?? errors == SslPolicyErrors.None;
            };

            logger?.LogWarning(
                "Source HTTP client '{ClientName}' is configured to bypass TLS certificate validation.",
                clientName);
        }

        var offlineRoot = section.GetValue<string?>(OfflineRootKey)
            ?? rootConfiguration.GetSection(FeedserSection).GetValue<string?>(OfflineRootKey)
            ?? Environment.GetEnvironmentVariable(OfflineRootEnvironmentVariable);

        ApplyTrustedRoots(section, offlineRoot, hostEnvironment, clientName, options, logger);
        ApplyProxyConfiguration(section, clientName, options, logger);
    }

    private static void ApplyTrustedRoots(
        IConfigurationSection section,
        string? offlineRoot,
        IHostEnvironment? hostEnvironment,
        string clientName,
        SourceHttpClientOptions options,
        ILogger? logger)
    {
        var trustedRootSection = section.GetSection(TrustedRootPathsKey);
        if (!trustedRootSection.Exists())
        {
            return;
        }

        var paths = trustedRootSection.Get<string[]?>();
        if (paths is null || paths.Length == 0)
        {
            return;
        }

        foreach (var rawPath in paths)
        {
            if (string.IsNullOrWhiteSpace(rawPath))
            {
                continue;
            }

            var resolvedPath = ResolvePath(rawPath, offlineRoot, hostEnvironment);
            if (!File.Exists(resolvedPath))
            {
                var message = string.Format(
                    CultureInfo.InvariantCulture,
                    "Trusted root certificate '{0}' resolved to '{1}' but was not found.",
                    rawPath,
                    resolvedPath);
                throw new FileNotFoundException(message, resolvedPath);
            }

            foreach (var certificate in LoadCertificates(resolvedPath))
            {
                try
                {
                    AddTrustedCertificate(options, certificate);
                    logger?.LogInformation(
                        "Source HTTP client '{ClientName}' loaded trusted root certificate '{Thumbprint}' from '{Path}'.",
                        clientName,
                        certificate.Thumbprint,
                        resolvedPath);
                }
                finally
                {
                    certificate.Dispose();
                }
            }
        }
    }

    private static void ApplyProxyConfiguration(
        IConfigurationSection section,
        string clientName,
        SourceHttpClientOptions options,
        ILogger? logger)
    {
        var proxySection = section.GetSection(ProxySection);
        if (!proxySection.Exists())
        {
            return;
        }

        var address = proxySection.GetValue<string?>(ProxyAddressKey);
        if (!string.IsNullOrWhiteSpace(address))
        {
            if (Uri.TryCreate(address, UriKind.Absolute, out var uri))
            {
                options.ProxyAddress = uri;
            }
            else
            {
                logger?.LogWarning(
                    "Source HTTP client '{ClientName}' has invalid proxy address '{ProxyAddress}'.",
                    clientName,
                    address);
            }
        }

        var bypassOnLocal = proxySection.GetValue<bool?>(ProxyBypassOnLocalKey);
        if (bypassOnLocal.HasValue)
        {
            options.ProxyBypassOnLocal = bypassOnLocal.Value;
        }

        var bypassListSection = proxySection.GetSection(ProxyBypassListKey);
        if (bypassListSection.Exists())
        {
            var entries = bypassListSection.Get<string[]?>();
            options.ProxyBypassList.Clear();
            if (entries is not null)
            {
                foreach (var entry in entries)
                {
                    if (!string.IsNullOrWhiteSpace(entry))
                    {
                        options.ProxyBypassList.Add(entry.Trim());
                    }
                }
            }
        }

        var useDefaultCredentials = proxySection.GetValue<bool?>(ProxyUseDefaultCredentialsKey);
        if (useDefaultCredentials.HasValue)
        {
            options.ProxyUseDefaultCredentials = useDefaultCredentials.Value;
        }

        var username = proxySection.GetValue<string?>(ProxyUsernameKey);
        if (!string.IsNullOrWhiteSpace(username))
        {
            options.ProxyUsername = username.Trim();
        }

        var password = proxySection.GetValue<string?>(ProxyPasswordKey);
        if (!string.IsNullOrWhiteSpace(password))
        {
            options.ProxyPassword = password;
        }
    }

    private static string ResolvePath(string path, string? offlineRoot, IHostEnvironment? hostEnvironment)
    {
        if (Path.IsPathRooted(path))
        {
            return path;
        }

        if (!string.IsNullOrWhiteSpace(offlineRoot))
        {
            return Path.GetFullPath(Path.Combine(offlineRoot!, path));
        }

        var baseDirectory = hostEnvironment?.ContentRootPath ?? AppContext.BaseDirectory;
        return Path.GetFullPath(Path.Combine(baseDirectory, path));
    }

    private static IEnumerable<X509Certificate2> LoadCertificates(string path)
    {
        var certificates = new List<X509Certificate2>();
        var extension = Path.GetExtension(path);

        if (extension.Equals(".pem", StringComparison.OrdinalIgnoreCase) || extension.Equals(".crt", StringComparison.OrdinalIgnoreCase))
        {
            var collection = new X509Certificate2Collection();
            try
            {
                collection.ImportFromPemFile(path);
            }
            catch (CryptographicException)
            {
                collection.Clear();
            }

            if (collection.Count > 0)
            {
                foreach (var certificate in collection)
                {
                    certificates.Add(certificate.CopyWithPrivateKeyIfAvailable());
                }
            }
            else
            {
                certificates.Add(X509Certificate2.CreateFromPemFile(path));
            }
        }
        else
        {
            // Use X509CertificateLoader to load certificates from PKCS#12 files (.pfx, .p12, etc.)
            var certificate = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadPkcs12(
                File.ReadAllBytes(path), 
                password: null);
            certificates.Add(certificate);
        }

        return certificates;
    }

    private static void AddTrustedCertificate(SourceHttpClientOptions options, X509Certificate2 certificate)
    {
        if (certificate is null)
        {
            return;
        }

        if (options.TrustedRootCertificates.Any(existing =>
                string.Equals(existing.Thumbprint, certificate.Thumbprint, StringComparison.OrdinalIgnoreCase)))
        {
            return;
        }

        options.TrustedRootCertificates.Add(certificate);
    }

    // Helper extension method to copy certificate (preserves private key if present)
    private static X509Certificate2 CopyWithPrivateKeyIfAvailable(this X509Certificate2 certificate)
    {
        // In .NET 9+, use X509CertificateLoader instead of obsolete constructors
        if (certificate.HasPrivateKey)
        {
            // Export with private key and re-import using X509CertificateLoader
            var exported = certificate.Export(X509ContentType.Pkcs12);
            return X509CertificateLoader.LoadPkcs12(exported, password: null);
        }
        else
        {
            // For certificates without private keys, load from raw data
            return X509CertificateLoader.LoadCertificate(certificate.RawData);
        }
    }
}
