package infra

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"
)

// WatchDirFsnotify crea un watcher recursivo sobre root y todos sus subdirectorios.
// Emite FsEvent al canal devuelto con debounce de 200ms por path (si llegan multiples
// eventos del mismo archivo en la ventana, se emite solo el ultimo).
// Cierra el canal cuando ctx.Done() se dispara.
func WatchDirFsnotify(ctx context.Context, root string) (<-chan FsEvent, error) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, fmt.Errorf("watch_dir_fsnotify: new watcher: %w", err)
	}

	// Anadir root y todos los subdirectorios recursivamente.
	if err := addDirsRecursive(watcher, root); err != nil {
		watcher.Close()
		return nil, fmt.Errorf("watch_dir_fsnotify: add dirs: %w", err)
	}

	ch := make(chan FsEvent, 64)

	go func() {
		defer watcher.Close()
		defer close(ch)

		// Mapa de debounce: path -> (timer, ultimo op)
		type pending struct {
			timer *time.Timer
			op    string
		}
		debounce := make(map[string]*pending)
		const debounceDelay = 200 * time.Millisecond

		for {
			select {
			case <-ctx.Done():
				// Cancelar todos los timers pendientes antes de salir.
				for _, p := range debounce {
					p.timer.Stop()
				}
				return

			case event, ok := <-watcher.Events:
				if !ok {
					return
				}

				op := fsnotifyOpToString(event.Op)
				if op == "" {
					continue
				}

				path := event.Name

				// Si el directorio nuevo fue creado, anadirlo al watcher.
				if event.Op&fsnotify.Create != 0 {
					if info, err := os.Stat(path); err == nil && info.IsDir() {
						if err := watcher.Add(path); err != nil {
							log.Printf("watch_dir_fsnotify: add new dir %s: %v", path, err)
						}
					}
				}

				// Debounce: resetear el timer si ya habia uno para este path.
				if p, exists := debounce[path]; exists {
					p.timer.Stop()
					p.op = op
					p.timer.Reset(debounceDelay)
				} else {
					p = &pending{op: op}
					p.timer = time.AfterFunc(debounceDelay, func() {
						select {
						case ch <- FsEvent{Path: path, Op: p.op}:
						case <-ctx.Done():
						}
						delete(debounce, path)
					})
					debounce[path] = p
				}

			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Printf("watch_dir_fsnotify: watcher error: %v", err)
			}
		}
	}()

	return ch, nil
}

// addDirsRecursive anade root y todos sus subdirectorios al watcher.
// Retorna error si root no existe o no es accesible.
func addDirsRecursive(watcher *fsnotify.Watcher, root string) error {
	if _, err := os.Stat(root); err != nil {
		return fmt.Errorf("root dir %s: %w", root, err)
	}
	return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // ignora errores de acceso en subdirs
		}
		if info.IsDir() {
			return watcher.Add(path)
		}
		return nil
	})
}

// fsnotifyOpToString convierte fsnotify.Op al string canonico del registry.
// Retorna "" para operaciones no mapeadas (CHMOD, etc.).
func fsnotifyOpToString(op fsnotify.Op) string {
	switch {
	case op&fsnotify.Create != 0:
		return "create"
	case op&fsnotify.Write != 0:
		return "write"
	case op&fsnotify.Remove != 0:
		return "remove"
	case op&fsnotify.Rename != 0:
		return "rename"
	default:
		return ""
	}
}
