/*
Copyright 2018 The Knative 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 webhook

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"reflect"
	"strings"

	"github.com/knative/pkg/webhook"
	"github.com/knative/test-infra/tools/webhook-apicoverage/coveragecalculator"
	"github.com/knative/test-infra/tools/webhook-apicoverage/resourcetree"
	"github.com/knative/test-infra/tools/webhook-apicoverage/view"
	"go.uber.org/zap"
	"k8s.io/api/admission/v1beta1"
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
)

var (
	decoder = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer()
)

const (
	// ResourceQueryParam query param name to provide the resource.
	ResourceQueryParam = "resource"

	// ResourceCoverageEndPoint is the endpoint for Resource Coverage API
	ResourceCoverageEndPoint = "/resourcecoverage"

	// TotalCoverageEndPoint is the endpoint for Total Coverage API
	TotalCoverageEndPoint = "/totalcoverage"

	// resourceChannelQueueSize size of the queue maintained for resource channel.
	resourceChannelQueueSize = 10
)

type resourceChannelMsg struct {
	resourceType schema.GroupVersionKind
	rawResourceValue []byte
}
// APICoverageRecorder type contains resource tree to record API coverage for resources.
type APICoverageRecorder struct {
	Logger *zap.SugaredLogger
	ResourceForest resourcetree.ResourceForest
	ResourceMap map[schema.GroupVersionKind]webhook.GenericCRD
	NodeRules resourcetree.NodeRules
	FieldRules resourcetree.FieldRules
	DisplayRules view.DisplayRules
	resourceChannel chan resourceChannelMsg
}

// Init initializes the resources trees for set resources.
func (a *APICoverageRecorder) Init() {
	for resourceKind, resourceObj := range a.ResourceMap {
		a.ResourceForest.AddResourceTree(resourceKind.Kind, reflect.ValueOf(resourceObj).Elem().Type())
	}
	a.resourceChannel = make(chan resourceChannelMsg, resourceChannelQueueSize)
	go a.updateResourceCoverageTree()
}

// updateResourceCoverageTree updates the resource coverage tree.
func (a *APICoverageRecorder) updateResourceCoverageTree() {
	for {
		channelMsg := <- a.resourceChannel
		if err := json.Unmarshal(channelMsg.rawResourceValue, a.ResourceMap[channelMsg.resourceType]); err != nil {
			a.Logger.Errorf("Failed unmarshalling review.Request.Object.Raw for type: %s Error: %v", channelMsg.resourceType.Kind, err)
			continue
		}
		resourceTree := a.ResourceForest.TopLevelTrees[channelMsg.resourceType.Kind]
		resourceTree.UpdateCoverage(reflect.ValueOf(a.ResourceMap[channelMsg.resourceType]).Elem())
		a.Logger.Info("Successfully recorded coverage for resource ", channelMsg.resourceType.Kind)
	}
}

// RecordResourceCoverage updates the resource tree with the request.
func (a *APICoverageRecorder) RecordResourceCoverage(w http.ResponseWriter, r *http.Request) {
	var (
		body []byte
		err error
	)

	review := &v1beta1.AdmissionReview{}
	if body, err = ioutil.ReadAll(r.Body); err != nil {
		a.Logger.Errorf("Failed reading request body: %v", err)
		a.appendAndWriteAdmissionResponse(review, false, "Admission Denied", w)
		return
	}

	if _, _, err := decoder.Decode(body, nil, review); err != nil {
		a.Logger.Errorf("Unable to decode request: %v", err)
		a.appendAndWriteAdmissionResponse(review, false, "Admission Denied", w)
		return
	}

	gvk := schema.GroupVersionKind {
		Group:   review.Request.Kind.Group,
		Version: review.Request.Kind.Version,
		Kind:    review.Request.Kind.Kind,
	}
	// We only care about resources the repo has setup.
	if _, ok := a.ResourceMap[gvk]; !ok {
		a.Logger.Info("By-passing resource coverage update for resource : %s", gvk.Kind)
		a.appendAndWriteAdmissionResponse(review, true, "Welcome Aboard", w)
		return
	}

	a.resourceChannel <- resourceChannelMsg{
		resourceType: gvk,
		rawResourceValue: review.Request.Object.Raw,
	}
	a.appendAndWriteAdmissionResponse(review, true, "Welcome Aboard", w)
}

func (a *APICoverageRecorder) appendAndWriteAdmissionResponse(review *v1beta1.AdmissionReview, allowed bool, message string, w http.ResponseWriter) {
	review.Response = &v1beta1.AdmissionResponse{
		Allowed: allowed,
		Result: &v1.Status{
			Message: message,
		},
	}

	responseInBytes, err := json.Marshal(review)
	if err != nil {
		a.Logger.Errorf("Failing mashalling review response: %v", err)
	}

	if _, err := w.Write(responseInBytes); err != nil {
		a.Logger.Errorf("%v", err)
	}
}

// GetResourceCoverage retrieves resource coverage data for the passed in resource via query param.
func (a *APICoverageRecorder) GetResourceCoverage(w http.ResponseWriter, r *http.Request) {
	resource := r.URL.Query().Get(ResourceQueryParam)
	if _, ok := a.ResourceForest.TopLevelTrees[resource]; !ok {
		fmt.Fprintf(w, "Resource information not found for resource: %s", resource)
		return
	}

	var ignoredFields coveragecalculator.IgnoredFields
	ignoredFieldsFilePath := os.Getenv("KO_DATA_PATH") + "/ignoredfields.yaml"
	if err := ignoredFields.ReadFromFile(ignoredFieldsFilePath); err != nil {
		fmt.Fprintf(w, "Error reading file: %s", ignoredFieldsFilePath)
	}

	tree := a.ResourceForest.TopLevelTrees[resource]
	typeCoverage := tree.BuildCoverageData(a.NodeRules, a.FieldRules, ignoredFields)
	coverageValues := coveragecalculator.CalculateTypeCoverage(typeCoverage)

	var buffer strings.Builder
	buffer.WriteString(view.GetHTMLDisplay(typeCoverage, a.DisplayRules))
	buffer.WriteString(view.GetHTMLCoverageValuesDisplay(coverageValues))
	fmt.Fprint(w, buffer.String())
}

// GetTotalCoverage goes over all the resources setup for the apicoverage tool and returns total coverage values.
func (a *APICoverageRecorder) GetTotalCoverage(w http.ResponseWriter, r *http.Request) {
	var (
		ignoredFields coveragecalculator.IgnoredFields
		err error
	)

	ignoredFieldsFilePath := os.Getenv("KO_DATA_PATH") + "/ignoredfields.yaml"
	if err = ignoredFields.ReadFromFile(ignoredFieldsFilePath); err != nil {
		fmt.Fprintf(w, "error reading file: %s error: %v", ignoredFieldsFilePath, err)
	}

	totalCoverage := coveragecalculator.CoverageValues{}
	for resource := range a.ResourceMap {
		tree := a.ResourceForest.TopLevelTrees[resource.Kind]
		typeCoverage := tree.BuildCoverageData(a.NodeRules, a.FieldRules, ignoredFields)
		coverageValues := coveragecalculator.CalculateTypeCoverage(typeCoverage)
		totalCoverage.TotalFields += coverageValues.TotalFields
		totalCoverage.CoveredFields += coverageValues.CoveredFields
		totalCoverage.IgnoredFields += coverageValues.IgnoredFields
	}

	var body []byte
	if body, err = json.Marshal(totalCoverage); err != nil {
		fmt.Fprintf(w, "error marshalling total coverage response: %v", err)
	}

	if _, err = w.Write(body); err != nil {
		fmt.Fprintf(w, "error writing total coverage response: %v", err)
	}
}