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

package wasm

import (
	"fmt"

	envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
	envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"

	"github.com/hashicorp/consul/api"
	cmn "github.com/hashicorp/consul/envoyextensions/extensioncommon"
)

// wasm is a built-in Envoy extension that can patch filter chains to insert Wasm plugins.
type wasm struct {
	cmn.BasicExtensionAdapter

	name       string
	wasmConfig *wasmConfig
}

var supportedRuntimes = []string{"v8", "wamr", "wavm", "wasmtime"}

var _ cmn.BasicExtension = (*wasm)(nil)

func Constructor(ext api.EnvoyExtension) (cmn.EnvoyExtender, error) {
	w, err := construct(ext)
	if err != nil {
		return nil, err
	}
	return &cmn.BasicEnvoyExtender{
		Extension: &w,
	}, nil
}

func construct(ext api.EnvoyExtension) (wasm, error) {
	w := wasm{name: ext.Name}

	if w.name != api.BuiltinWasmExtension {
		return w, fmt.Errorf("expected extension name %q but got %q", api.BuiltinWasmExtension, w.name)
	}

	if err := w.fromArguments(ext.Arguments); err != nil {
		return w, err
	}

	// Configure the failure behavior for the filter. If the plugin is required,
	// then filter runtime errors result in a failed request (fail "closed").
	// Otherwise, runtime errors result in the filter being skipped (fail "open").
	w.wasmConfig.PluginConfig.failOpen = !ext.Required

	return w, nil
}

func (w *wasm) fromArguments(args map[string]any) error {
	var err error
	w.wasmConfig, err = newWasmConfig(args)
	if err != nil {
		return fmt.Errorf("error decoding extension arguments: %w", err)
	}
	return w.wasmConfig.validate()
}

// CanApply indicates if the WASM extension can be applied to the given extension configuration.
func (w wasm) CanApply(cfg *cmn.RuntimeConfig) bool {
	return cfg.Kind == w.wasmConfig.ProxyType &&
		cfg.Protocol == w.wasmConfig.Protocol
}

func (w wasm) matchesConfigDirection(isInboundListener bool) bool {
	return (isInboundListener && w.wasmConfig.ListenerType == "inbound") ||
		(!isInboundListener && w.wasmConfig.ListenerType == "outbound")
}

// PatchFilters adds a Wasm HTTP or TCP filter to the filter chain.
func (w wasm) PatchFilters(cfg *cmn.RuntimeConfig, filters []*envoy_listener_v3.Filter, isInboundListener bool) ([]*envoy_listener_v3.Filter, error) {
	if !w.matchesConfigDirection(isInboundListener) {
		return filters, nil
	}

	// Check that the Wasm plugin protocol matches the service protocol.
	// It is a runtime error if the extension is configured to apply a Wasm plugin
	// that doesn't match the service's protocol.
	// This shouldn't happen because the caller should check CanApply first, but just in case.
	if cfg.Protocol != w.wasmConfig.Protocol {
		return filters, fmt.Errorf("failed to apply Wasm filter: service protocol for %q is %q but protocol for the Wasm extension is %q. Please ensure the protocols match",
			cfg.ServiceName.Name, cfg.Protocol, w.wasmConfig.Protocol)
	}

	// Generate the Wasm plugin configuration. It is the same config for HTTP and network filters.
	wasmPluginConfig, err := w.wasmConfig.PluginConfig.envoyPluginConfig(cfg)
	if err != nil {
		return filters, fmt.Errorf("failed to encode Envoy Wasm plugin configuration: %w", err)
	}

	// Insert the filter immediately before the terminal filter.
	insertOptions := cmn.InsertOptions{Location: cmn.InsertBeforeFirstMatch}

	switch cfg.Protocol {
	case "grpc", "http2", "http":
		insertOptions.FilterName = "envoy.filters.http.router"
		filter, err := cmn.MakeEnvoyHTTPFilter("envoy.filters.http.wasm", &envoy_http_wasm_v3.Wasm{Config: wasmPluginConfig})
		if err != nil {
			return filters, fmt.Errorf("failed to make Wasm HTTP filter: %w", err)
		}
		return cmn.InsertHTTPFilter(filters, filter, insertOptions)
	case "tcp":
		fallthrough
	default:
		insertOptions.FilterName = "envoy.filters.network.tcp_proxy"
		filter, err := cmn.MakeFilter("envoy.filters.network.wasm", &envoy_wasm_v3.Wasm{Config: wasmPluginConfig})
		if err != nil {
			return filters, fmt.Errorf("failed to make Wasm network filter: %w", err)
		}
		return cmn.InsertNetworkFilter(filters, filter, insertOptions)
	}
}
