//go:build !plan9 && !solaris && !js

package oracleobjectstorage

import (
	"context"
	"crypto/rsa"
	"errors"
	"net/http"
	"os"
	"path"
	"strings"

	"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/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/fserrors"
	"github.com/rclone/rclone/fs/fshttp"
)

func expandPath(filepath string) (expandedPath string) {
	if filepath == "" {
		return filepath
	}
	cleanedPath := path.Clean(filepath)
	expandedPath = cleanedPath
	if strings.HasPrefix(cleanedPath, "~") {
		rest := cleanedPath[2:]
		home, err := os.UserHomeDir()
		if err != nil {
			return expandedPath
		}
		expandedPath = path.Join(home, rest)
	}
	return
}

func getConfigurationProvider(opt *Options) (common.ConfigurationProvider, error) {
	switch opt.Provider {
	case instancePrincipal:
		return auth.InstancePrincipalConfigurationProvider()
	case userPrincipal:
		expandConfigFilePath := expandPath(opt.ConfigFile)
		if expandConfigFilePath != "" && !fileExists(expandConfigFilePath) {
			fs.Errorf(userPrincipal, "oci config file doesn't exist at %v", expandConfigFilePath)
		}
		return common.CustomProfileConfigProvider(expandConfigFilePath, opt.ConfigProfile), nil
	case resourcePrincipal:
		return auth.ResourcePrincipalConfigurationProvider()
	case noAuth:
		fs.Infof("client", "using no auth provider")
		return getNoAuthConfiguration()
	case workloadIdentity:
		return auth.OkeWorkloadIdentityConfigurationProvider()
	default:
	}
	return common.DefaultConfigProvider(), nil
}

func newObjectStorageClient(ctx context.Context, opt *Options) (*objectstorage.ObjectStorageClient, error) {
	p, err := getConfigurationProvider(opt)
	if err != nil {
		return nil, err
	}
	client, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(p)
	if err != nil {
		fs.Errorf(opt.Provider, "failed to create object storage client, %v", err)
		return nil, err
	}
	if opt.Region != "" {
		client.SetRegion(opt.Region)
	}
	if opt.Endpoint != "" {
		client.Host = opt.Endpoint
	}
	modifyClient(ctx, opt, &client.BaseClient)
	return &client, err
}

func fileExists(filePath string) bool {
	if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
		return false
	}
	return true
}

func modifyClient(ctx context.Context, opt *Options, client *common.BaseClient) {
	client.HTTPClient = getHTTPClient(ctx)
	if opt.Provider == noAuth {
		client.Signer = getNoAuthSigner()
	}
}

// getClient makes http client according to the global options
// this has rclone specific options support like dump headers, body etc.
func getHTTPClient(ctx context.Context) *http.Client {
	return fshttp.NewClient(ctx)
}

var retryErrorCodes = []int{
	408, // Request Timeout
	429, // Rate exceeded.
	500, // Get occasional 500 Internal Server Error
	503, // Service Unavailable
	504, // Gateway Time-out
}

func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
	if fserrors.ContextError(ctx, &err) {
		return false, err
	}
	// If this is an ocierr object, try and extract more useful information to determine if we should retry
	if ociError, ok := err.(common.ServiceError); ok {
		// Simple case, check the original embedded error in case it's generically retryable
		if fserrors.ShouldRetry(err) {
			return true, err
		}
		// If it is a timeout then we want to retry that
		if ociError.GetCode() == "RequestTimeout" {
			return true, err
		}
	}
	// Ok, not an oci error, check for generic failure conditions
	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
}

func getNoAuthConfiguration() (common.ConfigurationProvider, error) {
	return &noAuthConfigurator{}, nil
}

func getNoAuthSigner() common.HTTPRequestSigner {
	return &noAuthSigner{}
}

type noAuthConfigurator struct {
}

type noAuthSigner struct {
}

func (n *noAuthSigner) Sign(*http.Request) error {
	return nil
}

func (n *noAuthConfigurator) PrivateRSAKey() (*rsa.PrivateKey, error) {
	return nil, nil
}

func (n *noAuthConfigurator) KeyID() (string, error) {
	return "", nil
}

func (n *noAuthConfigurator) TenancyOCID() (string, error) {
	return "", nil
}

func (n *noAuthConfigurator) UserOCID() (string, error) {
	return "", nil
}

func (n *noAuthConfigurator) KeyFingerprint() (string, error) {
	return "", nil
}

func (n *noAuthConfigurator) Region() (string, error) {
	return "", nil
}

func (n *noAuthConfigurator) AuthType() (common.AuthConfig, error) {
	return common.AuthConfig{
		AuthType:         common.UnknownAuthenticationType,
		IsFromConfigFile: false,
		OboToken:         nil,
	}, nil
}

// Check the interfaces are satisfied
var (
	_ common.ConfigurationProvider = &noAuthConfigurator{}
	_ common.HTTPRequestSigner     = &noAuthSigner{}
)