package ffi

// ffi
//
// Please read the notes about safety (marked with `SAFETY`) in both this file,
// and in turborepo-ffi/lib.rs before modifying this file.

// #include "bindings.h"
//
// #cgo darwin,arm64 LDFLAGS:  -L${SRCDIR} -lturborepo_ffi_darwin_arm64  -lz -liconv -framework Security -framework CoreFoundation -framework SystemConfiguration
// #cgo darwin,amd64 LDFLAGS:  -L${SRCDIR} -lturborepo_ffi_darwin_amd64  -lz -liconv -framework Security -framework CoreFoundation -framework SystemConfiguration
// #cgo linux,arm64,staticbinary LDFLAGS:   -L${SRCDIR} -lturborepo_ffi_linux_arm64 -lunwind -lm
// #cgo linux,amd64,staticbinary LDFLAGS:   -L${SRCDIR} -lturborepo_ffi_linux_amd64 -lunwind -lm
// #cgo linux,arm64,!staticbinary LDFLAGS:   -L${SRCDIR} -lturborepo_ffi_linux_arm64 -lz -lm
// #cgo linux,amd64,!staticbinary LDFLAGS:   -L${SRCDIR} -lturborepo_ffi_linux_amd64 -lz -lm
// #cgo windows,amd64 LDFLAGS: -L${SRCDIR} -lturborepo_ffi_windows_amd64 -lole32 -lbcrypt -lws2_32 -luserenv -lntdll
import "C"

import (
	"errors"
	"fmt"
	"reflect"
	"unsafe"

	ffi_proto "github.com/vercel/turbo/cli/internal/ffi/proto"
	"google.golang.org/protobuf/proto"
)

// Unmarshal consumes a buffer and parses it into a proto.Message
func Unmarshal[M proto.Message](b C.Buffer, c M) error {
	bytes := toBytes(b)
	if err := proto.Unmarshal(bytes, c); err != nil {
		return err
	}

	// free the buffer on the rust side
	//
	// SAFETY: do not use `C.free_buffer` to free a buffer that has been allocated
	// on the go side. If you happen to accidentally use the wrong one, you can
	// expect a segfault on some platforms. This is the only valid callsite.
	C.free_buffer(b)

	return nil
}

// Marshal consumes a proto.Message and returns a bufferfire
//
// NOTE: the buffer must be freed by calling `Free` on it
func Marshal[M proto.Message](c M) C.Buffer {
	bytes, err := proto.Marshal(c)
	if err != nil {
		panic(err)
	}

	return toBuffer(bytes)
}

// Free frees a buffer that has been allocated *on the go side*.
//
// SAFETY: this is not the same as `C.free_buffer`, which frees a buffer that
// has been allocated *on the rust side*. If you happen to accidentally use
// the wrong one, you can expect a segfault on some platforms.
//
// EXAMPLE: it is recommended use this function via a `defer` statement, like so:
//
//	reqBuf := Marshal(&req)
//	defer reqBuf.Free()
func (c C.Buffer) Free() {
	C.free(unsafe.Pointer(c.data))
}

// rather than use C.GoBytes, we use this function to avoid copying the bytes,
// since it is going to be immediately Unmarshalled into a proto.Message
//
// SAFETY: go slices contain a pointer to an underlying buffer with a length.
// if the buffer is known to the garbage collector, dropping the last slice will
// cause the memory to be freed. this memory is owned by the rust side (and is
// not known the garbage collector), so dropping the slice will do nothing
func toBytes(b C.Buffer) []byte {
	var out []byte

	len := (uint32)(b.len)

	sh := (*reflect.SliceHeader)(unsafe.Pointer(&out))
	sh.Data = uintptr(unsafe.Pointer(b.data))
	sh.Len = int(len)
	sh.Cap = int(len)

	return out
}

func toBuffer(bytes []byte) C.Buffer {
	b := C.Buffer{}
	b.len = C.uint(len(bytes))
	b.data = (*C.uchar)(C.CBytes(bytes))
	return b
}

// GetTurboDataDir returns the path to the Turbo data directory
func GetTurboDataDir() string {
	buffer := C.get_turbo_data_dir()
	resp := ffi_proto.TurboDataDirResp{}
	if err := Unmarshal(buffer, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}
	return resp.Dir
}

// Go convention is to use an empty string for an uninitialized or null-valued
// string. Rust convention is to use an Option<String> for the same purpose, which
// is encoded on the Go side as *string. This converts between the two.
func stringToRef(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

// ChangedFiles returns the files changed in between two commits, the workdir and the index, and optionally untracked files
func ChangedFiles(gitRoot string, turboRoot string, fromCommit string, toCommit string) ([]string, error) {
	fromCommitRef := stringToRef(fromCommit)

	req := ffi_proto.ChangedFilesReq{
		GitRoot:    gitRoot,
		FromCommit: fromCommitRef,
		ToCommit:   toCommit,
		TurboRoot:  turboRoot,
	}

	reqBuf := Marshal(&req)
	defer reqBuf.Free()

	respBuf := C.changed_files(reqBuf)

	resp := ffi_proto.ChangedFilesResp{}
	if err := Unmarshal(respBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}
	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}

	return resp.GetFiles().GetFiles(), nil
}

// PreviousContent returns the content of a file at a previous commit
func PreviousContent(gitRoot, fromCommit, filePath string) ([]byte, error) {
	req := ffi_proto.PreviousContentReq{
		GitRoot:    gitRoot,
		FromCommit: fromCommit,
		FilePath:   filePath,
	}

	reqBuf := Marshal(&req)
	defer reqBuf.Free()

	respBuf := C.previous_content(reqBuf)

	resp := ffi_proto.PreviousContentResp{}
	if err := Unmarshal(respBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}
	content := resp.GetContent()
	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}

	return []byte(content), nil
}

// TransitiveDeps returns the transitive external deps for all provided workspaces
func TransitiveDeps(content []byte, packageManager string, workspaces map[string]map[string]string, resolutions map[string]string) (map[string]*ffi_proto.LockfilePackageList, error) {
	var additionalData *ffi_proto.AdditionalBerryData
	if resolutions != nil {
		additionalData = &ffi_proto.AdditionalBerryData{Resolutions: resolutions}
	}
	flatWorkspaces := make(map[string]*ffi_proto.PackageDependencyList)
	for workspace, deps := range workspaces {
		packageDependencyList := make([]*ffi_proto.PackageDependency, len(deps))
		i := 0
		for name, version := range deps {
			packageDependencyList[i] = &ffi_proto.PackageDependency{
				Name:  name,
				Range: version,
			}
			i++
		}
		flatWorkspaces[workspace] = &ffi_proto.PackageDependencyList{List: packageDependencyList}
	}
	req := ffi_proto.TransitiveDepsRequest{
		Contents:       content,
		PackageManager: toPackageManager(packageManager),
		Workspaces:     flatWorkspaces,
		Resolutions:    additionalData,
	}
	reqBuf := Marshal(&req)
	resBuf := C.transitive_closure(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.TransitiveDepsResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}

	dependencies := resp.GetDependencies()
	return dependencies.GetDependencies(), nil
}

func toPackageManager(packageManager string) ffi_proto.PackageManager {
	switch packageManager {
	case "npm":
		return ffi_proto.PackageManager_NPM
	case "berry":
		return ffi_proto.PackageManager_BERRY
	case "pnpm":
		return ffi_proto.PackageManager_PNPM
	case "yarn":
		return ffi_proto.PackageManager_YARN
	case "bun":
		return ffi_proto.PackageManager_BUN
	default:
		panic(fmt.Sprintf("Invalid package manager string: %s", packageManager))
	}
}

// GlobalChange checks if there are any differences between lockfiles that would completely invalidate
// the cache.
func GlobalChange(packageManager string, prevContents []byte, currContents []byte) bool {
	req := ffi_proto.GlobalChangeRequest{
		PackageManager: toPackageManager(packageManager),
		PrevContents:   prevContents,
		CurrContents:   currContents,
	}
	reqBuf := Marshal(&req)
	resBuf := C.global_change(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.GlobalChangeResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	return resp.GetGlobalChange()
}

// VerifySignature checks that the signature of an artifact matches the expected tag
func VerifySignature(teamID []byte, hash string, artifactBody []byte, expectedTag string, secretKeyOverride []byte) (bool, error) {
	req := ffi_proto.VerifySignatureRequest{
		TeamId:            teamID,
		Hash:              hash,
		ArtifactBody:      artifactBody,
		ExpectedTag:       expectedTag,
		SecretKeyOverride: secretKeyOverride,
	}
	reqBuf := Marshal(&req)
	resBuf := C.verify_signature(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.VerifySignatureResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	if err := resp.GetError(); err != "" {
		return false, errors.New(err)
	}

	return resp.GetVerified(), nil
}

// GetPackageFileHashes proxies to rust for hashing the files in a package
func GetPackageFileHashes(rootPath string, packagePath string, inputs []string) (map[string]string, error) {
	req := ffi_proto.GetPackageFileHashesRequest{
		TurboRoot:   rootPath,
		PackagePath: packagePath,
		Inputs:      inputs,
	}
	reqBuf := Marshal(&req)
	resBuf := C.get_package_file_hashes(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.GetPackageFileHashesResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}

	hashes := resp.GetHashes()
	return hashes.GetHashes(), nil
}

// GetHashesForFiles proxies to rust for hashing a given set of files
func GetHashesForFiles(rootPath string, files []string, allowMissing bool) (map[string]string, error) {
	req := ffi_proto.GetHashesForFilesRequest{
		TurboRoot:    rootPath,
		Files:        files,
		AllowMissing: allowMissing,
	}
	reqBuf := Marshal(&req)
	resBuf := C.get_hashes_for_files(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.GetHashesForFilesResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}
	hashes := resp.GetHashes()
	return hashes.GetHashes(), nil
}

// FromWildcards returns an EnvironmentVariableMap containing the variables
// in the environment which match an array of wildcard patterns.
func FromWildcards(environmentMap map[string]string, wildcardPatterns []string) (map[string]string, error) {
	if wildcardPatterns == nil {
		return nil, nil
	}
	req := ffi_proto.FromWildcardsRequest{
		EnvVars: &ffi_proto.EnvVarMap{
			Map: environmentMap,
		},
		WildcardPatterns: wildcardPatterns,
	}
	reqBuf := Marshal(&req)
	resBuf := C.from_wildcards(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.FromWildcardsResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}
	envVarMap := resp.GetEnvVars().GetMap()
	// If the map is nil, return an empty map instead of nil
	// to match with existing Go code.
	if envVarMap == nil {
		return map[string]string{}, nil
	}
	return envVarMap, nil
}

// GetGlobalHashableEnvVars calculates env var dependencies
func GetGlobalHashableEnvVars(envAtExecutionStart map[string]string, globalEnv []string) (*ffi_proto.DetailedMap, error) {
	req := ffi_proto.GetGlobalHashableEnvVarsRequest{
		EnvAtExecutionStart: &ffi_proto.EnvVarMap{Map: envAtExecutionStart},
		GlobalEnv:           globalEnv,
	}
	reqBuf := Marshal(&req)
	resBuf := C.get_global_hashable_env_vars(reqBuf)
	reqBuf.Free()

	resp := ffi_proto.GetGlobalHashableEnvVarsResponse{}
	if err := Unmarshal(resBuf, resp.ProtoReflect().Interface()); err != nil {
		panic(err)
	}

	if err := resp.GetError(); err != "" {
		return nil, errors.New(err)
	}

	respDetailedMap := resp.GetDetailedMap()
	if respDetailedMap == nil {
		return nil, nil
	}

	return respDetailedMap, nil
}
