//go:build windows

package fswatch

import (
	"errors"
	"fmt"
	"sync"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"
)

// ---------------------------------------------------------------------------
// windows.go: Windows ReadDirectoryChangesW backend
//
// Uses the Win32 ReadDirectoryChangesW API with overlapped (asynchronous)
// I/O to monitor directory trees. Unlike the Unix backends, there is no
// shared event loop; each watch owns its own goroutine that
// independently polls for directory changes.
//
//	┌──────────────────────────────────────────────────────────────┐
//	│  windowsBackend                                              │
//	│  (no event loop; start() just signals readiness)             │
//	│                                                              │
//	│  subscribe() per directory:                                  │
//	│       │                                                      │
//	│       ▼                                                      │
//	│  ┌───────────────────────────────────────────────────────┐   │
//	│  │ windowsSubscription                                   │   │
//	│  │                                                       │   │
//	│  │  handle ← CreateFile(dir, FILE_FLAG_OVERLAPPED)       │   │
//	│  │                                                       │   │
//	│  │  run() goroutine:                                     │   │
//	│  │    ┌───────────────────────────────┐                  │   │
//	│  │    │ ReadDirectoryChangesW (async) │◄──────────┐      │   │
//	│  │    └───────────────┬───────────────┘           │      │   │
//	│  │                    ▼                           │      │   │
//	│  │    ┌───────────────────────────────┐           │      │   │
//	│  │    │ WaitForSingleObject(event)    │           │      │   │
//	│  │    └───────────────┬───────────────┘           │      │   │
//	│  │                    ▼                           │      │   │
//	│  │    ┌───────────────────────────────┐           │      │   │
//	│  │    │ GetOverlappedResult           │           │      │   │
//	│  │    └───────────────┬───────────────┘           │      │   │
//	│  │                    ▼                           │      │   │
//	│  │    ┌───────────────────────────────┐           │      │   │
//	│  │    │ Walk FILE_NOTIFY_INFORMATION  │           │      │   │
//	│  │    │ chain → processOne()          ├───────────┘      │   │
//	│  │    └───────────────────────────────┘                  │   │
//	│  │                                                       │   │
//	│  │  stop: stopCh → CancelIoEx → run() exits              │   │
//	│  │  cleanup: deferred CloseHandle → doneCh closed        │   │
//	│  └───────────────────────────────────────────────────────┘   │
//	└──────────────────────────────────────────────────────────────┘
//
// Goroutines and threading:
//   - One goroutine per watch (run). It blocks in WaitForSingleObject
//     waiting for ReadDirectoryChangesW completions. processCompletion and
//     processOne execute on this goroutine. There is no shared event loop.
//   - subscribe runs on the caller's goroutine. It opens the directory handle,
//     arms the first ReadDirectoryChangesW, and spawns run().
//   - closeWatch runs on the caller's goroutine. It closes stopCh, which
//     triggers CancelIoEx (from a helper goroutine inside run's wait), waking
//     the run goroutine so it can exit cleanly.
//   - fatal() spawns a separate goroutine for handleWatcherError to avoid
//     deadlock: handleWatcherError → closeWatch → wait(doneCh), but doneCh
//     is only closed when run() returns. The indirection lets run() exit first.
//
// Callback delivery:
//   dirWatch.notify() posts to the shared process-wide debouncer. After a
//   coalescing window (50 ms min / 500 ms max), the debouncer invokes all
//   registered WatchCallbacks on its own dedicated goroutine; never on
//   the caller's goroutine or the per-watch goroutine.
//
// WatchDirectory flow:
//  1. Open the directory with CreateFile (FILE_FLAG_BACKUP_SEMANTICS |
//     FILE_FLAG_OVERLAPPED) on the caller's goroutine.
//  2. Arm the first ReadDirectoryChangesW synchronously so that any
//     filesystem operation after WatchDirectory returns is guaranteed to be
//     observed.
//  3. Spawn the run() goroutine.
//
// Event dispatch (processCompletion / processOne, on run goroutine):
//  1. Wait for the overlapped read to complete (WaitForSingleObject).
//  2. Arm the next ReadDirectoryChangesW immediately (double-buffering).
//  3. Walk the FILE_NOTIFY_INFORMATION linked list:
//     - FILE_ACTION_ADDED / RENAMED_NEW_NAME  → events.create (→ EventUpdate)
//     - FILE_ACTION_MODIFIED                  → events.update (→ EventUpdate)
//     - FILE_ACTION_REMOVED / RENAMED_OLD_NAME → events.remove + tree.remove
//  4. Call dirWatch.notify() to trigger the debouncer.
//
// Error recovery:
//   - ERROR_OPERATION_ABORTED → normal shutdown (CancelIoEx was called).
//   - ERROR_INVALID_PARAMETER → shrink buffer to 64 KB (network share limit).
//   - ERROR_NOTIFY_ENUM_DIR  → ErrOverflow (too many changes queued).
//   - ERROR_ACCESS_DENIED    → check if the watched dir was deleted.
//
// Shutdown:
//   close(stopCh) → CancelIoEx cancels in-flight IO → run() goroutine
//   exits → deferred CloseHandle closes the directory handle → doneCh closed.
// ---------------------------------------------------------------------------

var (
	errGetFileInfo         = errors.New("could not get file information")
	errReadChanges         = errors.New("failed to read changes")
	errGetOverlappedResult = errors.New("GetOverlappedResult failed")
	errUnknown             = errors.New("unknown error")
)

const (
	defaultBufSize = 1024 * 1024
	networkBufSize = 64 * 1024

	notifyChangeFilter = windows.FILE_NOTIFY_CHANGE_FILE_NAME |
		windows.FILE_NOTIFY_CHANGE_DIR_NAME |
		windows.FILE_NOTIFY_CHANGE_SIZE |
		windows.FILE_NOTIFY_CHANGE_LAST_WRITE
)

// windowsBackend.
type windowsBackend struct {
	watcherBase
}

func init() {
	windowsWatcher.factory = func() watcherImpl { return newWindowsBackend() }
}

func newWindowsBackend() *windowsBackend {
	b := &windowsBackend{}
	b.watcherBase.init(b)
	return b
}

// start notifies that the watcherImpl is ready. Each watch owns
// its own goroutine, so there's no shared event loop to start.
func (b *windowsBackend) start() error {
	b.notifyStarted()
	return nil
}

// windowsSubscription.
type windowsSubscription struct {
	mu          sync.Mutex
	watcherImpl *windowsBackend
	dirWatch    *dirWatch
	handle      windows.Handle
	stopped     bool
	stopCh      chan struct{}
	doneCh      chan struct{}
	bufBytes    int
	first       *windowsRead
}

type windowsRead struct {
	buf        []byte
	overlapped windows.Overlapped
	event      windows.Handle
}

func newWindowsSubscription(watcherImpl *windowsBackend, w *dirWatch) (*windowsSubscription, error) {
	pathPtr, err := windows.UTF16PtrFromString(w.physicalDir)
	if err != nil {
		return nil, &dirWatchError{err: err, dirWatch: w}
	}
	h, err := windows.CreateFile(
		pathPtr,
		windows.FILE_LIST_DIRECTORY,
		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
		nil,
		windows.OPEN_EXISTING,
		windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED,
		0,
	)
	if err != nil {
		return nil, &dirWatchError{err: fmt.Errorf("invalid handle: %w", err), dirWatch: w}
	}
	var info windows.ByHandleFileInformation
	if err := windows.GetFileInformationByHandle(h, &info); err != nil {
		_ = windows.CloseHandle(h)
		return nil, &dirWatchError{err: errGetFileInfo, dirWatch: w}
	}
	if info.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
		_ = windows.CloseHandle(h)
		return nil, &dirWatchError{err: syscall.ENOTDIR, dirWatch: w}
	}
	return &windowsSubscription{
		watcherImpl: watcherImpl,
		dirWatch:    w,

		handle:   h,
		stopCh:   make(chan struct{}),
		doneCh:   make(chan struct{}),
		bufBytes: defaultBufSize,
	}, nil
}

func (s *windowsSubscription) beginRead() (*windowsRead, error) {
	s.mu.Lock()
	if s.stopped {
		s.mu.Unlock()
		return nil, nil
	}
	bufSize := s.bufBytes
	s.mu.Unlock()

	req := &windowsRead{buf: make([]byte, bufSize)}
	ev, err := windows.CreateEvent(nil, 1, 0, nil)
	if err != nil {
		return nil, fmt.Errorf("CreateEvent: %w", err)
	}
	req.event = ev
	req.overlapped.HEvent = ev

	var bytesReturned uint32
	err = windows.ReadDirectoryChanges(
		s.handle,
		&req.buf[0],
		uint32(len(req.buf)),
		s.dirWatch.recursive, // recursive
		notifyChangeFilter,
		&bytesReturned,
		&req.overlapped,
		0,
	)
	if err != nil {
		_ = windows.CloseHandle(ev)
		return nil, &dirWatchError{err: errReadChanges, dirWatch: s.dirWatch}
	}
	return req, nil
}

func (r *windowsRead) wait(s *windowsSubscription) (uint32, error, error) {
	stopWait := make(chan struct{})
	go func() {
		select {
		case <-s.stopCh:
			_ = windows.CancelIoEx(s.handle, &r.overlapped)
		case <-stopWait:
			// Do nothing; wait completed normally.
		}
	}()
	_, waitErr := windows.WaitForSingleObject(r.event, windows.INFINITE)
	close(stopWait)
	var bytes uint32
	completionErr := windows.GetOverlappedResult(s.handle, &r.overlapped, &bytes, false)
	_ = windows.CloseHandle(r.event)
	return bytes, waitErr, completionErr
}

// run is the per-watch goroutine. It loops on ReadDirectoryChangesW
// until the watch is stopped or an unrecoverable error occurs.
//
// We close the directory handle here in a defer (not in stop()) to
// guarantee that any in-flight ReadDirectoryChangesW has completed and
// GetOverlappedResult has returned before the handle becomes invalid.
// Closing the handle from another goroutine while we're mid-syscall on
// it is undefined behavior on Windows.
func (s *windowsSubscription) run() {
	defer close(s.doneCh)
	defer func() { _ = windows.CloseHandle(s.handle) }()
	if s.first == nil {
		// subscribe always arms the initial read before spawning run.
		// Guard the invariant rather than silently producing a watch
		// that delivers neither events nor errors if it ever breaks.
		s.fatal(&dirWatchError{err: errors.New("fswatch: windows: missing initial read"), dirWatch: s.dirWatch})
		return
	}
	current := s.first
	s.first = nil
	for {
		bytes, waitErr, gErr := current.wait(s)
		if waitErr != nil && gErr != nil {
			s.fatal(&dirWatchError{err: errGetOverlappedResult, dirWatch: s.dirWatch})
			return
		}

		s.mu.Lock()
		if s.stopped {
			s.mu.Unlock()
			return
		}
		s.mu.Unlock()

		if gErr != nil {
			if shouldStop := s.processCompletion(gErr, current.buf, bytes); shouldStop {
				return
			}
			next, err := s.beginRead()
			if err != nil {
				s.fatal(err)
				return
			}
			if next == nil {
				return
			}
			current = next
			continue
		}

		next, err := s.beginRead()
		if err != nil {
			s.fatal(err)
			return
		}
		if next == nil {
			return
		}
		if shouldStop := s.processCompletion(nil, current.buf, bytes); shouldStop {
			return
		}
		current = next
	}
}

// processCompletion mirrors the body of `Watch::processEvents` for
// the cases that translate cleanly to Go's overlapped wrapper.
func (s *windowsSubscription) processCompletion(callErr error, buf []byte, bytes uint32) (stop bool) {
	if callErr != nil {
		switch {
		case errors.Is(callErr, windows.ERROR_OPERATION_ABORTED):
			return true
		case errors.Is(callErr, windows.ERROR_INVALID_PARAMETER):
			s.mu.Lock()
			s.bufBytes = networkBufSize
			s.mu.Unlock()
			return false
		case errors.Is(callErr, windows.ERROR_NOTIFY_ENUM_DIR):
			s.dirWatch.events.setError(ErrOverflow)
			s.dirWatch.notify()
			return false
		case errors.Is(callErr, windows.ERROR_ACCESS_DENIED):
			// Possibly the watched dir was deleted; check and handle.
			pathPtr, _ := windows.UTF16PtrFromString(s.dirWatch.physicalDir)
			attrs, err := windows.GetFileAttributes(pathPtr)
			if err != nil || attrs == windows.INVALID_FILE_ATTRIBUTES || attrs&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
				s.dirWatch.events.remove(s.dirWatch.dir)
				s.dirWatch.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated))
				s.dirWatch.notify()
				s.stop()
				return true
			}
			fallthrough
		default:
			s.fatal(&dirWatchError{err: errUnknown, dirWatch: s.dirWatch})
			return true
		}
	}

	// Walk the FILE_NOTIFY_INFORMATION chain.
	offset := uint32(0)
	if bytes == 0 {
		bytes = uint32(len(buf))
	}
	for offset < bytes {
		fni := (*windows.FileNotifyInformation)(unsafe.Pointer(&buf[offset]))
		nameLen := int(fni.FileNameLength) / 2
		// The FileName field is a flexible array; reslice.
		base := unsafe.Pointer(&fni.FileName)
		nameSlice := unsafe.Slice((*uint16)(base), nameLen)
		name := windows.UTF16ToString(nameSlice)

		s.processOne(fni.Action, name)

		if fni.NextEntryOffset == 0 {
			break
		}
		offset += fni.NextEntryOffset
	}
	s.dirWatch.notify()
	return false
}

func (s *windowsSubscription) processOne(action uint32, name string) {
	path := s.dirWatch.dir + "\\" + name
	watchPath := s.dirWatch.physicalDir + "\\" + name
	switch action {
	case windows.FILE_ACTION_ADDED, windows.FILE_ACTION_RENAMED_NEW_NAME:
		// Always emit the event, even if the file is already gone by the
		// time we look it up. The kernel told us it was added, and a
		// subsequent REMOVED needs to find this entry in the eventList so
		// the create+delete pair coalesces away.
		s.dirWatch.events.create(path)
	case windows.FILE_ACTION_MODIFIED:
		if pathPtr, err := windows.UTF16PtrFromString(watchPath); err == nil {
			var data windows.Win32FileAttributeData
			if err := windows.GetFileAttributesEx(pathPtr, windows.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&data))); err == nil {
				if data.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
					s.dirWatch.events.update(path)
				}
			}
		}
	case windows.FILE_ACTION_REMOVED, windows.FILE_ACTION_RENAMED_OLD_NAME:
		seq := s.dirWatch.events.removeAndGetSequence(path)
		if s.dirWatch.terminateCallbacksForDeletedRoot(path, seq, fmt.Errorf("%w: watched directory removed", ErrWatchTerminated)) {
			s.dirWatch.notify()
		}
	}
}

// fatal is invoked when the run goroutine hits an unrecoverable error.
// handleWatcherError eventually calls closeWatch which waits on doneCh,
// but doneCh isn't closed until run() returns. Calling handleWatcherError
// synchronously from inside run() would deadlock. Spawn a goroutine to do
// the cleanup so run() can exit and unblock the wait.
func (s *windowsSubscription) fatal(err error) {
	werr := &dirWatchError{err: err, dirWatch: s.dirWatch}
	go s.watcherImpl.handleWatcherError(werr)
	s.stop()
}

func (s *windowsSubscription) stopLocked() {
	if s.stopped {
		return
	}
	s.stopped = true
	close(s.stopCh)
	// Cancel any in-flight IO so the wait returns; the run goroutine
	// closes the handle in its deferred cleanup once the IO has fully
	// finished and GetOverlappedResult has returned.
	_ = windows.CancelIoEx(s.handle, nil)
}

func (s *windowsSubscription) stop() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.stopLocked()
}

// subscribe mirrors `windowsBackend::subscribe`.
func (b *windowsBackend) subscribe(w *dirWatch) error {
	sub, err := newWindowsSubscription(b, w)
	if err != nil {
		return err
	}
	// Arm the first ReadDirectoryChangesW synchronously so that any file
	// operation a caller performs after subscribe returns is guaranteed
	// to be observed. Doing this in run() would race the spawning
	// goroutine with the caller's first filesystem op, occasionally
	// missing the initial create event or seeing it as a stray modify.
	first, err := sub.beginRead()
	if err != nil {
		_ = windows.CloseHandle(sub.handle)
		return err
	}
	sub.first = first
	w.state = sub
	go sub.run()
	return nil
}

// closeWatch mirrors `windowsBackend::closeWatch`. Signals the watch
// goroutine to stop and waits for it to finish; that way the directory
// handle is guaranteed to be closed before this returns, so a follow-on
// operation (e.g. immediately re-watching, deleting the directory) sees
// a clean slate.
func (b *windowsBackend) closeWatch(w *dirWatch) error {
	sub, _ := w.state.(*windowsSubscription)
	w.state = nil
	if sub == nil {
		return nil
	}
	sub.stop()
	<-sub.doneCh
	return nil
}

// shutdown mirrors `windowsBackend::~windowsBackend`.
func (b *windowsBackend) shutdown() {
	// Nothing to do; each watch owns its goroutine and is stopped
	// by closeWatch.
}
