/*
 * MinIO .NET Library for Amazon S3 Compatible Cloud Storage,
 * (C) 2022 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Web;
using Minio.DataModel;
using Minio.Exceptions;
using Minio.Helper;
#if (NET472_OR_GREATER || NET6_0_OR_GREATER)
using System.Security.Authentication;
#else
using System.Net;
#endif

/*
 * Certificate Identity Credential provider.
 * This is a MinIO Extension to AssumeRole STS APIs on
 * AWS, purely based on client certificates mTLS authentication.
 */

namespace Minio.Credentials;

public class CertificateIdentityProvider : IClientProvider
{
    private readonly int defaultDurationInSeconds = 3600;

    public CertificateIdentityProvider()
    {
        DurationInSeconds = defaultDurationInSeconds;
    }

    internal string StsEndpoint { get; set; }
    internal int DurationInSeconds { get; set; }
    internal X509Certificate2 ClientCertificate { get; set; }
    internal Uri PostEndpoint { get; set; }
    internal HttpClient HttpClient { get; set; }
    internal AccessCredentials Credentials { get; set; }

    public AccessCredentials GetCredentials()
    {
        return GetCredentialsAsync().AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
    }

    public async ValueTask<AccessCredentials> GetCredentialsAsync()
    {
        if (Credentials?.AreExpired() == false)
            return Credentials;

        if (HttpClient is null)
            throw new InvalidOperationException(nameof(HttpClient) + " cannot be null or empty");

        if (ClientCertificate is null)
            throw new InvalidOperationException(nameof(ClientCertificate) + " cannot be null or empty");

        using var response = await HttpClient.PostAsync(PostEndpoint, null).ConfigureAwait(false);

        var certResponse = new CertificateResponse();
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content).AsMemory().ToArray());
            certResponse = Utils.DeserializeXml<CertificateResponse>(stream);
        }

        if (Credentials is null && certResponse?.Cr is not null)
            Credentials = certResponse.Cr.Credentials;

        return Credentials;
    }

    public CertificateIdentityProvider WithStsEndpoint(string stsEndpoint)
    {
        if (string.IsNullOrEmpty(stsEndpoint))
            throw new InvalidEndpointException("Missing mandatory argument: stsEndpoint");
        if (!stsEndpoint.StartsWith("https", StringComparison.OrdinalIgnoreCase))
            throw new InvalidEndpointException($"stsEndpoint {stsEndpoint} is invalid." + " The scheme must be https");

        StsEndpoint = stsEndpoint;
        return this;
    }

    public CertificateIdentityProvider WithHttpClient(HttpClient httpClient = null)
    {
        HttpClient = httpClient;
        return this;
    }

    public CertificateIdentityProvider WithCertificate(X509Certificate2 cert = null)
    {
        ClientCertificate = cert;
        return this;
    }

    public CertificateIdentityProvider Build()
    {
        if (string.IsNullOrEmpty(DurationInSeconds.ToString(CultureInfo.InvariantCulture)))
            DurationInSeconds = defaultDurationInSeconds;

        var builder = new UriBuilder(StsEndpoint);
        var query = HttpUtility.ParseQueryString(builder.Query);
        query["Action"] = "AssumeRoleWithCertificate";
        query["Version"] = "2011-06-15";
        query["DurationInSeconds"] = DurationInSeconds.ToString(CultureInfo.InvariantCulture);
        builder.Query = query.ToString();
        PostEndpoint = builder.Uri;

        var handler = new HttpClientHandler { ClientCertificateOptions = ClientCertificateOption.Manual };
#if (NET472_OR_GREATER || NET6_0_OR_GREATER)
        handler.SslProtocols = SslProtocols.Tls12;
#else
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;
#endif
        _ = handler.ClientCertificates.Add(ClientCertificate);
        HttpClient ??= new HttpClient(handler) { BaseAddress = new Uri(StsEndpoint) };

        Credentials = GetCredentials();
        return this;
    }
}
