package profile

import (
	"crypto/sha256"
	"log/slog"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
)

// FileWatcher monitors configuration YAML files and runtime config files
// for external changes and triggers a reload of the corresponding in-memory caches.
type FileWatcher struct {
	mu           sync.Mutex
	mgr          *Manager
	watcher      *fsnotify.Watcher
	cancel       chan struct{}
	stopped      bool
	debounce     map[string]*time.Timer
	runtimePaths map[string]bool
	selfWrites   map[string][sha256.Size]byte // exact revisions written by us
}

// NewFileWatcher creates a watcher that monitors profile YAML files in the
// given config directory. On detected changes, it reloads the corresponding
// data in the Manager.
func NewFileWatcher(mgr *Manager) (*FileWatcher, error) {
	w, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}

	fw := &FileWatcher{
		mgr:          mgr,
		watcher:      w,
		cancel:       make(chan struct{}),
		debounce:     make(map[string]*time.Timer),
		runtimePaths: make(map[string]bool),
		selfWrites:   make(map[string][sha256.Size]byte),
	}

	watchedFiles := []string{serversFileName, legacyChannelsFileName, "skills.yaml", mcpServersFileName}
	for _, name := range watchedFiles {
		path := filepath.Join(mgr.configDir, name)
		if err := w.Add(path); err != nil {
			slog.Debug("profile watcher: cannot watch file (may not exist yet)",
				"path", path, "error", err)
		}
	}

	go fw.agentteam()

	return fw, nil
}

// WatchRuntimeConfigs adds runtime config files to the watcher.
// Must be called with the Manager's lock held.
func (fw *FileWatcher) WatchRuntimeConfigs(runtimes map[string]*runtimeconfig.Runtime) {
	fw.mu.Lock()
	defer fw.mu.Unlock()

	paths := getRuntimeConfigPathsFromRuntimes(runtimes)
	for _, path := range paths {
		if fw.runtimePaths[path] {
			continue
		}
		fw.runtimePaths[path] = true
		if err := fw.watcher.Add(path); err != nil {
			slog.Debug("profile watcher: cannot watch runtime config file",
				"path", path, "error", err)
		}
	}
}

// SuppressRevision records the exact content revision written by ourselves.
// A later external write is never suppressed merely because it happened
// inside a time window.
func (fw *FileWatcher) SuppressRevision(path string, revision [sha256.Size]byte) {
	fw.mu.Lock()
	fw.selfWrites[path] = revision
	fw.mu.Unlock()
}

// Close stops the watcher and releases resources.
func (fw *FileWatcher) Close() {
	fw.mu.Lock()
	if fw.stopped {
		fw.mu.Unlock()
		return
	}
	fw.stopped = true
	close(fw.cancel)
	fw.mu.Unlock()

	fw.watcher.Close()
}

func (fw *FileWatcher) agentteam() {
	for {
		select {
		case <-fw.cancel:
			return
		case event, ok := <-fw.watcher.Events:
			if !ok {
				return
			}
			if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
				fw.onFileChanged(event.Name)
			}
		case err, ok := <-fw.watcher.Errors:
			if !ok {
				return
			}
			slog.Debug("profile watcher: error", "error", err)
		}
	}
}

const watchDebounceInterval = 500 * time.Millisecond

func (fw *FileWatcher) onFileChanged(path string) {
	fw.mu.Lock()
	if fw.stopped {
		fw.mu.Unlock()
		return
	}

	// Suppress only the exact revision written by ourselves. If an external H2
	// replaced it before this event is handled, keep the event and re-evaluate.
	if revision, ok := fw.selfWrites[path]; ok {
		if data, err := os.ReadFile(path); err == nil && sha256.Sum256(data) == revision {
			delete(fw.selfWrites, path)
			fw.mu.Unlock()
			return
		}
		delete(fw.selfWrites, path)
	}

	if timer, ok := fw.debounce[path]; ok {
		timer.Stop()
	}

	fw.debounce[path] = time.AfterFunc(watchDebounceInterval, func() {
		fw.handleReload(path)
	})

	fw.mu.Unlock()
}

func (fw *FileWatcher) handleReload(path string) {
	fw.mu.Lock()
	if fw.stopped {
		fw.mu.Unlock()
		return
	}
	isRuntime := fw.runtimePaths[path]
	fw.mu.Unlock()

	if isRuntime {
		slog.Debug("profile watcher: runtime config changed, auto-detect disabled (use dashboard)",
			"path", path)
		return
	}

	base := filepath.Base(path)
	switch base {
	case serversFileName:
		slog.Info("profile watcher: servers.yaml changed externally, reloading")
		if err := fw.mgr.ReloadServersChecked(); err != nil {
			slog.Warn("profile watcher: servers reload rejected", "error", err)
		}
	case legacyChannelsFileName:
		slog.Info("profile watcher: channels.yaml changed externally, attempting migration")
		if err := fw.mgr.ReloadServersChecked(); err != nil {
			slog.Warn("profile watcher: legacy server reload rejected", "error", err)
		}
	case "skills.yaml":
		slog.Info("profile watcher: skills.yaml changed externally, reloading")
		fw.mgr.ReloadSkills()
	case mcpServersFileName:
		slog.Info("profile watcher: mcp-servers.yaml changed externally, reloading")
		fw.mgr.ReloadMCPServers()
	}
}
