package clusterconfig

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/json"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/dynamic"
	"k8s.io/klog"

	configv1 "github.com/openshift/api/config/v1"

	"github.com/openshift/insights-operator/pkg/record"
)

const (
	// Log compression ratio is defining a multiplier for uncompressed logs
	// diskrecorder would refuse to write files larger than MaxLogSize, so GatherClusterOperators
	// has to limit the expected size of the buffer for logs
	logCompressionRatio = 2
)

type clusterOperatorResource struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`
	Name       string `json:"name"`
	Spec       interface{}
}

// GatherClusterOperators collects all ClusterOperators and their resource.
// It finds unhealthy Pods for unhealthy operators
// GatherClusterOperators collects all the ClusterOperators definitions and their resources.
//
// The Kubernetes api https://github.com/openshift/client-go/blob/master/config/clientset/versioned/typed/config/v1/clusteroperator.go#L62
// Response see https://docs.openshift.com/container-platform/4.3/rest_api/index.html#clusteroperatorlist-v1config-openshift-io
//
// Location of operators in archive: config/clusteroperator/
// See: docs/insights-archive-sample/config/clusteroperator
// Location of pods in archive: config/pod/
func GatherClusterOperators(g *Gatherer) func() ([]record.Record, []error) {
	return func() ([]record.Record, []error) {
		gatherConfigClient, err := configv1client.NewForConfig(g.gatherKubeConfig)
		if err != nil {
			return nil, []error{err}
		}
		discoveryClient, err := discovery.NewDiscoveryClientForConfig(g.gatherKubeConfig)
		if err != nil {
			return nil, []error{err}
		}
		dynamicClient, err := dynamic.NewForConfig(g.gatherKubeConfig)
		if err != nil {
			return nil, []error{err}
		}
		return gatherClusterOperators(g.ctx, gatherConfigClient, discoveryClient, dynamicClient)
	}
}

// gatherClusterOperators collects cluster operators
func gatherClusterOperators(ctx context.Context, configClient configv1client.ConfigV1Interface, discoveryClient discovery.DiscoveryInterface, dynamicClient dynamic.Interface) ([]record.Record, []error) {
	config, err := configClient.ClusterOperators().List(ctx, metav1.ListOptions{})
	if errors.IsNotFound(err) {
		return nil, nil
	}
	if err != nil {
		return nil, []error{err}
	}

	// collect the cluster operators reports
	records := clusterOperatorsRecords(ctx, config.Items, dynamicClient, discoveryClient)
	return records, nil
}

// clusterOperatorsRecords generates the cluster operator records
func clusterOperatorsRecords(ctx context.Context, items []configv1.ClusterOperator, dynamicClient dynamic.Interface, discoveryClient discovery.DiscoveryInterface) []record.Record {
	resVer, _ := getOperatorResourcesVersions(discoveryClient)
	records := make([]record.Record, 0, len(items))

	for idx, co := range items {
		records = append(records, record.Record{
			Name: fmt.Sprintf("config/clusteroperator/%s", items[idx].Name),
			Item: record.JSONMarshaller{Object: &items[idx]},
		})
		if resVer == nil {
			continue
		}

		relRes := collectClusterOperatorResources(ctx, dynamicClient, co, resVer)
		for _, rr := range relRes {
			// imageregistry resources (config, pruner) are gathered in image_registries.go, image_pruners.go
			if strings.Contains(rr.APIVersion, "imageregistry") {
				continue
			}
			gv, err := schema.ParseGroupVersion(rr.APIVersion)
			if err != nil {
				klog.Warningf("Unable to parse group version %s: %s", rr.APIVersion, err)
			}
			records = append(records, record.Record{
				Name: fmt.Sprintf("config/clusteroperator/%s/%s/%s", gv.Group, strings.ToLower(rr.Kind), rr.Name),
				Item: ClusterOperatorResourceAnonymizer{rr},
			})
		}
	}

	return records
}

// collectClusterOperatorResources list all cluster operator resources
func collectClusterOperatorResources(ctx context.Context, dynamicClient dynamic.Interface, co configv1.ClusterOperator, resVer map[string][]string) []clusterOperatorResource {
	var relObj []configv1.ObjectReference
	for _, ro := range co.Status.RelatedObjects {
		if strings.Contains(ro.Group, "operator.openshift.io") {
			relObj = append(relObj, ro)
		}
	}
	if len(relObj) == 0 {
		return nil
	}
	var res []clusterOperatorResource
	for _, ro := range relObj {
		key := fmt.Sprintf("%s-%s", ro.Group, strings.ToLower(ro.Resource))
		versions := resVer[key]
		for _, v := range versions {
			gvr := schema.GroupVersionResource{Group: ro.Group, Version: v, Resource: strings.ToLower(ro.Resource)}
			clusterResource, err := dynamicClient.Resource(gvr).Get(ctx, ro.Name, metav1.GetOptions{})
			if err != nil {
				klog.V(2).Infof("Unable to list %s resource due to: %s", gvr, err)
			}
			if clusterResource == nil {
				continue
			}
			var kind, name, apiVersion string
			err = failEarly(
				func() error { return parseJSONQuery(clusterResource.Object, "kind", &kind) },
				func() error { return parseJSONQuery(clusterResource.Object, "apiVersion", &apiVersion) },
				func() error { return parseJSONQuery(clusterResource.Object, "metadata.name", &name) },
			)
			if err != nil {
				continue
			}
			spec, ok := clusterResource.Object["spec"]
			if !ok {
				klog.Warningf("Can't find spec for cluster operator resource %s", name)
			}
			res = append(res, clusterOperatorResource{Spec: spec, Kind: kind, Name: name, APIVersion: apiVersion})
		}
	}
	return res
}

// getOperatorResourcesVersions get all the operator resource versions
func getOperatorResourcesVersions(discoveryClient discovery.DiscoveryInterface) (map[string][]string, error) {
	resources, err := discoveryClient.ServerPreferredResources()
	if err != nil {
		return nil, err
	}
	resourceVersionMap := make(map[string][]string)
	for _, v := range resources {
		if strings.Contains(v.GroupVersion, "operator.openshift.io") {
			gv, err := schema.ParseGroupVersion(v.GroupVersion)
			if err != nil {
				continue
			}
			for _, ar := range v.APIResources {
				key := fmt.Sprintf("%s-%s", gv.Group, ar.Name)
				_, ok := resourceVersionMap[key]
				if !ok {
					resourceVersionMap[key] = []string{gv.Version}
					continue
				}
				resourceVersionMap[key] = append(resourceVersionMap[key], gv.Version)
			}
		}
	}
	return resourceVersionMap, nil
}

// ClusterOperatorResourceAnonymizer implements serialization of clusterOperatorResource
type ClusterOperatorResourceAnonymizer struct{ resource clusterOperatorResource }

// Marshal serializes clusterOperatorResource with IP address anonymization
func (a ClusterOperatorResourceAnonymizer) Marshal(_ context.Context) ([]byte, error) {
	bytes, err := json.Marshal(a.resource)
	if err != nil {
		return nil, err
	}
	resStr := string(bytes)
	//anonymize URLs
	re := regexp.MustCompile(`"(https|http)://(.*?)"`)
	urlMatches := re.FindAllString(resStr, -1)
	for _, m := range urlMatches {
		m = strings.ReplaceAll(m, "\"", "")
		resStr = strings.ReplaceAll(resStr, m, anonymizeString(m))
	}
	return []byte(resStr), nil
}

// GetExtension returns extension for anonymized cluster operator objects
func (a ClusterOperatorResourceAnonymizer) GetExtension() string {
	return "json"
}
