// Copyright (c) 2022 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package lifecycle

import (
	
	
	
	
	
	
	
	

	
	
	
	
)

// Reflection types for each of the supported hook function signatures. These
// are used in cases in which the Callable constraint matches a user-defined
// function type that cannot be converted to an underlying function type with
// a conventional conversion or type switch.
var (
	_reflFunc             = reflect.TypeOf(Func(nil))
	_reflErrorFunc        = reflect.TypeOf(ErrorFunc(nil))
	_reflContextFunc      = reflect.TypeOf(ContextFunc(nil))
	_reflContextErrorFunc = reflect.TypeOf(ContextErrorFunc(nil))
)

// Discrete function signatures that are allowed as part of a [Callable].
type (
	// A Func can be converted to a ContextErrorFunc.
	Func = func()
	// An ErrorFunc can be converted to a ContextErrorFunc.
	ErrorFunc = func() error
	// A ContextFunc can be converted to a ContextErrorFunc.
	ContextFunc = func(context.Context)
	// A ContextErrorFunc is used as a [Hook.OnStart] or [Hook.OnStop]
	// function.
	ContextErrorFunc = func(context.Context) error
)

// A Callable is a constraint that matches functions that are, or can be
// converted to, functions suitable for a Hook.
//
// Callable must be identical to [fx.HookFunc].
type Callable interface {
	~Func | ~ErrorFunc | ~ContextFunc | ~ContextErrorFunc
}

// Wrap wraps x into a ContextErrorFunc suitable for a Hook.
func [ Callable]( ) (ContextErrorFunc, string) {
	if  == nil {
		return nil, ""
	}

	switch fn := any().(type) {
	case Func:
		return func(context.Context) error {
			()
			return nil
		}, fxreflect.FuncName()
	case ErrorFunc:
		return func(context.Context) error {
			return ()
		}, fxreflect.FuncName()
	case ContextFunc:
		return func( context.Context) error {
			()
			return nil
		}, fxreflect.FuncName()
	case ContextErrorFunc:
		return , fxreflect.FuncName()
	}

	// Since (1) we're already using reflect in Fx, (2) we're not particularly
	// concerned with performance, and (3) unsafe would require discrete build
	// targets for appengine (etc), just use reflect to convert user-defined
	// function types to their underlying function types and then call Wrap
	// again with the converted value.
	 := reflect.ValueOf()
	switch {
	case .CanConvert(_reflFunc):
		return (.Convert(_reflFunc).Interface().(Func))
	case .CanConvert(_reflErrorFunc):
		return (.Convert(_reflErrorFunc).Interface().(ErrorFunc))
	case .CanConvert(_reflContextFunc):
		return (.Convert(_reflContextFunc).Interface().(ContextFunc))
	default:
		// Is already convertible to ContextErrorFunc.
		return (.Convert(_reflContextErrorFunc).Interface().(ContextErrorFunc))
	}
}

// A Hook is a pair of start and stop callbacks, either of which can be nil,
// plus a string identifying the supplier of the hook.
type Hook struct {
	OnStart     func(context.Context) error
	OnStop      func(context.Context) error
	OnStartName string
	OnStopName  string

	callerFrame fxreflect.Frame
}

type appState int

const (
	stopped appState = iota
	starting
	incompleteStart
	started
	stopping
)

func ( appState) () string {
	switch  {
	case stopped:
		return "stopped"
	case starting:
		return "starting"
	case incompleteStart:
		return "incompleteStart"
	case started:
		return "started"
	case stopping:
		return "stopping"
	default:
		return "invalidState"
	}
}

// Lifecycle coordinates application lifecycle hooks.
type Lifecycle struct {
	clock        fxclock.Clock
	logger       fxevent.Logger
	state        appState
	hooks        []Hook
	numStarted   int
	startRecords HookRecords
	stopRecords  HookRecords
	runningHook  Hook
	mu           sync.Mutex
}

// New constructs a new Lifecycle.
func ( fxevent.Logger,  fxclock.Clock) *Lifecycle {
	return &Lifecycle{logger: , clock: }
}

// Append adds a Hook to the lifecycle.
func ( *Lifecycle) ( Hook) {
	// Save the caller's stack frame to report file/line number.
	if  := fxreflect.CallerStack(2, 0); len() > 0 {
		.callerFrame = [0]
	}
	.hooks = append(.hooks, )
}

// Start runs all OnStart hooks, returning immediately if it encounters an
// error.
func ( *Lifecycle) ( context.Context) error {
	if  == nil {
		return errors.New("called OnStart with nil context")
	}

	.mu.Lock()
	if .state != stopped {
		defer .mu.Unlock()
		return fmt.Errorf("attempted to start lifecycle when in state: %v", .state)
	}
	.numStarted = 0
	.state = starting

	.startRecords = make(HookRecords, 0, len(.hooks))
	.mu.Unlock()

	 := incompleteStart
	defer func() {
		.mu.Lock()
		.state = 
		.mu.Unlock()
	}()

	for ,  := range .hooks {
		// if ctx has cancelled, bail out of the loop.
		if  := .Err();  != nil {
			return 
		}

		if .OnStart != nil {
			.mu.Lock()
			.runningHook = 
			.mu.Unlock()

			,  := .runStartHook(, )
			if  != nil {
				return 
			}

			.mu.Lock()
			.startRecords = append(.startRecords, HookRecord{
				CallerFrame: .callerFrame,
				Func:        .OnStart,
				Runtime:     ,
			})
			.mu.Unlock()
		}
		.numStarted++
	}

	 = started
	return nil
}

func ( *Lifecycle) ( context.Context,  Hook) ( time.Duration,  error) {
	 := .OnStartName
	if len() == 0 {
		 = fxreflect.FuncName(.OnStart)
	}

	.logger.LogEvent(&fxevent.OnStartExecuting{
		CallerName:   .callerFrame.Function,
		FunctionName: ,
	})
	defer func() {
		.logger.LogEvent(&fxevent.OnStartExecuted{
			CallerName:   .callerFrame.Function,
			FunctionName: ,
			Runtime:      ,
			Err:          ,
		})
	}()

	 := .clock.Now()
	 = .OnStart()
	return .clock.Since(), 
}

// Stop runs any OnStop hooks whose OnStart counterpart succeeded. OnStop
// hooks run in reverse order.
func ( *Lifecycle) ( context.Context) error {
	if  == nil {
		return errors.New("called OnStop with nil context")
	}

	.mu.Lock()
	if .state != started && .state != incompleteStart && .state != starting {
		defer .mu.Unlock()
		return nil
	}
	.state = stopping
	.mu.Unlock()

	defer func() {
		.mu.Lock()
		.state = stopped
		.mu.Unlock()
	}()

	.mu.Lock()
	.stopRecords = make(HookRecords, 0, .numStarted)
	// Take a snapshot of hook state to avoid races.
	 := .hooks[:]
	 := .numStarted
	.mu.Unlock()

	// Run backward from last successful OnStart.
	var  []error
	for ;  > 0; -- {
		if  := .Err();  != nil {
			return 
		}
		 := [-1]
		if .OnStop == nil {
			continue
		}

		.mu.Lock()
		.runningHook = 
		.mu.Unlock()

		,  := .runStopHook(, )
		if  != nil {
			// For best-effort cleanup, keep going after errors.
			 = append(, )
		}

		.mu.Lock()
		.stopRecords = append(.stopRecords, HookRecord{
			CallerFrame: .callerFrame,
			Func:        .OnStop,
			Runtime:     ,
		})
		.mu.Unlock()
	}

	return multierr.Combine(...)
}

func ( *Lifecycle) ( context.Context,  Hook) ( time.Duration,  error) {
	 := .OnStopName
	if len() == 0 {
		 = fxreflect.FuncName(.OnStop)
	}

	.logger.LogEvent(&fxevent.OnStopExecuting{
		CallerName:   .callerFrame.Function,
		FunctionName: ,
	})
	defer func() {
		.logger.LogEvent(&fxevent.OnStopExecuted{
			CallerName:   .callerFrame.Function,
			FunctionName: ,
			Runtime:      ,
			Err:          ,
		})
	}()

	 := .clock.Now()
	 = .OnStop()
	return .clock.Since(), 
}

// RunningHookCaller returns the name of the hook that was running when a Start/Stop
// hook timed out.
func ( *Lifecycle) () string {
	.mu.Lock()
	defer .mu.Unlock()
	return .runningHook.callerFrame.Function
}

// HookRecord keeps track of each Hook's execution time, the caller that appended the Hook, and function that ran as the Hook.
type HookRecord struct {
	CallerFrame fxreflect.Frame             // stack frame of the caller
	Func        func(context.Context) error // function that ran as sanitized name
	Runtime     time.Duration               // how long the hook ran
}

// HookRecords is a Stringer wrapper of HookRecord slice.
type HookRecords []HookRecord

func ( HookRecords) () int {
	return len()
}

func ( HookRecords) (,  int) bool {
	// Sort by runtime, greater ones at top.
	return [].Runtime > [].Runtime
}

func ( HookRecords) (,  int) {
	[], [] = [], []
}

// Used for logging startup errors.
func ( HookRecords) () string {
	var  strings.Builder
	for ,  := range  {
		fmt.Fprintf(&, "%s took %v from %s",
			fxreflect.FuncName(.Func), .Runtime, .CallerFrame)
	}
	return .String()
}

// Format implements fmt.Formatter to handle "%+v".
func ( HookRecords) ( fmt.State,  rune) {
	if !.Flag('+') {
		// Without %+v, fall back to String().
		io.WriteString(, .String())
		return
	}

	for ,  := range  {
		fmt.Fprintf(, "\n%s took %v from:\n\t%+v",
			fxreflect.FuncName(.Func),
			.Runtime,
			.CallerFrame)
	}
	fmt.Fprintf(, "\n")
}