package config

import (
	"context"
	"path/filepath"
	"sync"
	"sync/atomic"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/rs/zerolog"
)

// Manager owns the live SystemConfig and notifies subscribers on hot-reload.
// Construct one with NewManager; the underlying fsnotify watcher is started
// immediately and runs until Stop is called.
//
// On validation failure during reload, the previous (valid) SystemConfig
// remains in effect and the failure is logged as a structured event.
type Manager struct {
	dir       string
	logger    zerolog.Logger
	current   atomic.Pointer[SystemConfig]
	listeners []func(*SystemConfig)
	mu        sync.Mutex

	watcher *fsnotify.Watcher
	stop    context.CancelFunc
	wg      sync.WaitGroup

	// debounceWindow coalesces rapid bursts of fsnotify events (editors often
	// fire many WRITE events in a row when saving). Tunable for tests.
	debounceWindow time.Duration
}

// NewManager loads the initial config from dir, starts watching dir for
// changes, and returns the Manager. The caller is responsible for calling
// Stop() at shutdown.
func NewManager(dir string, logger zerolog.Logger) (*Manager, error) {
	initial, err := LoadFromDir(dir)
	if err != nil {
		return nil, err
	}

	w, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}
	if err := w.Add(dir); err != nil {
		_ = w.Close()
		return nil, err
	}

	ctx, cancel := context.WithCancel(context.Background())
	m := &Manager{
		dir:            dir,
		logger:         logger.With().Str("component", "config").Logger(),
		watcher:        w,
		stop:           cancel,
		debounceWindow: 200 * time.Millisecond,
	}
	m.current.Store(initial)

	m.wg.Add(1)
	go m.runWatch(ctx)
	return m, nil
}

// Current returns the most recently validated SystemConfig. The returned
// pointer is treated as immutable by all callers; reload replaces it
// atomically.
func (m *Manager) Current() *SystemConfig {
	return m.current.Load()
}

// OnReload registers a callback fired after each successful reload. The
// callback runs synchronously in the watch goroutine, so it must return quickly
// or do its own dispatch.
func (m *Manager) OnReload(f func(*SystemConfig)) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.listeners = append(m.listeners, f)
}

// Stop terminates the watch goroutine and releases the underlying fsnotify
// watcher. Safe to call more than once.
func (m *Manager) Stop() {
	m.stop()
	_ = m.watcher.Close()
	m.wg.Wait()
}

func (m *Manager) runWatch(ctx context.Context) {
	defer m.wg.Done()

	var pending *time.Timer
	fire := func() {
		m.reload()
	}

	for {
		select {
		case <-ctx.Done():
			if pending != nil {
				pending.Stop()
			}
			return

		case ev, ok := <-m.watcher.Events:
			if !ok {
				return
			}
			// Only react to YAML files inside our directory.
			if filepath.Ext(ev.Name) != ".yaml" && filepath.Ext(ev.Name) != ".yml" {
				continue
			}
			if ev.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename|fsnotify.Remove) == 0 {
				continue
			}
			if pending != nil {
				pending.Stop()
			}
			pending = time.AfterFunc(m.debounceWindow, fire)

		case err, ok := <-m.watcher.Errors:
			if !ok {
				return
			}
			m.logger.Error().Err(err).Msg("fsnotify watcher error")
		}
	}
}

// reload re-reads the directory, validates, and either publishes the new
// config or retains the previous one with a logged error.
func (m *Manager) reload() {
	next, err := LoadFromDir(m.dir)
	if err != nil {
		m.logger.Error().Err(err).Msg("config reload rejected; previous config remains in effect")
		return
	}
	m.current.Store(next)
	m.logger.Info().Msg("config reloaded")

	m.mu.Lock()
	listeners := append([]func(*SystemConfig){}, m.listeners...)
	m.mu.Unlock()
	for _, f := range listeners {
		f(next)
	}
}
