package dao

import (
	"context"
	"errors"
	"sync"
	"time"

	"github.com/derailed/k9s/internal"
	"github.com/derailed/k9s/internal/client"
	"github.com/rs/zerolog/log"
)

// RefScanner represents a resource reference scanner.
type RefScanner interface {
	// Init initializes the scanner
	Init(Factory, client.GVR)
	// Scan scan the resource for references.
	Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error)
	ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error)
}

// Ref represents a resource reference.
type Ref struct {
	GVR string
	FQN string
}

// Refs represents a collection of resource references.
type Refs []Ref

var (
	_ RefScanner = (*Deployment)(nil)
	_ RefScanner = (*StatefulSet)(nil)
	_ RefScanner = (*DaemonSet)(nil)
	_ RefScanner = (*Job)(nil)
	_ RefScanner = (*CronJob)(nil)
	_ RefScanner = (*Pod)(nil)
)

func scanners() map[string]RefScanner {
	return map[string]RefScanner{
		"apps/v1/deployments":    &Deployment{},
		"apps/v1/statefulsets":   &StatefulSet{},
		"apps/v1/daemonsets":     &DaemonSet{},
		"batch/v1/jobs":          &Job{},
		"batch/v1beta1/cronjobs": &CronJob{},
		"v1/pods":                &Pod{},
	}
}

// ScanForRefs scans cluster resources for resource references.
func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
	defer func(t time.Time) {
		log.Debug().Msgf("Cluster Scan %v", time.Since(t))
	}(time.Now())

	gvr, ok := ctx.Value(internal.KeyGVR).(string)
	if !ok {
		return nil, errors.New("expecting context GVR")
	}
	fqn, ok := ctx.Value(internal.KeyPath).(string)
	if !ok {
		return nil, errors.New("expecting context Path")
	}
	wait, ok := ctx.Value(internal.KeyWait).(bool)
	if !ok {
		log.Error().Msgf("expecting Context Wait Key")
	}

	ss := scanners()
	var wg sync.WaitGroup
	wg.Add(len(ss))
	out := make(chan Refs)
	for k, s := range ss {
		go func(ctx context.Context, kind string, s RefScanner, out chan Refs, wait bool) {
			defer wg.Done()
			s.Init(f, client.NewGVR(kind))
			refs, err := s.Scan(ctx, gvr, fqn, wait)
			if err != nil {
				log.Error().Err(err).Msgf("scan failed for %T", s)
				return
			}
			select {
			case out <- refs:
			case <-ctx.Done():
				return
			}
		}(ctx, k, s, out, wait)
	}

	go func() {
		wg.Wait()
		close(out)
	}()

	res := make(Refs, 0, 10)
	for refs := range out {
		res = append(res, refs...)
	}

	return res, nil
}

// ScanForSARefs scans cluster resources for serviceaccount refs.
func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {
	defer func(t time.Time) {
		log.Debug().Msgf("SA Cluster Scan %v", time.Since(t))
	}(time.Now())

	fqn, ok := ctx.Value(internal.KeyPath).(string)
	if !ok {
		return nil, errors.New("expecting context Path")
	}
	wait, ok := ctx.Value(internal.KeyWait).(bool)
	if !ok {
		return nil, errors.New("expecting context Wait")
	}

	ss := scanners()
	var wg sync.WaitGroup
	wg.Add(len(ss))
	out := make(chan Refs)
	for k, s := range ss {
		go func(ctx context.Context, kind string, s RefScanner, out chan Refs, wait bool) {
			defer wg.Done()
			s.Init(f, client.NewGVR(kind))
			refs, err := s.ScanSA(ctx, fqn, wait)
			if err != nil {
				log.Error().Err(err).Msgf("scan failed for %T", s)
				return
			}
			select {
			case out <- refs:
			case <-ctx.Done():
				return
			}
		}(ctx, k, s, out, wait)
	}

	go func() {
		wg.Wait()
		close(out)
	}()

	res := make(Refs, 0, 10)
	for refs := range out {
		res = append(res, refs...)
	}

	return res, nil
}
