/*
Copyright 2016 The Kubernetes 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 routes

import (
	"strings"

	restful "github.com/emicklei/go-restful/v3"
	"k8s.io/klog/v2"

	"k8s.io/apiserver/pkg/server/mux"
	builder2 "k8s.io/kube-openapi/pkg/builder"
	"k8s.io/kube-openapi/pkg/builder3"
	"k8s.io/kube-openapi/pkg/common"
	"k8s.io/kube-openapi/pkg/common/restfuladapter"
	"k8s.io/kube-openapi/pkg/handler"
	"k8s.io/kube-openapi/pkg/handler3"
	"k8s.io/kube-openapi/pkg/spec3"
	"k8s.io/kube-openapi/pkg/validation/spec"
)

// OpenAPI installs spec endpoints for each web service.
type OpenAPI struct {
	Config   *common.Config
	V3Config *common.OpenAPIV3Config
}

// Install adds the SwaggerUI webservice to the given mux.
func (oa OpenAPI) InstallV2(c *restful.Container, mux *mux.PathRecorderMux) (*handler.OpenAPIService, *spec.Swagger) {
	spec, err := builder2.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(c.RegisteredWebServices()), oa.Config)
	if err != nil {
		klog.Fatalf("Failed to build open api spec for root: %v", err)
	}
	spec.Definitions = handler.PruneDefaults(spec.Definitions)
	openAPIVersionedService := handler.NewOpenAPIService(spec)
	openAPIVersionedService.RegisterOpenAPIVersionedService("/openapi/v2", mux)

	return openAPIVersionedService, spec
}

// InstallV3 adds the static group/versions defined in the RegisteredWebServices to the OpenAPI v3 spec.
// This only covers built-in resources served via go-restful; CRDs and aggregated APIs publish
// their OpenAPI v3 specs through separate code paths.
func (oa OpenAPI) InstallV3(c *restful.Container, mux *mux.PathRecorderMux) *handler3.OpenAPIService {
	openAPIVersionedService := handler3.NewOpenAPIService()
	err := openAPIVersionedService.RegisterOpenAPIV3VersionedService("/openapi/v3", mux)
	if err != nil {
		klog.Fatalf("Failed to register versioned open api spec for root: %v", err)
	}

	grouped := make(map[string][]*restful.WebService)

	for _, t := range c.RegisteredWebServices() {
		// Strip the "/" prefix from the name
		gvName := t.RootPath()[1:]
		grouped[gvName] = []*restful.WebService{t}
	}

	for gv, ws := range grouped {
		spec, err := builder3.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(ws), oa.V3Config)
		if err != nil {
			klog.Errorf("Failed to build OpenAPI v3 for group %s, %q", gv, err)
			continue
		}
		if group, version, ok := groupVersionFromPath(gv); ok {
			filterScopedGVKs(spec, group, version)
		}
		openAPIVersionedService.UpdateGroupVersion(gv, spec)
	}
	return openAPIVersionedService
}

// groupVersionFromPath extracts the API group and version from a root path like "apis/apps/v1" or "api/v1".
func groupVersionFromPath(path string) (group, version string, ok bool) {
	// "api/v1" → ("", "v1", true)
	// "apis/apps/v1" → ("apps", "v1", true)
	// "apis/networking.k8s.io/v1" → ("networking.k8s.io", "v1", true)
	parts := strings.SplitN(path, "/", 4)
	switch {
	case len(parts) < 2:
		return "", "", false
	case parts[0] == "api" && len(parts) == 2:
		return "", parts[1], true
	case parts[0] == "apis" && len(parts) == 3:
		return parts[1], parts[2], true
	default:
		return "", "", false
	}
}

// crossRegisteredKinds lists the kinds that AddToGroupVersion (in
// k8s.io/apimachinery/pkg/apis/meta/v1) registers into every API group.
// Only these types get their x-kubernetes-group-version-kind list filtered
// in per-GV v3 specs.
var crossRegisteredKinds = map[string]bool{
	"WatchEvent":    true,
	"DeleteOptions": true,
}

// filterScopedGVKs narrows x-kubernetes-group-version-kind on the meta types
// that AddToGroupVersion cross-registers into every API group (WatchEvent and
// DeleteOptions). Without filtering, each per-GV v3 spec carries ~60 GVKs for
// these types. This filter keeps only the entry matching this spec's group/version
// plus the canonical core/v1 entry.
//
// Only the meta types listed in crossRegisteredKinds are filtered. Other types
// that are intentionally registered into specific groups (like autoscaling/v1
// Scale into apps/v1 and core/v1) are left unchanged.
//
// This only affects built-in resource specs generated by InstallV3 above.
// CRDs and aggregated APIs are not affected as they publish specs through separate paths.
func filterScopedGVKs(s *spec3.OpenAPI, group, version string) {
	if s == nil || s.Components == nil {
		return
	}
	for _, schema := range s.Components.Schemas {
		if schema == nil {
			continue
		}
		ext, ok := schema.Extensions["x-kubernetes-group-version-kind"]
		if !ok {
			continue
		}
		gvks, ok := ext.([]interface{})
		if !ok || len(gvks) <= 1 {
			continue
		}
		if !isCrossRegisteredKind(gvks) {
			continue
		}
		var filtered []interface{}
		for _, item := range gvks {
			m, ok := item.(map[string]interface{})
			if !ok {
				continue
			}
			g, ok := m["group"].(string)
			if !ok {
				continue
			}
			v, ok := m["version"].(string)
			if !ok {
				continue
			}
			if (g == group && v == version) || (g == "" && v == "v1") {
				filtered = append(filtered, item)
			}
		}
		if len(filtered) > 0 {
			schema.Extensions["x-kubernetes-group-version-kind"] = filtered
		} else {
			klog.Warningf("Unexpected: filtering x-kubernetes-group-version-kind for %s/%s produced no matches", group, version)
			delete(schema.Extensions, "x-kubernetes-group-version-kind")
		}
	}
}

// isCrossRegisteredKind checks whether a GVK list represents one of the
// meta types from crossRegisteredKinds by inspecting the first entry's kind.
func isCrossRegisteredKind(gvks []interface{}) bool {
	if len(gvks) == 0 {
		return false
	}
	m, ok := gvks[0].(map[string]interface{})
	if !ok {
		return false
	}
	kind, _ := m["kind"].(string)
	return crossRegisteredKinds[kind]
}
