// Copyright Istio 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 apigen

import (
	"strings"

	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	"google.golang.org/grpc/codes"
	grpcstatus "google.golang.org/grpc/status"

	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/serviceregistry/provider"
	"istio.io/istio/pilot/pkg/serviceregistry/serviceentry"
	"istio.io/istio/pilot/pkg/util/protoconv"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/schema/gvk"
	"istio.io/istio/pkg/log"
)

// APIGenerator supports generation of high-level API resources, similar with the MCP
// protocol. This is a replacement for MCP, using XDS (and in future UDPA) as a transport.
// Based on lessons from MCP, the protocol allows incremental updates by
// default, using the same mechanism that EDS is using, i.e. sending only changed resources
// in a push. Incremental deletes are sent as a resource with empty body.
//
// Example: networking.istio.io/v1alpha3/VirtualService
//
// TODO: we can also add a special marker in the header)
type APIGenerator struct {
	// ConfigStore interface for listing istio api resources.
	store model.ConfigStore
	// requireAuth restricts the generator to verified control-plane identities.
	// Captured at construction to avoid reading the feature flag per-request.
	requireAuth bool
}

func NewGenerator(store model.ConfigStore) *APIGenerator {
	return &APIGenerator{
		store:       store,
		requireAuth: features.EnableXDSAPIGeneratorAuth,
	}
}

// TODO: take 'updates' into account, don't send pushes for resources that haven't changed
// TODO: support WorkloadEntry - to generate endpoints (equivalent with EDS)
// TODO: based on lessons from MCP, we want to send 'chunked' responses, like apiserver does.
// A first attempt added a 'sync' record at the end. Based on feedback and common use, a
// different approach can be used - for large responses, we can mark the last one as 'hasMore'
// by adding a field to the envelope.

// Generate implements the generate method for high level APIs, like Istio config types.
// This provides similar functionality with MCP and :8080/debug/configz.
//
// Names are based on the current resource naming in istiod stores.
func (g *APIGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource, req *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
	if err := g.authorize(proxy, req); err != nil {
		return nil, model.DefaultXdsLogDetails, err
	}

	resp := model.Resources{}

	// Note: this is the style used by MCP and its config. Pilot is using 'Group/Version/Kind' as the
	// key, which is similar.
	//
	// The actual type in the Any should be a real proto - which is based on the generated package name.
	// For example: type is for Any is 'type.googlepis.com/istio.networking.v1alpha3.EnvoyFilter
	// We use: networking.istio.io/v1alpha3/EnvoyFilter
	kind := strings.SplitN(w.TypeUrl, "/", 3)
	if len(kind) != 3 {
		log.Warnf("ADS: Unknown watched resources %s", w.TypeUrl)
		// Still return an empty response - to not break waiting code. It is fine to not know about some resource.
		return resp, model.DefaultXdsLogDetails, nil
	}
	// TODO: extra validation may be needed - at least logging that a resource
	// of unknown type was requested. This should not be an error - maybe client asks
	// for a valid CRD we just don't know about. An empty set indicates we have no such config.
	rgvk := config.GroupVersionKind{
		Group:   kind[0],
		Version: kind[1],
		Kind:    kind[2],
	}
	if w.TypeUrl == gvk.MeshConfig.String() {
		resp = append(resp, &discovery.Resource{
			Resource: protoconv.MessageToAny(req.Push.Mesh),
		})
		return resp, model.DefaultXdsLogDetails, nil
	}

	cfg := g.store.List(rgvk, "")
	for _, c := range cfg {
		// Right now model.Config is not a proto - until we change it, mcp.Resource.
		// This also helps migrating MCP users.

		b, err := config.PilotConfigToResource(&c)
		if err != nil {
			log.WithLabels("resource", c.NamespacedName()).Warnf("resource error: %v", err)
			continue
		}
		resp = append(resp, &discovery.Resource{
			Name:     c.Namespace + "/" + c.Name,
			Resource: protoconv.MessageToAny(b),
		})
	}

	// TODO: MeshConfig, current dynamic ProxyConfig (for this proxy), Networks

	if w.TypeUrl == gvk.ServiceEntry.String() {
		// Include 'synthetic' SE - but without the endpoints. Used to generate CDS, LDS.
		// EDS is pass-through.
		svcs := proxy.SidecarScope.Services()
		for _, s := range svcs {
			// Ignore services that are result of conversion from ServiceEntry.
			if s.Attributes.ServiceRegistry == provider.External {
				continue
			}
			c := serviceentry.ServiceToServiceEntry(s, proxy)
			b, err := config.PilotConfigToResource(c)
			if err != nil {
				log.WithLabels("resource", c.NamespacedName()).Warnf("resource error: %v", err)
				continue
			}
			resp = append(resp, &discovery.Resource{
				Name:     c.Namespace + "/" + c.Name,
				Resource: protoconv.MessageToAny(b),
			})
		}
	}

	return resp, model.DefaultXdsLogDetails, nil
}

// authorize protects the api generator from unauthorized access. It serves cluster-wide
// config, so only callers with a verified control-plane (root namespace) identity are allowed.
func (g *APIGenerator) authorize(proxy *model.Proxy, req *model.PushRequest) error {
	if !g.requireAuth {
		return nil
	}
	systemNamespace := constants.IstioSystemNamespace
	if req != nil && req.Push != nil && req.Push.Mesh != nil && req.Push.Mesh.GetRootNamespace() != "" {
		systemNamespace = req.Push.Mesh.GetRootNamespace()
	}
	if proxy == nil || proxy.VerifiedIdentity == nil {
		id := "<none>"
		if proxy != nil {
			id = proxy.ID
		}
		log.Warnf("ADS: api generator denied unauthenticated request from %q", id)
		return grpcstatus.Error(codes.Unauthenticated, "the api generator requires an authenticated control-plane identity")
	}
	if proxy.VerifiedIdentity.Namespace != systemNamespace {
		log.Warnf("ADS: api generator denied %q (identity %q), restricted to the control-plane namespace %q",
			proxy.ID, proxy.VerifiedIdentity.String(), systemNamespace)
		return grpcstatus.Error(codes.PermissionDenied, "the api generator is restricted to the control-plane (root) namespace")
	}
	return nil
}
