package app

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

	"github.com/fsnotify/fsnotify"
)

const fileWatcherDebounce = 500 * time.Millisecond

type FileWatcher struct {
	watcher  *fsnotify.Watcher
	rootDir  string
	onChange func(path string)
	logger   *slog.Logger
	debounce time.Duration
}

func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, fmt.Errorf("create fsnotify watcher: %w", err)
	}

	fw := &FileWatcher{
		watcher:  watcher,
		rootDir:  rootDir,
		logger:   logger,
		debounce: fileWatcherDebounce,
	}

	if err := fw.addWatches(); err != nil {
		watcher.Close()
		return nil, err
	}

	return fw, nil
}

func (fw *FileWatcher) addWatches() error {
	return filepath.WalkDir(fw.rootDir, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return nil
		}
		if !entry.IsDir() {
			return nil
		}
		if path != fw.rootDir && fw.shouldIgnorePath(path) {
			return filepath.SkipDir
		}
		if err := fw.watcher.Add(path); err != nil {
			fw.logger.Warn("watch directory", "path", path, "error", err)
		}
		return nil
	})
}

func (fw *FileWatcher) addWatchIfDirectory(path string) {
	if fw.shouldIgnorePath(path) {
		return
	}
	info, err := os.Stat(path)
	if err != nil || !info.IsDir() {
		return
	}
	if err := fw.watcher.Add(path); err != nil {
		fw.logger.Warn("watch new directory", "path", path, "error", err)
	}
}

func (fw *FileWatcher) flushPending(pending map[string]struct{}) {
	if len(pending) == 0 || fw.onChange == nil {
		return
	}

	var changedPath string
	for path := range pending {
		changedPath = path
		break
	}
	clear(pending)
	fw.onChange(changedPath)
}

func (fw *FileWatcher) scheduleSync(path string, pending map[string]struct{}, timer **time.Timer, timerC *<-chan time.Time) {
	pending[path] = struct{}{}

	if *timer == nil {
		*timer = time.NewTimer(fw.debounce)
		*timerC = (*timer).C
		return
	}

	if !(*timer).Stop() {
		select {
		case <-(*timer).C:
		default:
		}
	}
	(*timer).Reset(fw.debounce)
}

func (fw *FileWatcher) stopTimer(timer *time.Timer) {
	if timer == nil {
		return
	}
	if !timer.Stop() {
		select {
		case <-timer.C:
		default:
		}
	}
}

func (fw *FileWatcher) isMarkdownPath(path string) bool {
	return strings.EqualFold(filepath.Ext(path), ".md")
}

func (fw *FileWatcher) shouldIgnorePath(path string) bool {
	rel, err := filepath.Rel(fw.rootDir, path)
	if err != nil {
		return true
	}
	if rel == "." {
		return false
	}
	if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
		return true
	}

	for _, part := range strings.Split(rel, string(filepath.Separator)) {
		if part == "" {
			continue
		}
		if strings.HasPrefix(part, ".") {
			return true
		}
		if isTempFileName(part) {
			return true
		}
	}
	return false
}

func isTempFileName(name string) bool {
	if strings.HasSuffix(name, "~") {
		return true
	}
	if strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") {
		return true
	}

	switch strings.ToLower(filepath.Ext(name)) {
	case ".swp", ".swo", ".tmp", ".temp", ".bak", ".orig", ".part", ".crdownload":
		return true
	}

	switch name {
	case ".DS_Store", "Thumbs.db":
		return true
	default:
		return false
	}
}

func (fw *FileWatcher) isProcessableEvent(event fsnotify.Event) bool {
	if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
		return false
	}
	if fw.shouldIgnorePath(event.Name) {
		return false
	}
	return fw.isMarkdownPath(event.Name)
}

func (fw *FileWatcher) handleEvent(event fsnotify.Event, pending map[string]struct{}, timer **time.Timer, timerC *<-chan time.Time) {
	if event.Op&(fsnotify.Create|fsnotify.Rename) != 0 {
		fw.addWatchIfDirectory(event.Name)
	}
	if fw.isProcessableEvent(event) {
		fw.scheduleSync(event.Name, pending, timer, timerC)
	}
}

func (fw *FileWatcher) SetOnChange(fn func(path string)) {
	fw.onChange = fn
}

func (fw *FileWatcher) Run(ctx context.Context) {
	defer fw.watcher.Close()

	pending := make(map[string]struct{})
	var timer *time.Timer
	var timerC <-chan time.Time
	defer fw.stopTimer(timer)

	for {
		select {
		case <-ctx.Done():
			return
		case <-timerC:
			fw.flushPending(pending)
			timer = nil
			timerC = nil
		case event, ok := <-fw.watcher.Events:
			if !ok {
				return
			}
			fw.handleEvent(event, pending, &timer, &timerC)
		case err, ok := <-fw.watcher.Errors:
			if !ok {
				return
			}
			fw.logger.Warn("fsnotify error", "error", err)
		}
	}
}
