package kubernetes

import (
	"fmt"
	"strings"
	"time"

	"github.com/grafana/tanka/pkg/kubernetes/client"
	"github.com/grafana/tanka/pkg/kubernetes/manifest"
	"github.com/grafana/tanka/pkg/process"
	"github.com/rs/zerolog/log"
)

// ApplyOpts allow set additional parameters for the apply operation
type ApplyOpts client.ApplyOpts

// Apply receives a state object generated using `Reconcile()` and may apply it to the target system
func (k *Kubernetes) Apply(state manifest.List, opts ApplyOpts) error {
	return k.ctl.Apply(state, client.ApplyOpts(opts))
}

// AnnoationLastApplied is the last-applied-configuration annotation used by kubectl
const AnnotationLastApplied = "kubectl.kubernetes.io/last-applied-configuration"

// OrphanedOpts allow to specify additional parameters for finding orphaned resources
type OrphanedOpts struct {
	// Namespace limits the search to a single namespace. Empty string searches all namespaces.
	Namespace string
}

// Orphaned returns previously created resources that are missing from the
// local state. It uses UIDs to safely identify objects.
func (k *Kubernetes) Orphaned(state manifest.List, opts OrphanedOpts) (manifest.List, error) {
	log.Info().Msg("Finding orphaned resources to prune")

	if !k.Env.Spec.InjectLabels {
		return nil, fmt.Errorf(`spec.injectLabels is set to false in your spec.json. Tanka needs to add
a label to your resources to reliably detect which were removed from Jsonnet.
See https://tanka.dev/garbage-collection for more details`)
	}

	apiResources, err := k.ctl.Resources()
	if err != nil {
		return nil, err
	}

	start := time.Now()
	log.Info().Msg("creating list of UIDs from known resources (those generated by the env)")
	uids, err := k.uids(state)
	if err != nil {
		return nil, err
	}
	log.Info().Dur("duration_ms", time.Since(start)).Int("count", len(uids)).Msg("done creating list of UIDs")

	var orphaned manifest.List

	// Find all kinds into a comma separated string that should be checked for pruning:
	// * support LIST operation
	// * when namespace is set, only consider namespaced resources
	kinds := ""
	for _, r := range apiResources {
		if !strings.Contains(r.Verbs, "list") {
			continue
		}
		if opts.Namespace != "" && !r.Namespaced {
			continue
		}

		kinds += "," + r.FQN()
	}
	kinds = strings.TrimPrefix(kinds, ",")

	// get all resources matching our label
	start = time.Now()
	log.Info().Msg("fetching resources previously created by this env")

	nameLabel, err := k.Env.NameLabel()
	if err != nil {
		return nil, err
	}
	matched, err := k.ctl.GetByLabels(opts.Namespace, kinds, map[string]string{
		process.LabelEnvironment: nameLabel,
	})
	if err != nil {
		return nil, err
	}
	log.Info().Dur("duration_ms", time.Since(start)).Int("count", len(matched)).Msg("done fetching previously created resources")

	// filter unknown
	for _, m := range matched {
		resourceLog := log.Debug().Str("kind", m.Kind()).Str("name", m.Metadata().Name())

		// ignore known ones
		if uids[m.Metadata().UID()] {
			resourceLog.Msg("not orphaned. known resource")
			continue
		}

		// skip objects not created explicitly (e.g. pods created from deployments)
		if !k.isDirectlyCreated(m) {
			resourceLog.Msg("not orphaned. resource not directly created")
			continue
		}

		// record and skip from now on
		resourceLog.Msg("orphaned resource")
		orphaned = append(orphaned, m)
		uids[m.Metadata().UID()] = true
	}

	return orphaned, nil
}

func (k *Kubernetes) isDirectlyCreated(manifest manifest.Manifest) bool {
	// Check if created by client-side apply
	if _, ok := manifest.Metadata().Annotations()[AnnotationLastApplied]; ok {
		return true
	}
	// Check if created by server-side apply
	for _, manager := range manifest.Metadata().ManagedFields() {
		managerName := manager.(map[string]interface{})["manager"]
		switch managerName {
		// kubectl-client-side-apply is the default field manager for kubectl
		// kustomize-controller is what Flux reports as the manager
		case "tanka", "kubectl-client-side-apply", "kustomize-controller":
			return true
		}
	}
	return false
}

func (k *Kubernetes) uids(state manifest.List) (map[string]bool, error) {
	uids := make(map[string]bool)

	live, err := k.ctl.GetByState(state, client.GetByStateOpts{
		IgnoreNotFound: true,
	})
	if _, ok := err.(client.ErrorNothingReturned); ok {
		// return empty map of uids when kubectl returns nothing
		return uids, nil
	} else if err != nil {
		return nil, err
	}

	for _, m := range live {
		uids[m.Metadata().UID()] = true
	}

	return uids, nil
}
