// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package auth

import (
	"bytes"
	"context"
	"crypto/x509"
	"encoding/base64"
	"errors"
	"fmt"
	"os"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/opentofu/opentofu/internal/httpclient"
	"github.com/opentofu/opentofu/internal/tfdiags"
	"golang.org/x/crypto/pkcs12"
)

type ClientCertificateAuthConfig struct {
	ClientCertificate         string
	ClientCertificatePassword string
	ClientCertificatePath     string
}

type clientCertAuth struct{}

var _ AuthMethod = &clientCertAuth{}

func (cred *clientCertAuth) Name() string {
	return "Client Certificate Auth"
}

func (cred *clientCertAuth) Construct(ctx context.Context, config *Config) (azcore.TokenCredential, error) {
	client := httpclient.New(ctx)

	clientCertificate, err := consolidateCertificate(config.ClientCertificate, config.ClientCertificatePath)
	if err != nil {
		// This should never happen; this is checked in the Validate function
		return nil, err
	}

	privateKey, certificate, err := pkcs12.Decode(
		clientCertificate,
		config.ClientCertificatePassword,
	)
	if err != nil {
		return nil, err
	}

	clientId, err := consolidateClientId(config)
	if err != nil {
		// This should never happen; this is checked in the Validate function
		return nil, err
	}

	return azidentity.NewClientCertificateCredential(
		config.TenantID,
		clientId,
		[]*x509.Certificate{certificate},
		privateKey,
		&azidentity.ClientCertificateCredentialOptions{
			ClientOptions: clientOptions(client, config.CloudConfig),
		},
	)
}

func (cred *clientCertAuth) Validate(_ context.Context, config *Config) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	if config.TenantID == "" {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid Azure Client Certificate Auth",
			"Tenant ID is missing.",
		))
	}

	_, err := consolidateClientId(config)
	if err != nil {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid Azure Client Certificate Auth",
			fmt.Sprintf("The Client ID is misconfigured: %s.", tfdiags.FormatError(err)),
		))
	}
	clientCertificate, err := consolidateCertificate(config.ClientCertificate, config.ClientCertificatePath)
	if err != nil {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid Azure Client Certificate Auth",
			fmt.Sprintf("The Client Certificate is misconfigured: %s.", tfdiags.FormatError(err)),
		))
	} else {
		_, _, err := pkcs12.Decode(
			clientCertificate,
			config.ClientCertificatePassword,
		)
		if err != nil {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Invalid Azure Client Certificate Auth",
				fmt.Sprintf("The Client Certificate is invalid: %s.", tfdiags.FormatError(err)),
			))
		}
	}
	return diags
}

func (cred *clientCertAuth) AugmentConfig(_ context.Context, config *Config) error {
	return checkNamesForAccessKeyCredentials(config.StorageAddresses)
}

func consolidateCertificate(base64EncodedCertificate, certificateFilename string) ([]byte, error) {
	var certBytes []byte
	var fileBytes []byte

	if len(base64EncodedCertificate) > 0 {
		var err error
		certBytes, err = base64.StdEncoding.DecodeString(base64EncodedCertificate)
		if err != nil {
			return nil, fmt.Errorf("error decoding client certificate: %w", err)
		}
	}
	if len(certificateFilename) > 0 {
		var err error
		fileBytes, err = os.ReadFile(certificateFilename)
		if err != nil {
			return nil, fmt.Errorf("error reading client certificate file: %w", err)
		}
	}

	hasCert := len(certBytes) > 0
	hasFile := len(fileBytes) > 0

	if !hasCert && !hasFile {
		return nil, errors.New("missing certificate, client certificate is required")
	}

	if !hasCert {
		return fileBytes, nil
	}

	if !hasFile {
		return certBytes, nil
	}

	if !bytes.Equal(certBytes, fileBytes) {
		return nil, errors.New("client certificate provided directly and through file do not match; either make them the same value or only provide one")
	}
	return fileBytes, nil
}
