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

package logging

import (
	"context"
	"fmt"
	"os"
	"path"
	"sync"
	"time"

	"github.com/hashicorp/terraform-plugin-go/tfprotov5"
	"github.com/hashicorp/terraform-plugin-go/tfprotov6"
)

const (
	// fileExtEmpty is the file extension for empty data.
	// Empty data may be expected, depending on the RPC.
	fileExtEmpty = "empty"

	// fileExtJson is the file extension for JSON data.
	fileExtJson = "json"

	// fileExtMsgpack is the file extension for MessagePack data.
	fileExtMsgpack = "msgpack"
)

var protocolDataSkippedLog sync.Once

// ProtocolData emits raw protocol data to a file, if given a directory.
//
// The directory must exist and be writable, prior to invoking this function.
//
// File names are in the format: {TIME}_{RPC}_{MESSAGE}_{FIELD}.{EXT}
func ProtocolData(ctx context.Context, dataDir string, rpc string, message string, field string, data interface{}) {
	if dataDir == "" {
		// Write a log, only once, that explains how to enable this functionality.
		protocolDataSkippedLog.Do(func() {
			ProtocolTrace(ctx, "Skipping protocol data file writing because no data directory is set. "+
				fmt.Sprintf("Use the %s environment variable to enable this functionality.", EnvTfLogSdkProtoDataDir))
		})

		return
	}

	var fileContents []byte
	var fileExtension string

	switch data := data.(type) {
	case *tfprotov5.DynamicValue:
		fileExtension, fileContents = protocolDataDynamicValue5(ctx, data)
	case *tfprotov6.DynamicValue:
		fileExtension, fileContents = protocolDataDynamicValue6(ctx, data)
	default:
		ProtocolError(ctx, fmt.Sprintf("Skipping unknown protocol data type: %T", data))
		return
	}

	writeProtocolFile(ctx, dataDir, rpc, message, field, fileExtension, fileContents)
}

// ProtocolPrivateData emits raw protocol private data to a file, if given a
// directory. This data is "private" in the sense that it is provider-owned,
// rather than something managed by Terraform.
//
// The directory must exist and be writable, prior to invoking this function.
//
// File names are in the format: {TIME}_{RPC}_{MESSAGE}_{FIELD}(.empty)
func ProtocolPrivateData(ctx context.Context, dataDir string, rpc string, message string, field string, data []byte) {
	if dataDir == "" {
		// Write a log, only once, that explains how to enable this functionality.
		protocolDataSkippedLog.Do(func() {
			ProtocolTrace(ctx, "Skipping protocol data file writing because no data directory is set. "+
				fmt.Sprintf("Use the %s environment variable to enable this functionality.", EnvTfLogSdkProtoDataDir))
		})

		return
	}

	var fileExtension string

	if len(data) == 0 {
		fileExtension = fileExtEmpty
	}

	writeProtocolFile(ctx, dataDir, rpc, message, field, fileExtension, data)
}

func protocolDataDynamicValue5(_ context.Context, value *tfprotov5.DynamicValue) (string, []byte) {
	if value == nil {
		return fileExtEmpty, nil
	}

	// (tfprotov5.DynamicValue).Unmarshal() prefers JSON first, so prefer to
	// output JSON if found.
	if len(value.JSON) > 0 {
		return fileExtJson, value.JSON
	}

	if len(value.MsgPack) > 0 {
		return fileExtMsgpack, value.MsgPack
	}

	return fileExtEmpty, nil
}

func protocolDataDynamicValue6(_ context.Context, value *tfprotov6.DynamicValue) (string, []byte) {
	if value == nil {
		return fileExtEmpty, nil
	}

	// (tfprotov6.DynamicValue).Unmarshal() prefers JSON first, so prefer to
	// output JSON if found.
	if len(value.JSON) > 0 {
		return fileExtJson, value.JSON
	}

	if len(value.MsgPack) > 0 {
		return fileExtMsgpack, value.MsgPack
	}

	return fileExtEmpty, nil
}

func writeProtocolFile(ctx context.Context, dataDir string, rpc string, message string, field string, fileExtension string, fileContents []byte) {
	fileName := fmt.Sprintf("%d_%s_%s_%s", time.Now().UnixMilli(), rpc, message, field)

	if fileExtension != "" {
		fileName += "." + fileExtension
	}

	filePath := path.Join(dataDir, fileName)
	ctx = ProtocolSetField(ctx, KeyProtocolDataFile, filePath)

	ProtocolTrace(ctx, "Writing protocol data file")

	err := os.WriteFile(filePath, fileContents, 0644)

	if err != nil {
		ProtocolError(ctx, "Unable to write protocol data file", map[string]any{KeyError: err.Error()})
		return
	}

	ProtocolTrace(ctx, "Wrote protocol data file")
}
