// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package controllerinit

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"time"
)

// Runner is something that can be run such as a series of controllers.  Blocks until context is canceled.
type Runner func(context.Context)

// RunnerWrapper takes a Runner and wraps its execution with other logic.  Blocks until context is canceled.
// RunnerWrapper is responsible for the lifetime of the passed in Runner.
type RunnerWrapper func(context.Context, Runner)

// RunnerBuilder is a function that can be used to construct a Runner.
// It is expected to be called in the main go routine since the construction can fail.
type RunnerBuilder func(context.Context) (Runner, error)

// Informer is the subset of SharedInformerFactory needed for starting an informer cache and waiting for it to sync.
type Informer interface {
	Start(stopCh <-chan struct{})
	WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
}

// Prepare returns RunnerBuilder that, when called:
//   1. Starts all provided informers and waits for them sync (and fails if they hang)
//   2. Returns a Runner that combines the Runner and RunnerWrapper passed into Prepare
func Prepare(controllers Runner, controllersWrapper RunnerWrapper, informers ...Informer) RunnerBuilder {
	return func(ctx context.Context) (Runner, error) {
		for _, informer := range informers {
			informer := informer

			informer.Start(ctx.Done())

			// prevent us from blocking forever due to a broken informer
			waitCtx, waitCancel := context.WithTimeout(ctx, time.Minute)
			defer waitCancel()

			// wait until the caches are synced before returning
			status := informer.WaitForCacheSync(waitCtx.Done())

			if unsynced := unsyncedInformers(status); len(unsynced) > 0 {
				return nil, fmt.Errorf("failed to sync informers of %s: %v", anyToFullname(informer), unsynced)
			}
		}

		return func(controllerCtx context.Context) {
			controllersWrapper(controllerCtx, controllers)
		}, nil
	}
}

func unsyncedInformers(status map[reflect.Type]bool) []string {
	if len(status) == 0 {
		return []string{"all:empty"}
	}

	var names []string

	for typ, synced := range status {
		if !synced {
			names = append(names, typeToFullname(typ))
		}
	}

	sort.Strings(names)

	return names
}

func anyToFullname(any interface{}) string {
	typ := reflect.TypeOf(any)
	return typeToFullname(typ)
}

func typeToFullname(typ reflect.Type) string {
	if typ.Kind() == reflect.Ptr {
		typ = typ.Elem()
	}
	return typ.PkgPath() + "." + typ.Name()
}