package util

import (
	"bufio"
	"fmt"
	"log"
	"net/url"
	"os"
	"strings"

	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"

	"github.com/argoproj/argo-cd/v3/pkg/apis/application"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/argoproj/argo-cd/v3/util/config"
	"github.com/argoproj/argo-cd/v3/util/gpg"
)

type ProjectOpts struct {
	Description                string
	destinations               []string
	destinationServiceAccounts []string
	Sources                    []string
	SignatureKeys              []string
	SourceNamespaces           []string

	orphanedResourcesEnabled   bool
	orphanedResourcesWarn      bool
	allowedClusterResources    []string
	deniedClusterResources     []string
	allowedNamespacedResources []string
	deniedNamespacedResources  []string
}

func AddProjFlags(command *cobra.Command, opts *ProjectOpts) {
	command.Flags().StringVarP(&opts.Description, "description", "", "", "Project description")
	command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
		"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
	command.Flags().StringArrayVarP(&opts.Sources, "src", "s", []string{}, "Permitted source repository URL")
	command.Flags().StringSliceVar(&opts.SignatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
	command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
	command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should have a warning condition when orphaned resources detected")
	command.Flags().StringArrayVar(&opts.allowedClusterResources, "allow-cluster-resource", []string{}, "List of allowed cluster level resources, optionally with group and name (e.g. ClusterRole, apiextensions.k8s.io/CustomResourceDefinition, /Namespace/team1-*)")
	command.Flags().StringArrayVar(&opts.deniedClusterResources, "deny-cluster-resource", []string{}, "List of denied cluster level resources, optionally with group and name (e.g. ClusterRole, apiextensions.k8s.io/CustomResourceDefinition, /Namespace/kube-*)")
	command.Flags().StringArrayVar(&opts.allowedNamespacedResources, "allow-namespaced-resource", []string{}, "List of allowed namespaced resources")
	command.Flags().StringArrayVar(&opts.deniedNamespacedResources, "deny-namespaced-resource", []string{}, "List of denied namespaced resources")
	command.Flags().StringSliceVar(&opts.SourceNamespaces, "source-namespaces", []string{}, "List of source namespaces for applications")
	command.Flags().StringArrayVar(&opts.destinationServiceAccounts, "dest-service-accounts", []string{},
		"Destination server, namespace and target service account (e.g. https://192.168.99.100:8443,default,default-sa)")
}

func getGroupKindList(values []string) []metav1.GroupKind {
	var res []metav1.GroupKind
	for _, val := range values {
		if parts := strings.Split(val, "/"); len(parts) == 2 {
			res = append(res, metav1.GroupKind{Group: parts[0], Kind: parts[1]})
		} else if len(parts) == 1 {
			res = append(res, metav1.GroupKind{Kind: parts[0]})
		}
	}
	return res
}

func getClusterResourceRestrictionItemList(values []string) []v1alpha1.ClusterResourceRestrictionItem {
	var res []v1alpha1.ClusterResourceRestrictionItem
	for _, val := range values {
		if parts := strings.Split(val, "/"); len(parts) == 3 {
			res = append(res, v1alpha1.ClusterResourceRestrictionItem{Group: parts[0], Kind: parts[1], Name: parts[2]})
		} else if parts = strings.Split(val, "/"); len(parts) == 2 {
			res = append(res, v1alpha1.ClusterResourceRestrictionItem{Group: parts[0], Kind: parts[1]})
		} else if len(parts) == 1 {
			res = append(res, v1alpha1.ClusterResourceRestrictionItem{Kind: parts[0]})
		}
	}
	return res
}

func (opts *ProjectOpts) GetAllowedClusterResources() []v1alpha1.ClusterResourceRestrictionItem {
	return getClusterResourceRestrictionItemList(opts.allowedClusterResources)
}

func (opts *ProjectOpts) GetDeniedClusterResources() []v1alpha1.ClusterResourceRestrictionItem {
	return getClusterResourceRestrictionItemList(opts.deniedClusterResources)
}

func (opts *ProjectOpts) GetAllowedNamespacedResources() []metav1.GroupKind {
	return getGroupKindList(opts.allowedNamespacedResources)
}

func (opts *ProjectOpts) GetDeniedNamespacedResources() []metav1.GroupKind {
	return getGroupKindList(opts.deniedNamespacedResources)
}

func (opts *ProjectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
	destinations := make([]v1alpha1.ApplicationDestination, 0)
	for _, destStr := range opts.destinations {
		parts := strings.Split(destStr, ",")
		if len(parts) != 2 {
			log.Fatalf("Expected destination of the form: server,namespace. Received: %s", destStr)
		}
		destinations = append(destinations, v1alpha1.ApplicationDestination{
			Server:    parts[0],
			Namespace: parts[1],
		})
	}
	return destinations
}

func (opts *ProjectOpts) GetDestinationServiceAccounts() []v1alpha1.ApplicationDestinationServiceAccount {
	destinationServiceAccounts := make([]v1alpha1.ApplicationDestinationServiceAccount, 0)
	for _, destStr := range opts.destinationServiceAccounts {
		parts := strings.Split(destStr, ",")
		if len(parts) != 3 {
			log.Fatalf("Expected destination service account of the form: server,namespace, defaultServiceAccount. Received: %s", destStr)
		}
		destinationServiceAccounts = append(destinationServiceAccounts, v1alpha1.ApplicationDestinationServiceAccount{
			Server:                parts[0],
			Namespace:             parts[1],
			DefaultServiceAccount: parts[2],
		})
	}
	return destinationServiceAccounts
}

// GetSignatureKeys TODO: Get configured keys and emit warning when a key is specified that is not configured
func (opts *ProjectOpts) GetSignatureKeys() []v1alpha1.SignatureKey {
	signatureKeys := make([]v1alpha1.SignatureKey, 0)
	for _, keyStr := range opts.SignatureKeys {
		if !gpg.IsShortKeyID(keyStr) && !gpg.IsLongKeyID(keyStr) {
			log.Fatalf("'%s' is not a valid GnuPG key ID", keyStr)
		}
		signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
	}
	return signatureKeys
}

func (opts *ProjectOpts) GetSourceNamespaces() []string {
	return opts.SourceNamespaces
}

func GetOrphanedResourcesSettings(flagSet *pflag.FlagSet, opts ProjectOpts) *v1alpha1.OrphanedResourcesMonitorSettings {
	warnChanged := flagSet.Changed("orphaned-resources-warn")
	if opts.orphanedResourcesEnabled || warnChanged {
		settings := v1alpha1.OrphanedResourcesMonitorSettings{}
		if warnChanged {
			settings.Warn = ptr.To(opts.orphanedResourcesWarn)
		}
		return &settings
	}
	return nil
}

func readProjFromStdin(proj *v1alpha1.AppProject) error {
	reader := bufio.NewReader(os.Stdin)
	err := config.UnmarshalReader(reader, &proj)
	if err != nil {
		return fmt.Errorf("unable to read manifest from stdin: %w", err)
	}
	return nil
}

func readProjFromURI(fileURL string, proj *v1alpha1.AppProject) error {
	parsedURL, err := url.ParseRequestURI(fileURL)
	if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") {
		err = config.UnmarshalLocalFile(fileURL, &proj)
	} else {
		err = config.UnmarshalRemoteFile(fileURL, &proj)
	}
	if err != nil {
		return fmt.Errorf("error reading proj from uri: %w", err)
	}
	return nil
}

func SetProjSpecOptions(flags *pflag.FlagSet, spec *v1alpha1.AppProjectSpec, projOpts *ProjectOpts) int {
	visited := 0
	flags.Visit(func(f *pflag.Flag) {
		visited++
		switch f.Name {
		case "description":
			spec.Description = projOpts.Description
		case "dest":
			spec.Destinations = projOpts.GetDestinations()
		case "src":
			spec.SourceRepos = projOpts.Sources
		case "signature-keys":
			spec.SignatureKeys = projOpts.GetSignatureKeys()
		case "allow-cluster-resource":
			spec.ClusterResourceWhitelist = projOpts.GetAllowedClusterResources()
		case "deny-cluster-resource":
			spec.ClusterResourceBlacklist = projOpts.GetDeniedClusterResources()
		case "allow-namespaced-resource":
			spec.NamespaceResourceWhitelist = projOpts.GetAllowedNamespacedResources()
		case "deny-namespaced-resource":
			spec.NamespaceResourceBlacklist = projOpts.GetDeniedNamespacedResources()
		case "source-namespaces":
			spec.SourceNamespaces = projOpts.GetSourceNamespaces()
		case "dest-service-accounts":
			spec.DestinationServiceAccounts = projOpts.GetDestinationServiceAccounts()
		}
	})
	if flags.Changed("orphaned-resources") || flags.Changed("orphaned-resources-warn") {
		spec.OrphanedResources = GetOrphanedResourcesSettings(flags, *projOpts)
		visited++
	}
	return visited
}

func ConstructAppProj(fileURL string, args []string, opts ProjectOpts, c *cobra.Command) (*v1alpha1.AppProject, error) {
	proj := v1alpha1.AppProject{
		TypeMeta: metav1.TypeMeta{
			Kind:       application.AppProjectKind,
			APIVersion: application.Group + "/v1alpha1",
		},
	}
	switch {
	case fileURL == "-":
		// read stdin
		err := readProjFromStdin(&proj)
		if err != nil {
			return nil, err
		}
	case fileURL != "":
		// read uri
		err := readProjFromURI(fileURL, &proj)
		if err != nil {
			return nil, err
		}

		if len(args) == 1 && args[0] != proj.Name {
			return nil, fmt.Errorf("project name '%s' does not match project spec metadata.name '%s'", args[0], proj.Name)
		}
	default:
		// read arguments
		if len(args) == 0 {
			c.HelpFunc()(c, args)
			os.Exit(1)
		}
		proj.Name = args[0]
	}
	SetProjSpecOptions(c.Flags(), &proj.Spec, &opts)
	return &proj, nil
}
