// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package dao

import (
	"fmt"
	"sort"
	"strings"
	"sync"

	"github.com/derailed/k9s/internal/client"
	"github.com/rs/zerolog/log"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
)

const (
	crdCat  = "crd"
	k9sCat  = "k9s"
	helmCat = "helm"
)

// MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()

var stdGroups = []string{
	"admissionregistration.k8s.io/v1",
	"admissionregistration.k8s.io/v1beta1",
	"apiextensions.k8s.io/v1",
	"apiextensions.k8s.io/v1beta1",
	"apiregistration.k8s.io/v1",
	"apiregistration.k8s.io/v1beta1",
	"apps/v1",
	"authentication.k8s.io/v1",
	"authentication.k8s.io/v1beta1",
	"authorization.k8s.io/v1",
	"authorization.k8s.io/v1beta1",
	"autoscaling/v1",
	"autoscaling/v2beta1",
	"autoscaling/v2beta2",
	"batch/v1",
	"batch/v1beta1",
	"certificates.k8s.io/v1",
	"certificates.k8s.io/v1beta1",
	"coordination.k8s.io/v1",
	"coordination.k8s.io/v1beta1",
	"discovery.k8s.io/v1beta1",
	"dynatrace.com/v1alpha1",
	"events.k8s.io/v1",
	"extensions/v1beta1",
	"flowcontrol.apiserver.k8s.io/v1beta1",
	"metrics.k8s.io/v1beta1",
	"networking.k8s.io/v1",
	"networking.k8s.io/v1beta1",
	"node.k8s.io/v1",
	"node.k8s.io/v1beta1",
	"policy/v1beta1",
	"rbac.authorization.k8s.io/v1",
	"rbac.authorization.k8s.io/v1beta1",
	"scheduling.k8s.io/v1",
	"scheduling.k8s.io/v1beta1",
	"storage.k8s.io/v1",
	"storage.k8s.io/v1beta1",
	"v1",
}

// Meta represents available resource metas.
type Meta struct {
	resMetas ResourceMetas
	mx       sync.RWMutex
}

// NewMeta returns a resource meta.
func NewMeta() *Meta {
	return &Meta{resMetas: make(ResourceMetas)}
}

// AccessorFor returns a client accessor for a resource if registered.
// Otherwise it returns a generic accessor.
// Customize here for non resource types or types with metrics or logs.
func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
	m := Accessors{
		client.NewGVR("contexts"):               &Context{},
		client.NewGVR("containers"):             &Container{},
		client.NewGVR("scans"):                  &ImageScan{},
		client.NewGVR("screendumps"):            &ScreenDump{},
		client.NewGVR("benchmarks"):             &Benchmark{},
		client.NewGVR("portforwards"):           &PortForward{},
		client.NewGVR("v1/services"):            &Service{},
		client.NewGVR("v1/pods"):                &Pod{},
		client.NewGVR("v1/nodes"):               &Node{},
		client.NewGVR("apps/v1/deployments"):    &Deployment{},
		client.NewGVR("apps/v1/daemonsets"):     &DaemonSet{},
		client.NewGVR("apps/v1/statefulsets"):   &StatefulSet{},
		client.NewGVR("apps/v1/replicasets"):    &ReplicaSet{},
		client.NewGVR("batch/v1/cronjobs"):      &CronJob{},
		client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
		client.NewGVR("batch/v1/jobs"):          &Job{},
		client.NewGVR("v1/namespaces"):          &Namespace{},
		client.NewGVR("popeye"):                 &Popeye{},
		client.NewGVR("helm"):                   &HelmChart{},
		client.NewGVR("dir"):                    &Dir{},
	}

	r, ok := m[gvr]
	if !ok {
		r = new(Generic)
		log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr)
	}
	r.Init(f, gvr)

	return r, nil
}

// RegisterMeta registers a new resource meta object.
func (m *Meta) RegisterMeta(gvr string, res metav1.APIResource) {
	m.mx.Lock()
	defer m.mx.Unlock()

	m.resMetas[client.NewGVR(gvr)] = res
}

// AllGVRs returns all cluster resources.
func (m *Meta) AllGVRs() client.GVRs {
	m.mx.RLock()
	defer m.mx.RUnlock()

	kk := make(client.GVRs, 0, len(m.resMetas))
	for k := range m.resMetas {
		kk = append(kk, k)
	}
	sort.Sort(kk)

	return kk
}

// IsCRD checks if resource represents a CRD
func IsCRD(r metav1.APIResource) bool {
	for _, c := range r.Categories {
		if c == crdCat {
			return true
		}
	}
	return false
}

// MetaFor returns a resource metadata for a given gvr.
func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
	m.mx.RLock()
	defer m.mx.RUnlock()

	meta, ok := m.resMetas[gvr]
	if !ok {
		return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
	}
	return meta, nil
}

// IsK8sMeta checks for non resource meta.
func IsK8sMeta(m metav1.APIResource) bool {
	for _, c := range m.Categories {
		if c == k9sCat || c == helmCat {
			return false
		}
	}

	return true
}

// IsK9sMeta checks for non resource meta.
func IsK9sMeta(m metav1.APIResource) bool {
	for _, c := range m.Categories {
		if c == k9sCat {
			return true
		}
	}

	return false
}

// LoadResources hydrates server preferred+CRDs resource metadata.
func (m *Meta) LoadResources(f Factory) error {
	m.mx.Lock()
	defer m.mx.Unlock()

	m.resMetas = make(ResourceMetas, 100)
	if err := loadPreferred(f, m.resMetas); err != nil {
		return err
	}
	loadNonResource(m.resMetas)
	loadCRDs(f, m.resMetas)

	return nil
}

// BOZO!! Need countermeasures for direct commands!
func loadNonResource(m ResourceMetas) {
	loadK9s(m)
	loadRBAC(m)
	loadHelm(m)
}

func loadK9s(m ResourceMetas) {
	m[client.NewGVR("pulses")] = metav1.APIResource{
		Name:         "pulses",
		Kind:         "Pulse",
		SingularName: "pulses",
		ShortNames:   []string{"hz", "pu"},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("dir")] = metav1.APIResource{
		Name:         "dir",
		Kind:         "Dir",
		SingularName: "dir",
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("xrays")] = metav1.APIResource{
		Name:         "xray",
		Kind:         "XRays",
		SingularName: "xray",
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("references")] = metav1.APIResource{
		Name:         "references",
		Kind:         "References",
		SingularName: "reference",
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("aliases")] = metav1.APIResource{
		Name:         "aliases",
		Kind:         "Aliases",
		SingularName: "alias",
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("popeye")] = metav1.APIResource{
		Name:         "popeye",
		Kind:         "Popeye",
		SingularName: "popeye",
		Namespaced:   true,
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("sanitizer")] = metav1.APIResource{
		Name:         "sanitizer",
		Kind:         "Sanitizer",
		SingularName: "sanitizer",
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("contexts")] = metav1.APIResource{
		Name:         "contexts",
		Kind:         "Contexts",
		SingularName: "context",
		ShortNames:   []string{"ctx"},
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("screendumps")] = metav1.APIResource{
		Name:         "screendumps",
		Kind:         "ScreenDumps",
		SingularName: "screendump",
		ShortNames:   []string{"sd"},
		Verbs:        []string{"delete"},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("benchmarks")] = metav1.APIResource{
		Name:         "benchmarks",
		Kind:         "Benchmarks",
		SingularName: "benchmark",
		ShortNames:   []string{"be"},
		Verbs:        []string{"delete"},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("portforwards")] = metav1.APIResource{
		Name:         "portforwards",
		Namespaced:   true,
		Kind:         "PortForwards",
		SingularName: "portforward",
		ShortNames:   []string{"pf"},
		Verbs:        []string{"delete"},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("containers")] = metav1.APIResource{
		Name:         "containers",
		Kind:         "Containers",
		SingularName: "container",
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
	m[client.NewGVR("scans")] = metav1.APIResource{
		Name:         "scans",
		Kind:         "Scans",
		SingularName: "scan",
		Verbs:        []string{},
		Categories:   []string{k9sCat},
	}
}

func loadHelm(m ResourceMetas) {
	m[client.NewGVR("helm")] = metav1.APIResource{
		Name:       "chart",
		Kind:       "Chart",
		Namespaced: true,
		Verbs:      []string{"delete"},
		Categories: []string{helmCat},
	}
	m[client.NewGVR("helm-history")] = metav1.APIResource{
		Name:       "history",
		Kind:       "History",
		Namespaced: true,
		Verbs:      []string{"delete"},
		Categories: []string{k9sCat},
	}
}

func loadRBAC(m ResourceMetas) {
	m[client.NewGVR("rbac")] = metav1.APIResource{
		Name:       "rbacs",
		Kind:       "Rules",
		Categories: []string{k9sCat},
	}
	m[client.NewGVR("policy")] = metav1.APIResource{
		Name:       "policies",
		Kind:       "Rules",
		Namespaced: true,
		Categories: []string{k9sCat},
	}
	m[client.NewGVR("users")] = metav1.APIResource{
		Name:       "users",
		Kind:       "User",
		Categories: []string{k9sCat},
	}
	m[client.NewGVR("groups")] = metav1.APIResource{
		Name:       "groups",
		Kind:       "Group",
		Categories: []string{k9sCat},
	}
}

func loadPreferred(f Factory, m ResourceMetas) error {
	if f.Client() == nil || !f.Client().ConnectionOK() {
		log.Error().Msgf("Load cluster resources - No API server connection")
		return nil
	}

	dial, err := f.Client().CachedDiscovery()
	if err != nil {
		return err
	}
	rr, err := dial.ServerPreferredResources()
	if err != nil {
		log.Debug().Err(err).Msgf("Failed to load preferred resources")
	}
	for _, r := range rr {
		for _, res := range r.APIResources {
			gvr := client.FromGVAndR(r.GroupVersion, res.Name)
			if isDeprecated(gvr) {
				continue
			}
			res.Group, res.Version = gvr.G(), gvr.V()
			if res.SingularName == "" {
				res.SingularName = strings.ToLower(res.Kind)
			}
			if !isStandardGroup(res.Group) {
				res.Categories = append(res.Categories, crdCat)
			}
			m[gvr] = res
		}
	}

	return nil
}

func isStandardGroup(r string) bool {
	for _, res := range stdGroups {
		if strings.Index(res, r) == 0 {
			return true
		}
	}

	return false
}

var deprecatedGVRs = map[client.GVR]struct{}{
	client.NewGVR("extensions/v1beta1/ingresses"): {},
}

func isDeprecated(gvr client.GVR) bool {
	_, ok := deprecatedGVRs[gvr]
	return ok
}

func loadCRDs(f Factory, m ResourceMetas) {
	if f.Client() == nil || !f.Client().ConnectionOK() {
		return
	}
	const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
	oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
	if err != nil {
		log.Warn().Err(err).Msgf("Fail CRDs load")
		return
	}

	for _, o := range oo {
		meta, errs := extractMeta(o)
		if len(errs) > 0 {
			log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs))
			continue
		}
		meta.Categories = append(meta.Categories, crdCat)
		gvr := client.NewGVRFromMeta(meta)
		m[gvr] = meta
	}
}

func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
	var (
		m    metav1.APIResource
		errs []error
	)

	crd, ok := o.(*unstructured.Unstructured)
	if !ok {
		return m, append(errs, fmt.Errorf("expected unstructured, but got %T", o))
	}

	var spec map[string]interface{}
	spec, errs = extractMap(crd.Object, "spec", errs)

	var meta map[string]interface{}
	meta, errs = extractMap(crd.Object, "metadata", errs)
	m.Name, errs = extractStr(meta, "name", errs)

	m.Group, errs = extractStr(spec, "group", errs)
	versions, errs := extractSlice(spec, "versions", errs)
	if len(versions) > 0 {
		m.Version = versions[0]
	}

	var scope string
	scope, errs = extractStr(spec, "scope", errs)

	m.Namespaced = isNamespaced(scope)

	var names map[string]interface{}
	names, errs = extractMap(spec, "names", errs)
	m.Kind, errs = extractStr(names, "kind", errs)
	m.SingularName, errs = extractStr(names, "singular", errs)
	m.Name, errs = extractStr(names, "plural", errs)
	m.ShortNames, errs = extractSlice(names, "shortNames", errs)

	return m, errs
}

func isNamespaced(scope string) bool {
	return scope == "Namespaced"
}

func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) {
	if m[n] == nil {
		return nil, errs
	}

	s, ok := m[n].([]string)
	if ok {
		return s, errs
	}

	ii, ok := m[n].([]interface{})
	if !ok {
		return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m))
	}

	ss := make([]string, 0, len(ii))
	for _, name := range ii {
		switch o := name.(type) {
		case string:
			ss = append(ss, o)
		case map[string]interface{}:
			s, ok := o["name"].(string)
			if ok {
				ss = append(ss, s)
			} else {
				errs = append(errs, fmt.Errorf("unable to find key %q in map", n))
			}
		default:
			errs = append(errs, fmt.Errorf("unknown field type %t for key %q", o, n))
		}
	}

	return ss, errs
}

func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) {
	s, ok := m[n].(string)
	if !ok {
		return s, append(errs, fmt.Errorf("failed to extract string %s", n))
	}
	return s, errs
}

func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) {
	v, ok := m[n].(map[string]interface{})
	if !ok {
		return v, append(errs, fmt.Errorf("failed to extract field %s", n))
	}
	return v, errs
}
