package path_watcher

import (
	
	
	
	
	
	

	

	
	ss 
	amhelp 
	am 
)

func init() {
	// load .env
	_ = godotenv.Load()

	// am-dbg is required for debugging, go run it
	// go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest
	// amhelp.EnableDebugging(false)
	// amhelp.SetEnvLogLevel(am.LogOps)
}

// PathWatcher watches all dirs in PATH for changes and returns a list
// of executables.
type PathWatcher struct {
	am.ExceptionHandler

	Mach        *am.Machine
	ResultsLock sync.Mutex
	Results     []string
	EnvPath     string

	watcher     *fsnotify.Watcher
	dirCache    map[string][]string
	dirState    map[string]*am.Machine
	ongoing     map[string]context.Context
	lastRefresh map[string]time.Time
}

func ( context.Context) (*PathWatcher, error) {
	 := &PathWatcher{
		EnvPath:     os.Getenv("PATH"),
		dirCache:    make(map[string][]string),
		dirState:    make(map[string]*am.Machine),
		ongoing:     make(map[string]context.Context),
		lastRefresh: make(map[string]time.Time),
	}
	 := &am.Opts{
		Id: "watcher",
	}

	if os.Getenv("YASM_DEBUG") != "" {
		.HandlerTimeout = time.Minute
		.DontPanicToException = true
	}
	.Mach = am.New(, ss.States, )

	 := .Mach.VerifyStates(ss.Names)
	if  != nil {
		return nil, 
	}

	 = .Mach.BindHandlers()
	if  != nil {
		return nil, 
	}

	amhelp.MachDebugEnv(.Mach)

	return , nil
}

func ( *PathWatcher) ( *am.Event) {
	var  error

	.watcher,  = fsnotify.NewWatcher()
	if  != nil {
		.Mach.Remove1(ss.Init, nil)
		.Mach.AddErr(, nil)
	}
}

func ( *PathWatcher) ( *am.Event) {
	.watcher.Close()
}

func ( *PathWatcher) ( *am.Event) {
	 := .EnvPath
	 := strings.Split(, string(os.PathListSeparator))

	// start the loop (bound to this instance)
	 := .Machine().NewStateCtx(ss.Watching)
	go .watchLoop()

	// subscribe
	for ,  := range  {

		// if path doesn't exist, continue
		if ,  := os.Stat(); os.IsNotExist() {
			continue
		}

		// create state per dir
		 := .watcher.Add()
		if  != nil {
			.Machine().AddErr(, nil)
		}

		// create a state for each dir
		 := am.New(, ss.StatesDir, nil)
		 = .VerifyStates(ss.NamesDir)
		if  != nil {
			.Machine().AddErr(, nil)
			continue
		}

		.dirState[] = 

		// schedule a refresh
		.Mach.Add1(ss.Refreshing, am.A{"dirName": })
	}
}

func ( *PathWatcher) ( *am.Event) {
	 := .watcher.WatchList()

	for ,  := range  {
		 := .watcher.Remove()
		if  != nil {
			.Machine().AddErr(, nil)
		}
	}
}

func ( *PathWatcher) ( context.Context) {
	for {
		select {

		case ,  := <-.watcher.Events:
			if ! {
				.Mach.Remove1(ss.Watching, nil)
				return
			}
			.Mach.Add1(ss.ChangeEvent, am.A{
				"fsnotify.Event": ,
			})

		case ,  := <-.watcher.Errors:
			if ! {
				.Mach.Remove1(ss.Watching, nil)
				return
			}
			.Mach.AddErr(, nil)

		case <-.Done():
			// state expired
			return
		}
	}
}

func ( *PathWatcher) ( *am.Event) {
	defer .Machine().Remove1(ss.ChangeEvent, nil)
	 := .Args["fsnotify.Event"].(fsnotify.Event)

	// exe
	 := .Op&fsnotify.Remove == fsnotify.Remove
	if ! {
		,  := isExecutable(.Name)
		if ! ||  != nil {
			return
		}
	}
	 := filepath.Dir(.Name)

	.Mach.Add1(ss.Refreshing, am.A{
		"dirName": ,
	})
}

func ( *PathWatcher) ( *am.Event) {
	.ExceptionHandler.ExceptionState()
}

func ( *PathWatcher) ( *am.Event) bool {
	// validate req params
	,  := .Args["dirName"]
	,  := .Args["dirName"].(string)
	,  := .dirState[]
	 :=  &&  && 
	if ! {
		return false
	}

	// let the debounced refreshes pass
	,  := .Args["isDebounce"].(bool)
	if .Is1(ss.Refreshing) || (.Is1(ss.DirDebounced) && !) {
		return false
	}

	return true
}

func ( *PathWatcher) ( *am.Event) {
	.Mach.Remove1(ss.Refreshing, nil)

	 := .Args["dirName"].(string)
	 := .dirState[]
	// TODO config
	 := time.Second

	// max 1 refresh per second
	 := time.Since(.lastRefresh[])
	 :=  < 
	if .Is1(ss.DirCached) &&  {
		.Mach.Log("Debounce for %s", )
		.Add1(ss.DirDebounced, nil)

		go func() {
			time.Sleep()
			.Mach.Add1(ss.Refreshing, am.A{
				"dirName":    ,
				"isDebounce": true,
			})
		}()

		return
	}

	.Mach.Log("Refreshing execs in %s", )
	.Add1(ss.Refreshing, nil)
	.ongoing[] = .NewStateCtx(ss.Refreshing)
	 := .ongoing[]

	go func() {
		if .Err() != nil {
			return // expired
		}

		,  := listExecutables()
		if  != nil {
			.Machine().AddErr(, nil)
		}

		.Mach.Remove1(ss.Refreshing, am.A{
			"dirName": ,
		})
		.Mach.Add1(ss.Refreshed, am.A{
			"dirName":     ,
			"executables": ,
		})
	}()
}

func ( *PathWatcher) ( *am.Event) bool {
	// GC
	,  := .Args["dirName"]
	if  {
		,  := .Args["dirName"].(string)
		if  {
			delete(.ongoing, )
		}
	}

	// check completions
	 := .Mutation()

	// removing Init is a force shutdown
	 := .Type == am.MutationRemove && .IsCalled(.Mach.Index1(ss.Init))

	return len(.ongoing) == 0 || 
}

func ( *PathWatcher) ( *am.Event) {
	// forced cleanup
	for  := range .ongoing {
		delete(.ongoing, )
	}
}

func ( *PathWatcher) ( *am.Event) bool {
	// validate req params
	,  := .Args["dirName"].(string)
	,  := .Args["executables"].([]string)

	return  && 
}

func ( *PathWatcher) ( *am.Event) {
	.Mach.Remove1(ss.Refreshed, nil)

	 := .Args["dirName"].(string)
	 := .Args["executables"].([]string)
	.dirCache[] = 
	.lastRefresh[] = time.Now()

	// update the per-dir state
	.dirState[].Add(am.S{ss.Refreshed, ss.DirCached}, nil)

	// try to finish the whole refresh
	.Mach.Add1(ss.AllRefreshed, nil)
}

func ( *PathWatcher) ( *am.Event) bool {
	return len(.ongoing) == 0
}

func ( *PathWatcher) ( *am.Event) {
	.ResultsLock.Lock()
	defer .ResultsLock.Unlock()

	for ,  := range .dirCache {
		.Results = append(.Results, ...)
	}
	.Results = uniqueStrings(.Results)
}

func ( *PathWatcher) () {
	.Mach.Add1(ss.Init, nil)
}

func ( *PathWatcher) () {
	.Mach.Remove1(ss.Init, nil)
}

// /// HELPERS /////

func isExecutable( string) (bool, error) {
	,  := os.Stat()
	if  != nil {
		return false, 
	}

	return .Mode().Perm()&0o111 != 0, nil
}

func listExecutables( string) ([]string, error) {
	,  := os.ReadDir()
	if  != nil {
		return nil, 
	}

	var  []string
	for ,  := range  {
		if .IsDir() {
			continue
		}

		 :=  + "/" + .Name()
		,  := isExecutable()
		if  != nil {
			continue
		}

		if  {
			 = append(, .Name())
		}
	}

	return , nil
}

func uniqueStrings( []string) []string {
	 := make(map[string]struct{})
	var  []string

	for ,  := range  {
		if ,  := [];  {
			continue
		}
		[] = struct{}{}
		 = append(, )
	}

	return 
}