//go:build !windows

package main

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

var (
	hostDir  = flag.String("host", "/host", "Host source directory (read-only)")
	dataDir  = flag.String("data", "/data", "Data destination directory")
	dryRun   = flag.Bool("dry-run", false, "Preview what would be synced without copying")
	listen   = flag.String("listen", ":5151", "TCP address to listen on for Windows watcher connections")
)

func run() {
	flag.Parse()

	if *verbose {
		*logLevel = "debug"
	}

	level, err := ParseLogLevel(*logLevel)
	if err != nil {
		logErrorf("%v", err)
		os.Exit(1)
	}
	SetLogLevel(level)

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

	stack := NewGitignoreStack(*hostDir)

	if *listen == "" {
		logInfo("no -listen specified, running standalone (full scan)")
		manifest, err := initialSync(*hostDir, *dataDir, stack, *dryRun)
		if err != nil {
			logErrorf("initial sync failed: %v", err)
			os.Exit(1)
		}
		if *dryRun {
			logInfo("dry-run complete, exiting")
			return
		}
		logInfo("watcher: ready (no file watching in standalone mode)")
		_ = manifest
		<-ctx.Done()
		return
	}

	ln, err := net.Listen("tcp", *listen)
	if err != nil {
		logErrorf("listen: %v", err)
		os.Exit(1)
	}
	defer ln.Close()

	logInfo("watcher: listening on %s", *listen)
	logInfo("watcher: waiting for Windows watcher to connect ...")

	for {
		if ctx.Err() != nil {
			return
		}

		conn, err := ln.Accept()
		if err != nil {
			logErrorf("accept: %v", err)
			continue
		}

		logInfo("watcher: client connected from %s", conn.RemoteAddr())
		handleClient(conn, stack)
		logInfo("watcher: client disconnected, waiting for reconnect ...")
	}
}

func handleClient(conn net.Conn, stack *GitignoreStack) {
	defer conn.Close()

	remoteManifest := &Manifest{Files: make(map[string]FileMeta)}
	batches := 0

	scanner := bufio.NewScanner(conn)
	scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) // 1MB buffer per batch

	for {
		conn.SetReadDeadline(time.Now().Add(30 * time.Second))
		if !scanner.Scan() {
			break
		}
		conn.SetReadDeadline(time.Time{}) // reset deadline

		msg, err := DecodeMessage(scanner.Bytes())
		if err != nil {
			logDebug("bad message: %v", err)
			continue
		}

		switch msg.Type {
		case "manifest_batch":
			for k, v := range msg.Files {
				remoteManifest.Files[k] = FileMeta{
					Size:    v.Size,
					Mode:    os.FileMode(v.Mode),
					ModTime: parseTime(v.ModTime),
					Symlink: v.Symlink,
				}
			}
			batches++
			logDebug("manifest: batch %d received (%d files, total: %d)", batches, len(msg.Files), len(remoteManifest.Files))

		case "manifest_done":
			logInfo("manifest: received %d files in %d batches from host", len(remoteManifest.Files), batches)
			logInfo("manifest: comparing with local state ...")
			syncFromManifest(remoteManifest, stack)
			logInfo("sync: done")
			logInfo("watcher: monitoring for changes ...")
			conn.SetReadDeadline(time.Time{}) // no timeout during event monitoring

		case "event_create":
			logDebug("event: create %s", msg.Path)
			if !isGitDir(msg.Path) && !stack.IsIgnored(msg.Path) {
				syncCreate(
					filepath.Join(*hostDir, msg.Path),
					filepath.Join(*dataDir, msg.Path),
					msg.Path, remoteManifest, stack, *dryRun, nil,
				)
			}

		case "event_write":
			logDebug("event: write %s", msg.Path)
			if !isGitDir(msg.Path) && !stack.IsIgnored(msg.Path) {
				syncWrite(
					filepath.Join(*hostDir, msg.Path),
					filepath.Join(*dataDir, msg.Path),
					msg.Path, remoteManifest, *dryRun,
				)
			}

		case "event_remove":
			logDebug("event: remove %s", msg.Path)
			if !isGitDir(msg.Path) && !stack.IsIgnored(msg.Path) {
				syncRemove(
					filepath.Join(*dataDir, msg.Path),
					msg.Path, remoteManifest, *dryRun,
				)
			}

		case "ping":
			logDebug("ping from client")
			pong := &ProtocolMessage{Type: "pong"}
			pongData, _ := EncodeMessage(pong)
			pongData = append(pongData, '\n')
			conn.Write(pongData)
		}
	}

	if err := scanner.Err(); err != nil {
		logErrorf("client read: %v", err)
	}
}

func syncFromManifest(remote *Manifest, stack *GitignoreStack) {
	stats := &syncStats{}
	start := time.Now()

	localManifest := scanLocalDirectory(*dataDir)

	for relPath, remoteMeta := range remote.Files {
		if isGitDir(relPath) {
			continue
		}
		if stack.IsIgnored(relPath) {
			stats.skipped++
			continue
		}

		destPath := filepath.Join(*dataDir, relPath)
		srcPath := filepath.Join(*hostDir, relPath)

		if localMeta, exists := localManifest.Files[relPath]; exists {
			if localMeta.Size == remoteMeta.Size &&
				localMeta.Mode == remoteMeta.Mode &&
				localMeta.ModTime.Equal(remoteMeta.ModTime) &&
				localMeta.Symlink == remoteMeta.Symlink {
				continue
			}
		}

		info, err := os.Lstat(srcPath)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			stats.errors++
			logErrorf("stat %s: %v", relPath, err)
			continue
		}

		if info.IsDir() {
			stats.dirs++
			if *dryRun {
				logInfo("[dry-run] would create dir: %s", relPath)
				continue
			}
			if err := os.MkdirAll(destPath, info.Mode()); err != nil {
				stats.errors++
				logErrorf("mkdir %s: %v", relPath, err)
			}
			continue
		}

		stats.files++

		if *dryRun {
			logInfo("[dry-run] would sync: %s", relPath)
			continue
		}

		os.MkdirAll(filepath.Dir(destPath), 0755)
		os.Remove(destPath)

		linkTarget := ""
		if info.Mode()&os.ModeSymlink != 0 {
			linkTarget, err = os.Readlink(srcPath)
			if err != nil {
				stats.errors++
				logErrorf("readlink %s: %v", relPath, err)
				continue
			}
			if err := os.Symlink(linkTarget, destPath); err != nil {
				stats.errors++
				logErrorf("symlink %s: %v", relPath, err)
			}
			continue
		}

		if err := copyFileWithRetry(srcPath, destPath, info.Mode()); err != nil {
			stats.errors++
			logErrorf("copy %s: %v", relPath, err)
		}

		if stats.files%1000 == 0 && stats.files > 0 {
			logInfo("  synced %d files ...", stats.files)
		}
	}

	elapsed := time.Since(start)
	logInfo("sync: done in %s — %d files, %d dirs, %d skipped, %d errors",
		elapsed.Round(time.Millisecond), stats.files, stats.dirs, stats.skipped, stats.errors)
}

func scanLocalDirectory(root string) *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
		}

		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 parseTime(s string) time.Time {
	t, _ := time.Parse(time.RFC3339Nano, s)
	return t
}
