// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package wasm

import (
	"context"
	"errors"
	"fmt"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

const (
	// defaultMaxMemoryBytes is the maximum memory size in bytes.
	defaultMaxMemoryBytes = 1 << 29 // 512 MiB
	// wasmPageSize is the page size in bytes.
	wasmPageSize = 1 << 16 // 64 KiB
)

type runtime struct {
	runtime          wazero.Runtime
	compilationCache wazero.CompilationCache
}

func newRuntime(ctx context.Context, options ...RuntimeOption) (*runtime, error) {
	runtimeOptions := newRuntimeOptions()
	for _, option := range options {
		option(runtimeOptions)
	}
	if runtimeOptions.maxMemoryBytes == 0 {
		return nil, fmt.Errorf("Wasm max memory bytes must be greater than 0")
	}
	// The maximum memory size is limited to 4 GiB. Sizes less than the page
	// size (64 KiB) are truncated. memoryLimitPages is guaranteed to be
	// below 2^16 as the maximum uint32 value is 2^32 - 1.
	// NOTE: The option represented as a uint32 restricts the max number of
	// pages to 2^16-1, one less the the actual max value of 2^16. But this
	// is a nicer API then specifying the max number of pages directly.
	memoryLimitPages := runtimeOptions.maxMemoryBytes / wasmPageSize
	if memoryLimitPages == 0 {
		return nil, fmt.Errorf("Wasm max memory bytes %d is too small", runtimeOptions.maxMemoryBytes)
	}

	// Create the wazero.RuntimeConfig with enforceable limits. Limits are
	// enforced through the Wasm sandbox. The following limits are set:
	//  - Memory limit: The maximum memory size in pages.
	//  - CPU limit: The runtime stops work on context done.
	//  - Access limit: All system interfaces are stubbed. No network,
	//    disk, clock, etc.
	// See wazero.NewRuntimeConfig for more details.
	wazeroRuntimeConfig := wazero.NewRuntimeConfig().
		WithCoreFeatures(api.CoreFeaturesV2).
		WithCloseOnContextDone(true).
		WithMemoryLimitPages(memoryLimitPages)
	var wazeroCompilationCache wazero.CompilationCache
	if runtimeOptions.cacheDir != "" {
		var err error
		wazeroCompilationCache, err = wazero.NewCompilationCacheWithDir(runtimeOptions.cacheDir)
		if err != nil {
			return nil, fmt.Errorf("failed to create compilation cache: %w", err)
		}
		wazeroRuntimeConfig = wazeroRuntimeConfig.WithCompilationCache(wazeroCompilationCache)
	}
	wazeroRuntime := wazero.NewRuntimeWithConfig(ctx, wazeroRuntimeConfig)

	// Init WASI preview1 APIs. This is required to support the pluginrpc
	// protocol. The returned closer method is discarded as the
	// instantiated module is never required to be unloaded.
	if _, err := wasi_snapshot_preview1.Instantiate(ctx, wazeroRuntime); err != nil {
		return nil, fmt.Errorf("failed to instantiate WASI snapshot preview1: %w", err)
	}
	return &runtime{
		runtime:          wazeroRuntime,
		compilationCache: wazeroCompilationCache,
	}, nil
}

func (r *runtime) Compile(ctx context.Context, moduleName string, moduleWasm []byte) (CompiledModule, error) {
	if moduleName == "" {
		// The plugin is required to be named. We cannot use the name
		// from the Wasm binary as this is not guaranteed to be set and
		// may conflict with the provided name.
		return nil, fmt.Errorf("name is empty")
	}
	// Compile the WebAssembly. This operation is hashed on the module
	// bytes and the runtime configuration. The compiled module is
	// cached in memory and on disk if an optional cache directory is
	// provided.
	wazeroCompiledModule, err := r.runtime.CompileModule(ctx, moduleWasm)
	if err != nil {
		return nil, err
	}
	return &compiledModule{
		moduleName:     moduleName,
		runtime:        r.runtime,
		compiledModule: wazeroCompiledModule,
	}, nil
}

func (r *runtime) Close(ctx context.Context) error {
	err := r.runtime.Close(ctx)
	if r.compilationCache != nil {
		err = errors.Join(err, r.compilationCache.Close(ctx))
	}
	return err
}

type runtimeOptions struct {
	maxMemoryBytes uint32
	cacheDir       string
}

func newRuntimeOptions() *runtimeOptions {
	return &runtimeOptions{
		maxMemoryBytes: defaultMaxMemoryBytes,
	}
}
