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

package oci

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/terraform/internal/states/remote"
	"github.com/hashicorp/terraform/internal/states/statemgr"
	"github.com/hashicorp/terraform/internal/tfdiags"
	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/objectstorage"
)

type RemoteClient struct {
	objectStorageClient  *objectstorage.ObjectStorageClient
	namespace            string
	bucketName           string
	path                 string
	lockFilePath         string
	kmsKeyID             string
	SSECustomerKey       string
	SSECustomerKeySHA256 string
	SSECustomerAlgorithm string
}

func (c *RemoteClient) Get() (*remote.Payload, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	logger := logWithOperation("download-state-file").Named(c.path)
	logger.Info("Downloading remote state")
	ctx := context.WithValue(context.Background(), "logger", logger)
	payload, err := c.getObject(ctx)
	if err != nil || len(payload.Data) == 0 {
		return nil, diags.Append(err)
	}
	// md5 hash of whole state
	sum := md5.Sum(payload.Data)
	payload.MD5 = sum[:]
	return payload, diags
}

func (c *RemoteClient) getObject(ctx context.Context) (*remote.Payload, error) {
	logger := ctx.Value("logger").(hclog.Logger)
	headRequest := objectstorage.HeadObjectRequest{
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.path),
		BucketName:    common.String(c.bucketName),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}
	if c.SSECustomerKey != "" && c.SSECustomerKeySHA256 != "" {
		headRequest.OpcSseCustomerKey = common.String(c.SSECustomerKey)
		headRequest.OpcSseCustomerKeySha256 = common.String(c.SSECustomerKeySHA256)
		headRequest.OpcSseCustomerAlgorithm = common.String(c.SSECustomerAlgorithm)
	}
	// Get object from OCI
	headResponse, headErr := c.objectStorageClient.HeadObject(ctx, headRequest)
	if headErr != nil {
		var ociHeadErr common.ServiceError
		if errors.As(headErr, &ociHeadErr) && ociHeadErr.GetHTTPStatusCode() == 404 {
			logger.Debug(" State file '%s' not found. Initializing Terraform state...", c.path)
			return &remote.Payload{}, nil
		} else {
			return nil, fmt.Errorf("failed to access object '%s' in bucket '%s': %w", c.path, c.bucketName, headErr)
		}
	}

	getRequest := objectstorage.GetObjectRequest{
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.path),
		BucketName:    common.String(c.bucketName),
		IfMatch:       headResponse.ETag,
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}
	if c.SSECustomerKey != "" && c.SSECustomerKeySHA256 != "" {
		getRequest.OpcSseCustomerKey = common.String(c.SSECustomerKey)
		getRequest.OpcSseCustomerKeySha256 = common.String(c.SSECustomerKeySHA256)
		getRequest.OpcSseCustomerAlgorithm = common.String(c.SSECustomerAlgorithm)
	}
	// Get object from OCI
	getResponse, err := c.objectStorageClient.GetObject(ctx, getRequest)
	if err != nil {
		var ociErr common.ServiceError
		if errors.As(err, &ociErr) {
			return nil, fmt.Errorf("failed to access object HttpStatusCode: %d\nOpcRequestId: %s\n message: %s\n ErrorCode: %s", ociErr.GetHTTPStatusCode(), ociErr.GetOpcRequestID(), ociErr.GetMessage(), ociErr.GetCode())

		}
		return nil, fmt.Errorf("failed to access object '%s' in bucket '%s': %w", c.path, c.bucketName, err)
	}
	defer getResponse.Content.Close()

	// Read object content
	contentArray, err := io.ReadAll(getResponse.Content)
	if err != nil {
		return nil, fmt.Errorf("unable to read 'content' from response: %w", err)
	}

	// Compute MD5 hash
	md5Hash := getResponse.ContentMd5
	if md5Hash == nil || len(*md5Hash) == 0 {
		md5Hash = getResponse.OpcMultipartMd5
	}
	// Construct payload
	payload := &remote.Payload{
		Data: contentArray,
		MD5:  []byte(*md5Hash),
	}

	// Return an error instead of `nil, nil` if the object is empty
	if len(payload.Data) == 0 {
		return nil, fmt.Errorf("object %q is empty", c.path)
	}

	return payload, nil
}

func (c *RemoteClient) Put(data []byte) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	logger := logWithOperation("upload-state-file").Named(c.path)
	ctx := context.WithValue(context.Background(), "logger", logger)
	dataSize := int64(len(data))
	sum := md5.Sum(data)
	var err error
	if dataSize > DefaultFilePartSize {
		logger.Info("Using Multipart Feature")
		var multipartUploadData = MultipartUploadData{
			client: c,
			Data:   data,
			RequestMetadata: common.RequestMetadata{
				RetryPolicy: getDefaultRetryPolicy(),
			},
		}
		err = multipartUploadData.multiPartUploadImpl(ctx)
		if err != nil && dataSize <= MaxFilePartSize {
			logger.Error(fmt.Sprintf("Multipart upload failed, falling back to single part upload: %v", err))
			err = c.uploadSinglePartObject(ctx, data, sum[:])
		}
	} else {
		err = c.uploadSinglePartObject(ctx, data, sum[:])
	}
	if err != nil {
		return diags.Append(err)
	}

	return diags
}

func (c *RemoteClient) uploadSinglePartObject(ctx context.Context, data, sum []byte) error {
	logger := ctx.Value("logger").(hclog.Logger).Named("singlePartUpload")
	logger.Info("Uploading single part object")
	if len(data) == 0 {
		return fmt.Errorf("uploadSinglePartObject: data is empty")
	}

	contentType := "application/json"

	putRequest := objectstorage.PutObjectRequest{
		ContentType:   common.String(contentType),
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.path),
		BucketName:    common.String(c.bucketName),
		PutObjectBody: io.NopCloser(bytes.NewReader(data)),
		ContentMD5:    common.String(base64.StdEncoding.EncodeToString(sum)),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}

	// Handle encryption settings
	if c.kmsKeyID != "" {
		putRequest.OpcSseKmsKeyId = common.String(c.kmsKeyID)
	} else if c.SSECustomerKey != "" && c.SSECustomerKeySHA256 != "" {
		putRequest.OpcSseCustomerKey = common.String(c.SSECustomerKey)
		putRequest.OpcSseCustomerKeySha256 = common.String(c.SSECustomerKeySHA256)
		putRequest.OpcSseCustomerAlgorithm = common.String(c.SSECustomerAlgorithm)
	}

	logger.Info(fmt.Sprintf("Uploading remote state: %s", c.path))

	putResponse, err := c.objectStorageClient.PutObject(ctx, putRequest)
	if err != nil {
		return fmt.Errorf("failed to upload object: %w", err)
	}

	logger.Info("Uploaded state file response: %+v\n", putResponse)
	return nil
}

func (c *RemoteClient) Delete() tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	return diags.Append(c.DeleteAllObjectVersions())
}
func (c *RemoteClient) DeleteAllObjectVersions() error {
	request := objectstorage.ListObjectVersionsRequest{
		BucketName:    common.String(c.bucketName),
		NamespaceName: common.String(c.namespace),
		Prefix:        common.String(c.path),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}

	response, err := c.objectStorageClient.ListObjectVersions(context.Background(), request)
	if err != nil {
		return err
	}

	request.Page = response.OpcNextPage

	for request.Page != nil {
		request.RequestMetadata.RetryPolicy = getDefaultRetryPolicy()

		listResponse, err := c.objectStorageClient.ListObjectVersions(context.Background(), request)
		if err != nil {
			return err
		}
		response.Items = append(response.Items, listResponse.Items...)
		request.Page = listResponse.OpcNextPage
	}

	var diagErr tfdiags.Diagnostics

	for _, objectVersion := range response.Items {

		deleteObjectVersionRequest := objectstorage.DeleteObjectRequest{
			BucketName:    common.String(c.bucketName),
			NamespaceName: common.String(c.namespace),
			ObjectName:    objectVersion.Name,
			VersionId:     objectVersion.VersionId,
			RequestMetadata: common.RequestMetadata{
				RetryPolicy: getDefaultRetryPolicy(),
			},
		}

		_, err := c.objectStorageClient.DeleteObject(context.Background(), deleteObjectVersionRequest)
		if err != nil {
			diagErr = diagErr.Append(err)
		}
	}
	if diagErr != nil {
		return diagErr.Err()
	}

	return nil
}

func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
	logger := logWithOperation("lock-state-file").Named(c.lockFilePath)
	logger.Info("Locking remote state")
	ctx := context.TODO()
	info.Path = c.path
	infoBytes, err := json.Marshal(info)
	if err != nil {
		return "", err
	}

	putObjReq := objectstorage.PutObjectRequest{
		BucketName:    common.String(c.bucketName),
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.lockFilePath),
		IfNoneMatch:   common.String("*"),
		PutObjectBody: io.NopCloser(bytes.NewReader(infoBytes)),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}

	putResponse, putErr := c.objectStorageClient.PutObject(ctx, putObjReq)
	if putErr != nil {
		lockInfo, _, err := c.getLockInfo(ctx)
		if err != nil {
			putErr = errors.Join(putErr, err)
		}
		return "", &statemgr.LockError{
			Err:  putErr,
			Info: lockInfo,
		}
	}
	logger.Info("state lock response code: %+d\n", putResponse.RawResponse.StatusCode)
	return info.ID, nil

}

// getLockInfo retrieves and parses a lock file from an oci bucket.
func (c *RemoteClient) getLockInfo(ctx context.Context) (*statemgr.LockInfo, string, error) {
	// Attempt to retrieve the lock file from
	getRequest := objectstorage.GetObjectRequest{
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.lockFilePath),
		BucketName:    common.String(c.bucketName),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}

	getResponse, err := c.objectStorageClient.GetObject(ctx, getRequest)
	if err != nil {
		return nil, "", fmt.Errorf("failed to get existing lock file: %w", err)
	}
	lockByteData, err := io.ReadAll(getResponse.Content)
	if err != nil {
		return nil, *getResponse.ETag, fmt.Errorf("failed to read existing lock file content: %w", err)
	}
	lockInfo := &statemgr.LockInfo{}
	if err := json.Unmarshal(lockByteData, lockInfo); err != nil {
		return lockInfo, "", fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
	}
	return lockInfo, *getResponse.ETag, nil
}
func (c *RemoteClient) Unlock(id string) error {
	ctx := context.TODO()
	logger := logWithOperation("unlock-state-file").Named(c.lockFilePath)
	logger.Info("unlocking remote state")
	lockInfo, etag, err := c.getLockInfo(ctx)

	if err != nil {
		return fmt.Errorf("Failed to retrieve lock information from OCI Object Storage: %w", err)
	}
	// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
	if lockInfo.ID != id {
		return &statemgr.LockError{
			Info: lockInfo,
			Err:  fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID),
		}
	}

	deleteRequest := objectstorage.DeleteObjectRequest{
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.lockFilePath),
		BucketName:    common.String(c.bucketName),
		IfMatch:       common.String(etag),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}
	deleteResponse, err := c.objectStorageClient.DeleteObject(ctx, deleteRequest)
	if err != nil {
		return &statemgr.LockError{
			Info: lockInfo,
			Err:  err,
		}
	}
	logger.Info("Unlock response: %v\n", deleteResponse.RawResponse.StatusCode)
	return nil
}
