/*
 *
 * Copyright 2025 gRPC authors.
 *
 * 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 xdsresource

import (
	"fmt"
	"net/netip"

	"google.golang.org/grpc/internal/envconfig"
	"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
	"google.golang.org/protobuf/types/known/anypb"

	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	v3gcpauthnpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3"
)

func init() {
	if envconfig.XDSHTTPConnectEnabled {
		registerMetadataConverter(version.V3AddressURL, proxyAddressConverter{})
	}
	if envconfig.GCPAuthenticationFilterEnabled {
		registerMetadataConverter(version.V3AudienceURL, audienceConverter{})
	}
}

var (
	// metadataRegistry is a map from proto type to metadataConverter.
	metadataRegistry = make(map[string]metadataConverter)
)

// metadataConverter converts xds metadata entries in
// Metadata.typed_filter_metadata into an internal form with the fields relevant
// to gRPC.
type metadataConverter interface {
	// convert parses the Any proto into a concrete struct.
	convert(*anypb.Any) (any, error)
}

// registerMetadataConverter registers the converter to the map keyed on a proto
// type_url. Must be called at init time. Not thread safe.
func registerMetadataConverter(protoType string, c metadataConverter) {
	metadataRegistry[protoType] = c
}

// metadataConverterForType retrieves a converter based on key given.
func metadataConverterForType(typeURL string) metadataConverter {
	return metadataRegistry[typeURL]
}

// RegisterMetadataConverterForTesting registers the converter for testing
// purposes and returns a cleanup function to restore the registry to its
// previous state.
func RegisterMetadataConverterForTesting(typeURL string) (func(), error) {
	var conv metadataConverter
	switch typeURL {
	case version.V3AddressURL:
		conv = proxyAddressConverter{}
	case version.V3AudienceURL:
		conv = audienceConverter{}
	default:
		return nil, fmt.Errorf("unknown typeURL for testing: %s", typeURL)
	}
	curConverter, found := metadataRegistry[typeURL]
	registerMetadataConverter(typeURL, conv)
	return func() {
		if found {
			metadataRegistry[typeURL] = curConverter
			return
		}
		delete(metadataRegistry, typeURL)
	}, nil
}

// UnregisterMetadataConverterForTesting unregisters the converter for testing
// purposes and returns a cleanup function to restore the registry to its
// previous state.
func UnregisterMetadataConverterForTesting(typeURL string) func() {
	curConverter, found := metadataRegistry[typeURL]
	delete(metadataRegistry, typeURL)
	return func() {
		if found {
			metadataRegistry[typeURL] = curConverter
		}
	}
}

// StructMetadataValue stores the values in a google.protobuf.Struct from
// FilterMetadata.
type StructMetadataValue struct {
	// Data stores the parsed JSON representation of a google.protobuf.Struct.
	Data map[string]any
}

// ProxyAddressMetadataValue holds the address parsed from the
// envoy.config.core.v3.Address proto message, as specified in gRFC A86.
type ProxyAddressMetadataValue struct {
	// Address stores the proxy address configured (A86). It will be in the form
	// of host:port. It has to be either IPv6 or IPv4.
	Address string
}

// proxyAddressConvertor implements the metadataConverter interface to handle
// the conversion of envoy.config.core.v3.Address protobuf messages into an
// internal representation.
type proxyAddressConverter struct{}

func (proxyAddressConverter) convert(anyProto *anypb.Any) (any, error) {
	addressProto := &v3corepb.Address{}
	if err := anyProto.UnmarshalTo(addressProto); err != nil {
		return nil, fmt.Errorf("failed to unmarshal resource from Any proto: %v", err)
	}
	socketaddress := addressProto.GetSocketAddress()
	if socketaddress == nil {
		return nil, fmt.Errorf("no socket_address field in metadata")
	}
	if _, err := netip.ParseAddr(socketaddress.GetAddress()); err != nil {
		return nil, fmt.Errorf("address field is not a valid IPv4 or IPv6 address: %q", socketaddress.GetAddress())
	}
	portvalue := socketaddress.GetPortValue()
	if portvalue == 0 {
		return nil, fmt.Errorf("port value not set in socket_address")
	}
	return ProxyAddressMetadataValue{Address: parseAddress(socketaddress)}, nil
}

// AudienceMetadataValue holds the audience parsed from the
// envoy.extensions.filters.http.gcp_authn.v3.Audience proto message, as
// specified in gRFC A83.
type AudienceMetadataValue struct {
	// Audience is the URL of the receiving service that performs token
	// authentication.
	Audience string
}

// audienceConverter implements the metadataConverter interface to
// handle the conversion of envoy.extensions.filters.http.gcp_authn.v3.Audience
// protobuf messages into an internal representation.
type audienceConverter struct{}

func (audienceConverter) convert(anyProto *anypb.Any) (any, error) {
	audienceProto := &v3gcpauthnpb.Audience{}
	if err := anyProto.UnmarshalTo(audienceProto); err != nil {
		return nil, fmt.Errorf("failed to unmarshal the envoy.extensions.filters.http.gcp_authn.v3.Audience resource from Any proto: %v", err)
	}
	if audienceProto.GetUrl() == "" {
		return nil, fmt.Errorf("empty url field in audience metadata")
	}
	return AudienceMetadataValue{Audience: audienceProto.GetUrl()}, nil
}
