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

package external

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os/exec"
	"strings"
	"time"

	"github.com/opentofu/opentofu/internal/encryption/keyprovider"
	"github.com/opentofu/opentofu/internal/encryption/method"
)

type command struct {
	keys           *keyprovider.Output
	encryptCommand []string
	decryptCommand []string
}

func (c command) Encrypt(data []byte) ([]byte, error) {
	var key []byte
	if c.keys != nil {
		key = c.keys.EncryptionKey
	}
	input := InputV1{
		Key:     key,
		Payload: data,
	}
	result, err := c.run(c.encryptCommand, input)
	if err != nil {
		return nil, &method.ErrEncryptionFailed{
			Cause: err,
		}
	}
	return result, nil
}

func (c command) Decrypt(data []byte) ([]byte, error) {
	var key []byte
	if c.keys != nil {
		key = c.keys.DecryptionKey
		if len(c.keys.EncryptionKey) > 0 && len(key) == 0 {
			return nil, &method.ErrDecryptionKeyUnavailable{}
		}
	}
	if len(data) == 0 {
		return nil, &method.ErrDecryptionFailed{Cause: &method.ErrCryptoFailure{
			Message: "Cannot decrypt empty data.",
		}}
	}
	input := InputV1{
		Key:     key,
		Payload: data,
	}
	result, err := c.run(c.decryptCommand, input)
	if err != nil {
		return nil, &method.ErrDecryptionFailed{
			Cause: err,
		}
	}
	return result, nil
}

func (c command) run(command []string, input any) ([]byte, error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()

	inputData, err := json.Marshal(input)
	if err != nil {
		return nil, &method.ErrCryptoFailure{
			Message: "failed to marshal input",
			Cause:   err,
		}
	}

	stderr := &bytes.Buffer{}

	cmd := exec.CommandContext(ctx, command[0], command[1:]...)

	handler := &ioHandler{
		false,
		bytes.NewBuffer(inputData),
		[]byte{},
		cancel,
		nil,
	}

	cmd.Stdin = handler
	cmd.Stdout = handler
	cmd.Stderr = stderr
	if err := cmd.Run(); err != nil {
		if handler.err != nil {
			return nil, &method.ErrCryptoFailure{
				Message:          "external encryption method failure",
				Cause:            handler.err,
				SupplementalData: fmt.Sprintf("Stderr:\n-------\n%s\n", stderr.String()),
			}
		}

		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			if exitErr.ExitCode() != 0 {
				return nil, &method.ErrCryptoFailure{
					Message:          "external encryption method exited with non-zero exit code",
					Cause:            err,
					SupplementalData: fmt.Sprintf("Stderr:\n-------\n%s\n", stderr.String()),
				}
			}
		}
		return nil, &method.ErrCryptoFailure{
			Message:          "external encryption method exited with an error",
			Cause:            err,
			SupplementalData: fmt.Sprintf("Stderr:\n-------\n%s\n", stderr.String()),
		}
	}

	var result *OutputV1
	decoder := json.NewDecoder(bytes.NewReader(handler.output))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&result); err != nil {
		return nil, &method.ErrCryptoFailure{
			Message:          "external encryption method returned an invalid JSON",
			Cause:            err,
			SupplementalData: fmt.Sprintf("Stderr:\n-------\n%s\n", stderr.String()),
		}
	}

	return result.Payload, nil
}

type ioHandler struct {
	headerFinished bool
	input          *bytes.Buffer
	output         []byte
	cancel         func()
	err            error
}

func (i *ioHandler) Write(p []byte) (int, error) {
	i.output = append(i.output, p...)
	n := len(p)
	if i.headerFinished {
		// Header is finished, just collect the output.
		return n, nil
	}
	// Check if the full header is present.
	parts := strings.SplitN(string(i.output), "\n", 2)
	if len(parts) == 1 {
		return n, nil
	}
	var header Header
	// Note: this is intentionally not using strict decoding. Later protocol versions may introduce additional header
	// fields.
	if jsonErr := json.Unmarshal([]byte(parts[0]), &header); jsonErr != nil {
		err := fmt.Errorf("failed to unmarshal header from external method (%w)", jsonErr)
		i.err = err
		i.cancel()
		return n, err
	}

	if header.Magic != Magic {
		err := fmt.Errorf("invalid magic received from external method: %s", header.Magic)
		i.err = err
		i.cancel()
		return n, err
	}
	if header.Version != 1 {
		err := fmt.Errorf("invalid version number received from external method: %d", header.Version)
		i.err = err
		i.cancel()
		return n, err
	}
	i.headerFinished = true
	i.output = []byte(parts[1])
	return n, nil
}

func (i *ioHandler) Read(p []byte) (int, error) {
	return i.input.Read(p)
}
