package fileutil

import (
	"context"
	"sync"

	"github.com/fsnotify/fsnotify"
	"github.com/rs/zerolog"
	"namespacelabs.dev/go-filenotify"

	"github.com/pomerium/pomerium/internal/chanutil"
	"github.com/pomerium/pomerium/internal/log"
	"github.com/pomerium/pomerium/internal/signal"
)

// A Watcher watches files for changes.
type Watcher struct {
	*signal.Signal

	mu             sync.Mutex
	watching       map[string]struct{}
	eventWatcher   filenotify.FileWatcher
	pollingWatcher filenotify.FileWatcher
}

// NewWatcher creates a new Watcher.
func NewWatcher() *Watcher {
	return &Watcher{
		Signal:   signal.New(),
		watching: make(map[string]struct{}),
	}
}

// Add adds a new watch.
func (watcher *Watcher) Add(filePath string) {
	watcher.mu.Lock()
	defer watcher.mu.Unlock()

	// already watching
	if _, ok := watcher.watching[filePath]; ok {
		return
	}

	ctx := log.WithContext(context.Background(), func(c zerolog.Context) zerolog.Context {
		return c.Str("watch_file", filePath)
	})
	watcher.initLocked(ctx)

	if watcher.eventWatcher != nil {
		if err := watcher.eventWatcher.Add(filePath); err != nil {
			log.Error(ctx).Msg("fileutil/watcher: failed to watch file with event-based file watcher")
		}
	}

	if watcher.pollingWatcher != nil {
		if err := watcher.pollingWatcher.Add(filePath); err != nil {
			log.Error(ctx).Msg("fileutil/watcher: failed to watch file with polling-based file watcher")
		}
	}
}

// Clear removes all watches.
func (watcher *Watcher) Clear() {
	watcher.mu.Lock()
	defer watcher.mu.Unlock()

	if w := watcher.eventWatcher; w != nil {
		_ = watcher.pollingWatcher.Close()
		watcher.eventWatcher = nil
	}

	if w := watcher.pollingWatcher; w != nil {
		_ = watcher.pollingWatcher.Close()
		watcher.pollingWatcher = nil
	}

	watcher.watching = make(map[string]struct{})
}

func (watcher *Watcher) initLocked(ctx context.Context) {
	if watcher.eventWatcher != nil || watcher.pollingWatcher != nil {
		return
	}

	if watcher.eventWatcher == nil {
		var err error
		watcher.eventWatcher, err = filenotify.NewEventWatcher()
		if err != nil {
			log.Error(ctx).Msg("fileutil/watcher: failed to create event-based file watcher")
		}
	}
	if watcher.pollingWatcher == nil {
		watcher.pollingWatcher = filenotify.NewPollingWatcher(nil)
	}

	var errors <-chan error = watcher.pollingWatcher.Errors()          //nolint
	var events <-chan fsnotify.Event = watcher.pollingWatcher.Events() //nolint

	if watcher.eventWatcher != nil {
		errors = chanutil.Merge(errors, watcher.eventWatcher.Errors())
		events = chanutil.Merge(events, watcher.eventWatcher.Events())
	}

	// log errors
	go func() {
		for err := range errors {
			log.Error(ctx).Err(err).Msg("fileutil/watcher: file notification error")
		}
	}()

	// handle events
	go func() {
		for evts := range chanutil.Batch(events) {
			for _, evt := range evts {
				log.Info(ctx).Str("name", evt.Name).Str("op", evt.Op.String()).Msg("fileutil/watcher: file notification event")
			}
			watcher.Broadcast(ctx)
		}
	}()
}