//go:build windows

package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"net"
	"os"
	"os/signal"
	"path/filepath"
	"sync"
	"syscall"
	"time"

	"github.com/fsnotify/fsnotify"
)

var (
	socketPath = flag.String("socket", "", "Unix socket path to serve events on (required)")
	sourceDir  = flag.String("dir", "", "Source directory to watch (required)")
)

func run() {
	flag.Parse()

	if *socketPath == "" || *sourceDir == "" {
		fmt.Fprintln(os.Stderr, "Usage: sourcewatch.exe -dir <source> -socket <socket-path>")
		os.Exit(1)
	}

	level, err := ParseLogLevel(*logLevel)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	SetLogLevel(level)

	logInfo("watcher: scanning %s ...", *sourceDir)

	stack := NewGitignoreStack(*sourceDir)
	manifest := scanDirectory(*sourceDir, stack)

	logInfo("watcher: found %d files, serving on %s", len(manifest.Files), *socketPath)

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logErrorf("watcher: %v", err)
		os.Exit(1)
	}
	defer watcher.Close()

	if err := addWatchersRecursively(watcher, *sourceDir, stack); err != nil {
		logErrorf("watcher: %v", err)
		os.Exit(1)
	}

	os.Remove(*socketPath)

	ln, err := net.Listen("unix", *socketPath)
	if err != nil {
		logErrorf("socket: %v", err)
		os.Exit(1)
	}
	defer func() {
		ln.Close()
		os.Remove(*socketPath)
	}()

	var mu sync.Mutex
	debounceTimers := make(map[string]*time.Timer)
	debounceOps := make(map[string]fsnotify.Op)

	clients := make(map[net.Conn]bool)
	var clientsMu sync.Mutex

	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				return
			}
			clientsMu.Lock()
			clients[conn] = true
			clientsMu.Unlock()
			logDebug("client connected: %s", conn.RemoteAddr())

			go func(c net.Conn) {
				sendManifest(c, manifest)

				scanner := bufio.NewScanner(c)
				for scanner.Scan() {
				}

				clientsMu.Lock()
				delete(clients, c)
				clientsMu.Unlock()
				c.Close()
			}(conn)
		}
	}()

	broadcast := func(evt FileEvent) {
		clientsMu.Lock()
		defer clientsMu.Unlock()
		for c := range clients {
			sendEvent(c, evt)
		}
	}

	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()

	go func() {
		<-ctx.Done()
		ln.Close()
	}()

	for {
		select {
		case <-ctx.Done():
			return

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

			relPath, err := filepath.Rel(*sourceDir, event.Name)
			if err != nil {
				continue
			}

			if isGitDir(relPath) {
				continue
			}

			if stack.IsIgnored(relPath) {
				continue
			}

			logDebug("event: %s %s", event.Op, relPath)

			if event.Op&fsnotify.Create != 0 {
				srcPath := filepath.Join(*sourceDir, relPath)
				info, err := os.Lstat(srcPath)
				if err == nil && info.IsDir() {
					if hasGitignore(srcPath) {
						stack.Push(srcPath)
					}
					if err := watcher.Add(srcPath); err != nil {
						logErrorf("watch %s: %v", relPath, err)
					}
					filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
						if err != nil || !info.IsDir() || path == srcPath {
							return nil
						}
						watcher.Add(path)
						return nil
					})
				}
			}

			mu.Lock()
			if existing, exists := debounceOps[relPath]; exists {
				debounceTimers[relPath].Stop()
				debounceOps[relPath] = existing | event.Op
			} else {
				debounceOps[relPath] = event.Op
			}
			path := relPath
			debounceTimers[relPath] = time.AfterFunc(200*time.Millisecond, func() {
				mu.Lock()
				op := debounceOps[path]
				delete(debounceOps, path)
				delete(debounceTimers, path)
				mu.Unlock()

				evt := FileEvent{Path: path}
				switch {
				case op&fsnotify.Create != 0:
					evt.Op = "create"
				case op&fsnotify.Write != 0:
					evt.Op = "write"
				case op&fsnotify.Remove != 0:
					evt.Op = "remove"
				case op&fsnotify.Rename != 0:
					evt.Op = "remove"
				default:
					return
				}

				// Update manifest
				srcPath := filepath.Join(*sourceDir, path)
				switch evt.Op {
				case "create":
					if info, err := os.Lstat(srcPath); err == nil {
						linkTarget := ""
						if info.Mode()&os.ModeSymlink != 0 {
							linkTarget, _ = os.Readlink(srcPath)
						}
						manifest.Files[path] = FileMeta{
							Size:    info.Size(),
							Mode:    info.Mode(),
							ModTime: info.ModTime(),
							Symlink: linkTarget,
						}
					}
				case "write":
					if info, err := os.Lstat(srcPath); err == nil {
						linkTarget := ""
						if info.Mode()&os.ModeSymlink != 0 {
							linkTarget, _ = os.Readlink(srcPath)
						}
						manifest.Files[path] = FileMeta{
							Size:    info.Size(),
							Mode:    info.Mode(),
							ModTime: info.ModTime(),
							Symlink: linkTarget,
						}
					}
				case "remove":
					delete(manifest.Files, path)
				}

				broadcast(evt)
			})
			mu.Unlock()

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

func scanDirectory(root string, stack *GitignoreStack) *Manifest {
	manifest := &Manifest{Files: make(map[string]FileMeta)}

	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}

		relPath, err := filepath.Rel(root, path)
		if err != nil {
			return nil
		}

		if relPath == "." {
			return nil
		}

		if isGitDir(relPath) {
			if info.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}

		if info.IsDir() && hasGitignore(path) {
			stack.Push(path)
		}

		if stack.IsIgnored(relPath) {
			return nil
		}

		linkTarget := ""
		if info.Mode()&os.ModeSymlink != 0 {
			linkTarget, _ = os.Readlink(path)
		}

		manifest.Files[relPath] = FileMeta{
			Size:    info.Size(),
			Mode:    info.Mode(),
			ModTime: info.ModTime(),
			Symlink: linkTarget,
		}

		return nil
	})

	return manifest
}

func sendManifest(conn net.Conn, manifest *Manifest) {
	msg := &ProtocolMessage{
		Type:  "manifest",
		Files: manifest.ToJSON(),
	}
	data, err := EncodeMessage(msg)
	if err != nil {
		return
	}
	data = append(data, '\n')
	conn.Write(data)

	done := &ProtocolMessage{Type: "manifest_done"}
	doneData, _ := EncodeMessage(done)
	doneData = append(doneData, '\n')
	conn.Write(doneData)
}

func sendEvent(conn net.Conn, evt FileEvent) {
	msg := &ProtocolMessage{
		Type: "event",
		Path: evt.Path,
	}
	switch evt.Op {
	case "create":
		msg.Type = "event_create"
	case "write":
		msg.Type = "event_write"
	case "remove":
		msg.Type = "event_remove"
	}
	data, err := EncodeMessage(msg)
	if err != nil {
		return
	}
	data = append(data, '\n')
	conn.Write(data)
}

func addWatchersRecursively(watcher *fsnotify.Watcher, root string, stack *GitignoreStack) error {
	return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relPath, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}

		if isGitDir(relPath) {
			if info.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}

		if info.IsDir() {
			if hasGitignore(path) {
				stack.Push(path)
			}
			if !stack.IsIgnored(relPath) {
				return watcher.Add(path)
			}
		}

		return nil
	})
}
