package common

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-git/go-billy/v5"
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/autogen"
	"github.com/kyverno/kyverno/pkg/clients/dclient"
	kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
	yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"
	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	"k8s.io/api/admissionregistration/v1alpha1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/yaml"
)

// GetResources gets matched resources by the given policies
// the resources are fetched from
// - local paths to resources, if given
// - the k8s cluster, if given
func GetResources(
	policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, resourcePaths []string, dClient dclient.Interface, cluster bool,
	namespace string, policyReport bool,
) ([]*unstructured.Unstructured, error) {
	resources := make([]*unstructured.Unstructured, 0)
	var err error

	if cluster && dClient != nil {
		if len(policies) > 0 {
			matchedResources := &KyvernoResources{
				policies: policies,
			}

			resources, err = matchedResources.FetchResourcesFromPolicy(resourcePaths, dClient, namespace, policyReport)
			if err != nil {
				return resources, err
			}
		}

		if len(validatingAdmissionPolicies) > 0 {
			matchedResources := &ValidatingAdmissionResources{
				policies: validatingAdmissionPolicies,
			}

			resources, err = matchedResources.FetchResourcesFromPolicy(resourcePaths, dClient, namespace, policyReport)
			if err != nil {
				return resources, err
			}
		}
	} else if len(resourcePaths) > 0 {
		resources, err = whenClusterIsFalse(resourcePaths, policyReport)
		if err != nil {
			return resources, err
		}
	}
	return resources, err
}

func whenClusterIsTrue(resourceTypes []schema.GroupVersionKind, subresourceMap map[schema.GroupVersionKind]Subresource, dClient dclient.Interface, namespace string, resourcePaths []string, policyReport bool) ([]*unstructured.Unstructured, error) {
	resources := make([]*unstructured.Unstructured, 0)
	resourceMap, err := getResourcesOfTypeFromCluster(resourceTypes, subresourceMap, dClient, namespace)
	if err != nil {
		return nil, err
	}
	if len(resourcePaths) == 0 {
		for _, rr := range resourceMap {
			resources = append(resources, rr)
		}
	} else {
		for _, resourcePath := range resourcePaths {
			lenOfResource := len(resources)
			for rn, rr := range resourceMap {
				s := strings.Split(rn, "-")
				if s[2] == resourcePath {
					resources = append(resources, rr)
				}
			}
			if lenOfResource >= len(resources) {
				if policyReport {
					log.V(3).Info(fmt.Sprintf("%s not found in cluster", resourcePath))
				} else {
					fmt.Printf("\n----------------------------------------------------------------------\nresource %s not found in cluster\n----------------------------------------------------------------------\n", resourcePath)
				}
				return nil, fmt.Errorf("%s not found in cluster", resourcePath)
			}
		}
	}
	return resources, nil
}

func whenClusterIsFalse(resourcePaths []string, policyReport bool) ([]*unstructured.Unstructured, error) {
	resources := make([]*unstructured.Unstructured, 0)
	for _, resourcePath := range resourcePaths {
		resourceBytes, err := getFileBytes(resourcePath)
		if err != nil {
			if policyReport {
				log.V(3).Info(fmt.Sprintf("failed to load resources: %s.", resourcePath), "error", err)
			} else {
				fmt.Printf("\n----------------------------------------------------------------------\nfailed to load resources: %s. \nerror: %s\n----------------------------------------------------------------------\n", resourcePath, err)
			}
			continue
		}

		getResources, err := GetResource(resourceBytes)
		if err != nil {
			return nil, err
		}

		resources = append(resources, getResources...)
	}
	return resources, nil
}

// GetResourcesWithTest with gets matched resources by the given policies
func GetResourcesWithTest(fs billy.Filesystem, policies []kyvernov1.PolicyInterface, resourcePaths []string, isGit bool, policyResourcePath string) ([]*unstructured.Unstructured, error) {
	resources := make([]*unstructured.Unstructured, 0)
	resourceTypesMap := make(map[string]bool)
	for _, policy := range policies {
		for _, rule := range autogen.ComputeRules(policy) {
			for _, kind := range rule.MatchResources.Kinds {
				resourceTypesMap[kind] = true
			}
		}
	}
	if len(resourcePaths) > 0 {
		for _, resourcePath := range resourcePaths {
			var resourceBytes []byte
			var err error
			if isGit {
				filep, err := fs.Open(filepath.Join(policyResourcePath, resourcePath))
				if err != nil {
					fmt.Printf("Unable to open resource file: %s. error: %s", resourcePath, err)
					continue
				}
				resourceBytes, _ = io.ReadAll(filep)
			} else {
				resourceBytes, err = getFileBytes(resourcePath)
			}
			if err != nil {
				fmt.Printf("\n----------------------------------------------------------------------\nfailed to load resources: %s. \nerror: %s\n----------------------------------------------------------------------\n", resourcePath, err)
				continue
			}

			getResources, err := GetResource(resourceBytes)
			if err != nil {
				return nil, err
			}

			resources = append(resources, getResources...)
		}
	}
	return resources, nil
}

// GetResource converts raw bytes to unstructured object
func GetResource(resourceBytes []byte) ([]*unstructured.Unstructured, error) {
	resources := make([]*unstructured.Unstructured, 0)
	var getErrString string

	files, splitDocError := yamlutils.SplitDocuments(resourceBytes)
	if splitDocError != nil {
		return nil, splitDocError
	}

	for _, resourceYaml := range files {
		resource, err := convertResourceToUnstructured(resourceYaml)
		if err != nil {
			if strings.Contains(err.Error(), "Object 'Kind' is missing") {
				log.V(3).Info("skipping resource as kind not found")
				continue
			}
			getErrString = getErrString + err.Error() + "\n"
		}
		resources = append(resources, resource)
	}

	if getErrString != "" {
		return nil, errors.New(getErrString)
	}

	return resources, nil
}

func getResourcesOfTypeFromCluster(resourceTypes []schema.GroupVersionKind, subresourceMap map[schema.GroupVersionKind]Subresource, dClient dclient.Interface, namespace string) (map[string]*unstructured.Unstructured, error) {
	r := make(map[string]*unstructured.Unstructured)
	for _, kind := range resourceTypes {
		resourceList, err := dClient.ListResource(context.TODO(), kind.GroupVersion().String(), kind.Kind, namespace, nil)
		if err != nil {
			continue
		}
		gvk := resourceList.GroupVersionKind()
		for _, resource := range resourceList.Items {
			key := kind.Kind + "-" + resource.GetNamespace() + "-" + resource.GetName()
			resource.SetGroupVersionKind(schema.GroupVersionKind{
				Group:   gvk.Group,
				Version: gvk.Version,
				Kind:    kind.Kind,
			})
			r[key] = resource.DeepCopy()
		}
	}
	for _, subresource := range subresourceMap {
		parentGV := schema.GroupVersion{Group: subresource.ParentResource.Group, Version: subresource.ParentResource.Version}
		resourceList, err := dClient.ListResource(context.TODO(), parentGV.String(), subresource.ParentResource.Kind, namespace, nil)
		if err != nil {
			continue
		}
		parentResourceNames := make([]string, 0)
		for _, resource := range resourceList.Items {
			parentResourceNames = append(parentResourceNames, resource.GetName())
		}
		for _, parentResourceName := range parentResourceNames {
			subresourceName := strings.Split(subresource.APIResource.Name, "/")[1]
			resource, err := dClient.GetResource(context.TODO(), parentGV.String(), subresource.ParentResource.Kind, namespace, parentResourceName, subresourceName)
			if err != nil {
				fmt.Printf("Error: %s", err.Error())
				continue
			}
			key := subresource.APIResource.Kind + "-" + resource.GetNamespace() + "-" + resource.GetName()
			resource.SetGroupVersionKind(schema.GroupVersionKind{
				Group:   subresource.APIResource.Group,
				Version: subresource.APIResource.Version,
				Kind:    subresource.APIResource.Kind,
			})
			r[key] = resource.DeepCopy()
		}
	}
	return r, nil
}

func getFileBytes(path string) ([]byte, error) {
	var (
		file []byte
		err  error
	)

	if IsHTTPRegex.MatchString(path) {
		// We accept here that a random URL might be called based on user provided input.
		req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, path, nil)
		if err != nil {
			return nil, err
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			return nil, err
		}

		file, err = io.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}
	} else {
		path = filepath.Clean(path)
		// We accept the risk of including a user provided file here.
		file, err = os.ReadFile(path) // #nosec G304
		if err != nil {
			return nil, err
		}
	}

	return file, err
}

func convertResourceToUnstructured(resourceYaml []byte) (*unstructured.Unstructured, error) {
	decode := scheme.Codecs.UniversalDeserializer().Decode
	_, metaData, decodeErr := decode(resourceYaml, nil, nil)

	if decodeErr != nil {
		if !strings.Contains(decodeErr.Error(), "no kind") {
			return nil, decodeErr
		}
	}

	resourceJSON, err := yaml.YAMLToJSON(resourceYaml)
	if err != nil {
		return nil, err
	}

	resource, err := kubeutils.BytesToUnstructured(resourceJSON)
	if err != nil {
		return nil, err
	}

	if decodeErr == nil {
		resource.SetGroupVersionKind(*metaData)
	}

	if resource.GetNamespace() == "" {
		resource.SetNamespace("default")
	}
	return resource, nil
}

// GetPatchedAndGeneratedResource converts raw bytes to unstructured object
func GetPatchedAndGeneratedResource(resourceBytes []byte) (unstructured.Unstructured, error) {
	getResource, err := GetResource(resourceBytes)
	if err != nil {
		return unstructured.Unstructured{}, err
	}
	if len(getResource) > 0 && getResource[0] != nil {
		resource := *getResource[0]
		return resource, nil
	}
	return unstructured.Unstructured{}, err
}

// GetKindsFromRule will return the kinds from policy match block
func GetKindsFromRule(rule kyvernov1.Rule, client dclient.Interface) (map[schema.GroupVersionKind]bool, map[schema.GroupVersionKind]Subresource) {
	resourceTypesMap := make(map[schema.GroupVersionKind]bool)
	subresourceMap := make(map[schema.GroupVersionKind]Subresource)
	for _, kind := range rule.MatchResources.Kinds {
		addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
	}
	if rule.MatchResources.Any != nil {
		for _, resFilter := range rule.MatchResources.Any {
			for _, kind := range resFilter.ResourceDescription.Kinds {
				addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
			}
		}
	}
	if rule.MatchResources.All != nil {
		for _, resFilter := range rule.MatchResources.All {
			for _, kind := range resFilter.ResourceDescription.Kinds {
				addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
			}
		}
	}
	return resourceTypesMap, subresourceMap
}

func getKindsFromValidatingAdmissionRule(rule admissionregistrationv1.Rule, client dclient.Interface) (map[schema.GroupVersionKind]bool, map[schema.GroupVersionKind]Subresource, error) {
	resourceTypesMap := make(map[schema.GroupVersionKind]bool)
	subresourceMap := make(map[schema.GroupVersionKind]Subresource)

	group := rule.APIGroups[0]
	if group == "" {
		group = "*"
	}
	version := rule.APIVersions[0]

	for _, resource := range rule.Resources {
		var kind, subresource string

		isSubresource := kubeutils.IsSubresource(resource)
		if isSubresource {
			parts := strings.Split(resource, "/")
			kind = cases.Title(language.English, cases.NoLower).String(parts[0])
			kind, _ = strings.CutSuffix(kind, "s")

			subresource = parts[1]
		} else {
			resource = cases.Title(language.English, cases.NoLower).String(resource)
			resource, _ = strings.CutSuffix(resource, "s")

			kind = resource
			subresource = ""
		}

		gvrss, err := client.Discovery().FindResources(group, version, kind, subresource)
		if err != nil {
			log.Info("failed to find resource", "kind", kind, "error", err)
			return resourceTypesMap, subresourceMap, err
		}

		for parent, child := range gvrss {
			// The resource is not a subresource
			if parent.SubResource == "" {
				resourceTypesMap[parent.GroupVersionKind()] = true
			} else {
				gvk := schema.GroupVersionKind{
					Group: child.Group, Version: child.Version, Kind: child.Kind,
				}
				subresourceMap[gvk] = Subresource{
					APIResource: child,
					ParentResource: metav1.APIResource{
						Group:   parent.Group,
						Version: parent.Version,
						Kind:    parent.Kind,
						Name:    parent.Resource,
					},
				}
			}
		}
	}

	return resourceTypesMap, subresourceMap, nil
}

func addGVKToResourceTypesMap(kind string, resourceTypesMap map[schema.GroupVersionKind]bool, subresourceMap map[schema.GroupVersionKind]Subresource, client dclient.Interface) {
	group, version, kind, subresource := kubeutils.ParseKindSelector(kind)
	gvrss, err := client.Discovery().FindResources(group, version, kind, subresource)
	if err != nil {
		log.Info("failed to find resource", "kind", kind, "error", err)
		return
	}
	for parent, child := range gvrss {
		// The resource is not a subresource
		if parent.SubResource == "" {
			resourceTypesMap[parent.GroupVersionKind()] = true
		} else {
			gvk := schema.GroupVersionKind{
				Group: child.Group, Version: child.Version, Kind: child.Kind,
			}
			subresourceMap[gvk] = Subresource{
				APIResource: child,
				ParentResource: metav1.APIResource{
					Group:   parent.Group,
					Version: parent.Version,
					Kind:    parent.Kind,
					Name:    parent.Resource,
				},
			}
		}
	}
}
