package discovery

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"

	"github.com/emicklei/go-restful/v3"
	apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
	apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
	"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
	"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
)

// RootDiscoveryHandler serves the `/apis` endpoint.
type RootDiscoveryHandler struct {
	delegate http.Handler
	codecs   serializer.CodecFactory
}

func NewRootDiscoveryHandler(delegate http.Handler) *RootDiscoveryHandler {
	scheme := runtime.NewScheme()
	metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})

	// TODO: keep the generic API server from wanting this
	unversioned := schema.GroupVersion{Group: "", Version: "v1"}
	scheme.AddUnversionedTypes(unversioned,
		&metav1.Status{},
		&metav1.APIVersions{},
		&metav1.APIGroupList{},
		&metav1.APIGroup{},
		&metav1.APIResourceList{},
	)
	utilruntime.Must(apidiscoveryv2.AddToScheme(scheme))
	utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
	codecs := serializer.NewCodecFactory(scheme)
	return &RootDiscoveryHandler{
		delegate: delegate,
		codecs:   codecs,
	}
}

func (a *RootDiscoveryHandler) Handle(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
	if req.Request.URL.Path != "/apis" && req.Request.URL.Path != "/apis/" {
		chain.ProcessFilter(req, resp)
		return
	}
	apisHandlerWithAggregationSupport := aggregated.WrapAggregatedDiscoveryToHandler(
		a.v1handler(chain),
		a.v2handler(chain),
		nil, // peerAggregatedHandler
	)
	apisHandlerWithAggregationSupport.ServeHTTP(resp.ResponseWriter, req.Request)
}

func (a *RootDiscoveryHandler) v2handler(chain *restful.FilterChain) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		newReq := restful.NewRequest(req)
		newRes := restful.NewResponse(w)
		chain.ProcessFilter(newReq, newRes)
	}
}

func (a *RootDiscoveryHandler) v1handler(chain *restful.FilterChain) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		clonedReq := req.Clone(req.Context())
		clonedReq.Header.Set("Accept", "application/json")
		newReq := restful.NewRequest(clonedReq)
		rw := httptest.NewRecorder()
		newRes := restful.NewResponse(rw)

		chain.ProcessFilter(newReq, newRes)
		if rw.Code != http.StatusOK {
			http.Error(w, rw.Body.String(), rw.Code)
			return
		}

		discoveryGroupList := metav1.APIGroupList{}
		if err := json.Unmarshal(rw.Body.Bytes(), &discoveryGroupList); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		clonedReq = req.Clone(req.Context())
		clonedReq.Header.Set("Accept", "application/json")
		rw = httptest.NewRecorder()

		a.delegate.ServeHTTP(rw, clonedReq)
		if rw.Code != http.StatusOK {
			http.Error(w, rw.Body.String(), rw.Code)
			return
		}

		proxiedGroups := metav1.APIGroupList{}
		if err := json.Unmarshal(rw.Body.Bytes(), &proxiedGroups); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		discoveryGroupList.Groups = append(discoveryGroupList.Groups, proxiedGroups.Groups...)
		responsewriters.WriteObjectNegotiated(a.codecs, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, &discoveryGroupList, false)
	}
}
