// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package oci

import (
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"

	"github.com/hashicorp/terraform/version"
	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/common/auth"
	"github.com/oracle/oci-go-sdk/v65/objectstorage"
	"github.com/zclconf/go-cty/cty"
)

var ApiKeyConfigAttributes = [5]string{UserOcidAttrName, FingerprintAttrName, PrivateKeyAttrName, PrivateKeyPathAttrName, PrivateKeyPasswordAttrName}

type ociAuthConfigProvider struct {
	authType           string
	configFileProfile  string
	region             string
	tenancyOcid        string
	userOcid           string
	fingerprint        string
	privateKey         string
	privateKeyPath     string
	privateKeyPassword string
}

func newOciAuthConfigProvider(obj cty.Value) ociAuthConfigProvider {
	p := ociAuthConfigProvider{}

	if authVal, ok := getBackendAttrWithDefault(obj, AuthAttrName, AuthAPIKeySetting); ok {
		p.authType = authVal.AsString()
	}

	if configFileProfileVal, ok := getBackendAttr(obj, ConfigFileProfileAttrName); ok {
		p.configFileProfile = configFileProfileVal.AsString()
	}
	if regionVal, ok := getBackendAttr(obj, RegionAttrName); ok {
		p.region = regionVal.AsString()
	}

	if tenancyOcidVal, ok := getBackendAttr(obj, TenancyOcidAttrName); ok {
		p.tenancyOcid = tenancyOcidVal.AsString()
	}

	if userOcidVal, ok := getBackendAttr(obj, UserOcidAttrName); ok {
		p.userOcid = userOcidVal.AsString()
	}

	if fingerprintVal, ok := getBackendAttr(obj, FingerprintAttrName); ok {
		p.fingerprint = fingerprintVal.AsString()
	}

	if privateKeyVal, ok := getBackendAttr(obj, PrivateKeyAttrName); ok {
		p.privateKey = privateKeyVal.AsString()
	}

	if privateKeyPathVal, ok := getBackendAttr(obj, PrivateKeyPathAttrName); ok {
		p.privateKeyPath = privateKeyPathVal.AsString()
	}

	if privateKeyPasswordVal, ok := getBackendAttr(obj, PrivateKeyPasswordAttrName); ok {
		p.privateKeyPassword = privateKeyPasswordVal.AsString()
	}

	return p
}
func (p ociAuthConfigProvider) AuthType() (common.AuthConfig, error) {
	return common.AuthConfig{
			AuthType:         common.UnknownAuthenticationType,
			IsFromConfigFile: false,
			OboToken:         nil,
		},
		fmt.Errorf("unsupported, keep the interface")
}

func (p ociAuthConfigProvider) TenancyOCID() (string, error) {
	if p.tenancyOcid != "" {
		return p.tenancyOcid, nil
	}
	return "", fmt.Errorf("can not get %s from Terraform backend configuration", TenancyOcidAttrName)
}

func (p ociAuthConfigProvider) UserOCID() (string, error) {
	if p.userOcid != "" {
		return p.userOcid, nil
	}
	return "", fmt.Errorf("can not get %s from Terraform backend configuration", UserOcidAttrName)
}

func (p ociAuthConfigProvider) KeyFingerprint() (string, error) {
	if p.fingerprint != "" {
		return p.fingerprint, nil
	}
	return "", fmt.Errorf("can not get %s from Terraform backend configuration", FingerprintAttrName)
}

func (p ociAuthConfigProvider) Region() (string, error) {
	if p.region != "" {
		return p.region, nil
	}
	return "", fmt.Errorf("can not get %s from Terraform backend configuration", RegionAttrName)
}
func (p ociAuthConfigProvider) KeyID() (string, error) {
	tenancy, err := p.TenancyOCID()
	if err != nil {
		return "", err
	}

	user, err := p.UserOCID()
	if err != nil {
		return "", err
	}

	fingerprint, err := p.KeyFingerprint()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil
}

func (p ociAuthConfigProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {

	if p.privateKey != "" {
		keyData := strings.ReplaceAll(p.privateKey, "\\n", "\n") // Ensure \n is replaced by actual newlines
		return common.PrivateKeyFromBytesWithPassword([]byte(keyData), []byte(p.privateKeyPassword))
	}

	if p.privateKeyPath != "" {
		resolvedPath := expandPath(p.privateKeyPath)
		pemFileContent, readFileErr := os.ReadFile(resolvedPath)
		if readFileErr != nil {
			return nil, fmt.Errorf("can not read private key from: '%s', Error: %q", p.privateKeyPath, readFileErr)
		}
		return common.PrivateKeyFromBytesWithPassword(pemFileContent, []byte(p.privateKeyPassword))
	}

	return nil, fmt.Errorf("can not get private_key or private_key_path from Terraform configuration")
}

func (p ociAuthConfigProvider) getConfigProviders() ([]common.ConfigurationProvider, error) {
	var configProviders []common.ConfigurationProvider
	logger := logWithOperation("AuthConfigProvider")
	logger.Debug(fmt.Sprintf("Using %s authentication", p.authType))
	switch strings.ToLower(p.authType) {
	case strings.ToLower(AuthAPIKeySetting):
		// No additional config providers needed
	case strings.ToLower(AuthInstancePrincipalSetting):

		logger.Info("Attempting to authenticate using instance principal credentials")
		if p.region == "" {
			return nil, fmt.Errorf("unable to determine region from Terraform backend configuration while using Instance Principal")
		}

		// Used to modify InstancePrincipal auth clients so that `accept_local_certs` is honored for auth clients as well
		instancePrincipalAuthClientModifier := func(client common.HTTPRequestDispatcher) (common.HTTPRequestDispatcher, error) {
			if acceptLocalCerts := getEnvSettingWithBlankDefault(AcceptLocalCerts); acceptLocalCerts != "" {
				if value, err := strconv.ParseBool(acceptLocalCerts); err == nil {
					modifiedClient := buildHttpClient()
					modifiedClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = value
					return modifiedClient, nil
				}
			}
			return client, nil
		}

		cfg, err := auth.InstancePrincipalConfigurationForRegionWithCustomClient(common.StringToRegion(p.region), instancePrincipalAuthClientModifier)
		if err != nil {
			return nil, err
		}
		logger.Debug(" Configuration provided by: %s", cfg)

		configProviders = append(configProviders, cfg)
	case strings.ToLower(AuthInstancePrincipalWithCertsSetting):
		logger.Info("Attempting to authenticate using instance principal with certificates")

		if p.region == "" {
			return nil, fmt.Errorf("unable to determine region from Terraform backend configuration while using Instance Principal with certificates")
		}

		defaultCertsDir, err := os.Getwd()
		if err != nil {
			return nil, fmt.Errorf("can not get working directory for current os platform")
		}

		certsDir := filepath.Clean(getEnvSettingWithDefault("test_certificates_location", defaultCertsDir))
		leafCertificateBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "ip_cert.pem"))
		if err != nil {
			return nil, fmt.Errorf("can not read leaf certificate from %s", filepath.Join(certsDir, "ip_cert.pem"))
		}

		leafPrivateKeyBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "ip_key.pem"))
		if err != nil {
			return nil, fmt.Errorf("can not read leaf private key from %s", filepath.Join(certsDir, "ip_key.pem"))
		}

		leafPassphraseBytes := []byte{}
		if _, err := os.Stat(certsDir + "/leaf_passphrase"); !os.IsNotExist(err) {
			leafPassphraseBytes, err = getCertificateFileBytes(filepath.Join(certsDir + "leaf_passphrase"))
			if err != nil {
				return nil, fmt.Errorf("can not read leafPassphraseBytes from %s", filepath.Join(certsDir+"leaf_passphrase"))
			}
		}

		intermediateCertificateBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "intermediate.pem"))
		if err != nil {
			return nil, fmt.Errorf("can not read intermediate certificate from %s", filepath.Join(certsDir, "intermediate.pem"))
		}

		intermediateCertificatesBytes := [][]byte{
			intermediateCertificateBytes,
		}

		cfg, err := auth.InstancePrincipalConfigurationWithCerts(common.StringToRegion(p.region), leafCertificateBytes, leafPassphraseBytes, leafPrivateKeyBytes, intermediateCertificatesBytes)
		if err != nil {
			return nil, err
		}
		logger.Debug(" Configuration provided by: %s", cfg)

		configProviders = append(configProviders, cfg)

	case strings.ToLower(AuthSecurityToken):
		logger.Info("Attempting to authenticate using security token")
		if p.region == "" {
			return nil, fmt.Errorf("can not get %s from Terraform configuration (SecurityToken)", RegionAttrName)
		}
		// if region is part of the provider block make sure it is part of the final configuration too, and overwrites the region in the profile. +
		regionProvider := common.NewRawConfigurationProvider("", "", p.region, "", "", nil)
		configProviders = append(configProviders, regionProvider)

		if p.configFileProfile == "" {
			return nil, fmt.Errorf("missing profile in provider block %v", ConfigFileProfileAttrName)
		}

		defaultPath := path.Join(getHomeFolder(), DefaultConfigDirName, DefaultConfigFileName)
		if err := checkProfile(p.configFileProfile, defaultPath); err != nil {
			return nil, err
		}
		securityTokenBasedAuthConfigProvider, err := common.ConfigurationProviderForSessionTokenWithProfile(defaultPath, p.configFileProfile, p.privateKeyPassword)
		if err != nil {
			return nil, fmt.Errorf("could not create security token based auth config provider %v", err)
		}
		configProviders = append(configProviders, securityTokenBasedAuthConfigProvider)
	case strings.ToLower(ResourcePrincipal):
		logger.Info("Attempting to authenticate using resource principal credentials")
		var err error
		var resourcePrincipalAuthConfigProvider auth.ConfigurationProviderWithClaimAccess

		if p.region == "" {
			logger.Debug("did not get %s from Terraform configuration (ResourcePrincipal), falling back to environment variable", RegionAttrName)
			resourcePrincipalAuthConfigProvider, err = auth.ResourcePrincipalConfigurationProvider()
		} else {
			resourcePrincipalAuthConfigProvider, err = auth.ResourcePrincipalConfigurationProviderForRegion(common.StringToRegion(p.region))
		}
		if err != nil {
			return nil, err
		}
		configProviders = append(configProviders, resourcePrincipalAuthConfigProvider)
	case strings.ToLower(AuthOKEWorkloadIdentity):
		logger.Info("Attempting to authenticate using OKE workload identity")
		okeWorkloadIdentityConfigProvider, err := auth.OkeWorkloadIdentityConfigurationProvider()
		if err != nil {
			return nil, fmt.Errorf("can not get oke workload indentity based auth config provider %v", err)
		}
		configProviders = append(configProviders, okeWorkloadIdentityConfigProvider)
	default:
		return nil, fmt.Errorf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity)
	}

	return configProviders, nil
}
func (p ociAuthConfigProvider) getSdkConfigProvider() (common.ConfigurationProvider, error) {

	configProviders, err := p.getConfigProviders()
	if err != nil {
		return nil, err
	}

	configProviders = append(configProviders, p)
	//In GoSDK, the first step is to check if AuthType exists,
	//for composite provider, we only check the first provider in the list for the AuthType.
	//Then SDK will based on the AuthType to Create the actual provider if it's a valid value.
	//If not, then SDK will base on the order in the composite provider list to check for necessary info (tenancyid, userID, fingerprint, region, keyID).
	if p.configFileProfile == "" {
		configProviders = append(configProviders, common.DefaultConfigProvider())
	} else {
		defaultPath := path.Join(getHomeFolder(), DefaultConfigDirName, DefaultConfigFileName)
		err := checkProfile(p.configFileProfile, defaultPath)
		if err != nil {
			return nil, err
		}
		configProviders = append(configProviders, common.CustomProfileConfigProvider(defaultPath, p.configFileProfile))
	}
	sdkConfigProvider, err := common.ComposingConfigurationProvider(configProviders)
	if err != nil {
		return nil, err
	}

	return sdkConfigProvider, nil
}
func buildHttpClient() (httpClient *http.Client) {
	httpClient = &http.Client{
		Timeout: getDurationFromEnvVar(HTTPRequestTimeOut, DefaultRequestTimeout),
		Transport: &http.Transport{
			DialContext: (&net.Dialer{
				Timeout: getDurationFromEnvVar(DialContextConnectionTimeout, DefaultConnectionTimeout),
			}).DialContext,
			TLSHandshakeTimeout: getDurationFromEnvVar(TLSHandshakeTimeout, DefaultTLSHandshakeTimeout),
			TLSClientConfig:     &tls.Config{MinVersion: tls.VersionTLS12},
			Proxy:               http.ProxyFromEnvironment,
		},
	}
	return
}

func getCertificateFileBytes(certificateFileFullPath string) (pemRaw []byte, err error) {
	absFile, err := filepath.Abs(certificateFileFullPath)
	if err != nil {
		return nil, fmt.Errorf("can't form absolute path of %s: %v", certificateFileFullPath, err)
	}

	if pemRaw, err = os.ReadFile(absFile); err != nil {
		return nil, fmt.Errorf("can't read %s: %v", certificateFileFullPath, err)
	}
	return
}
func UserAgentFromEnv() string {

	userAgentFromEnv := getEnvSettingWithBlankDefault(UserAgentSDKNameEnv)
	if userAgentFromEnv == "" {
		userAgentFromEnv = getEnvSettingWithBlankDefault(UserAgentTerraformNameEnv)
	}
	if userAgentFromEnv == "" {
		userAgentFromEnv = DefaultUserAgentBackendName
	}

	return userAgentFromEnv
}

// OboTokenProvider interface that wraps information about auth tokens so the sdk client can make calls
// on behalf of a different authorized user
type OboTokenProvider interface {
	OboToken() (string, error)
}

// EmptyOboTokenProvider always provides an empty obo token
type emptyOboTokenProvider struct{}

// OboToken provides the obo token
func (provider emptyOboTokenProvider) OboToken() (string, error) {
	return "", nil
}

type oboTokenProviderFromEnv struct{}

func (p oboTokenProviderFromEnv) OboToken() (string, error) {
	// priority token from path than token from environment
	if tokenPath := getEnvSettingWithBlankDefault(OboTokenPath); tokenPath != "" {
		tokenByte, err := os.ReadFile(tokenPath)
		if err != nil {
			return "", err
		}
		return string(tokenByte), nil
	}
	return getEnvSettingWithBlankDefault(OboTokenAttrName), nil
}
func buildConfigureClient(configProvider common.ConfigurationProvider, httpClient *http.Client) (*objectstorage.ObjectStorageClient, error) {

	userAgentName := UserAgentFromEnv()
	userAgent := fmt.Sprintf(UserAgentFormatter, common.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH, version.String(), userAgentName)

	useOboToken, err := strconv.ParseBool(getEnvSettingWithDefault("use_obo_token", "false"))
	if err != nil {
		return nil, err
	}
	setSDKLogger()
	client, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(configProvider)
	if err != nil {
		return nil, err
	}
	if hostOverride := getClientHostOverride(); hostOverride != "" {
		client.Host = hostOverride
	}
	requestSigner := common.DefaultRequestSigner(configProvider)
	var oboTokenProvider OboTokenProvider
	oboTokenProvider = emptyOboTokenProvider{}
	if useOboToken {
		// Add Obo token to the default list and Update the signer
		httpHeadersToSign := append(common.DefaultGenericHeaders(), RequestHeaderOpcOboToken)
		requestSigner = common.RequestSigner(configProvider, httpHeadersToSign, common.DefaultBodyHeaders())
		oboTokenProvider = oboTokenProviderFromEnv{}
	}

	client.HTTPClient = httpClient
	client.UserAgent = userAgent
	client.Signer = requestSigner
	client.Interceptor = func(r *http.Request) error {
		if oboToken, err := oboTokenProvider.OboToken(); err == nil && oboToken != "" {
			r.Header.Set(RequestHeaderOpcOboToken, oboToken)
		}
		return nil
	}

	domainNameOverride := getEnvSettingWithBlankDefault(DomainNameOverrideEnv)

	if domainNameOverride != "" {
		hasCorrectDomainName := getEnvSettingWithBlankDefault(HasCorrectDomainNameEnv)
		re := regexp.MustCompile(`(.*?)[-\w]+\.\w+$`) // (capture: preamble) match: d0main-name . tld end-of-string
		if hasCorrectDomainName == "" || !strings.HasSuffix(client.Host, hasCorrectDomainName) {
			client.Host = re.ReplaceAllString(client.Host, "${1}"+domainNameOverride) // non-match conveniently returns original string
		}
	}

	customCertLoc := getEnvSettingWithBlankDefault(CustomCertLocationEnv)

	if customCertLoc != "" {
		cert, err := os.ReadFile(customCertLoc)
		if err != nil {
			return &client, err
		}
		pool := x509.NewCertPool()
		if ok := pool.AppendCertsFromPEM(cert); !ok {
			return nil, fmt.Errorf("failed to append custom cert to the pool")
		}
		// install the certificates in the client
		httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = pool
	}

	if acceptLocalCerts := getEnvSettingWithBlankDefault(AcceptLocalCerts); acceptLocalCerts != "" {
		if boolVal, err := strconv.ParseBool(acceptLocalCerts); err == nil {
			httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = boolVal
		}
	}

	return &client, nil
}
func getClientHostOverride() string {
	// Get the host URL override for clients

	clientHostOverridesString := getEnvSettingWithBlankDefault(ClientHostOverridesEnv)
	if clientHostOverridesString == "" {
		return ""
	}

	clientHostFlags := strings.Split(clientHostOverridesString, ColonDelimiter)
	for _, item := range clientHostFlags {
		clientNameHost := strings.Split(item, EqualToOperatorDelimiter)
		if clientNameHost == nil || len(clientNameHost) != 2 {
			continue
		}
		if clientNameHost[0] == ObjectStorageClientName {
			return clientNameHost[1]
		}
	}
	return ""
}
