// SyncGuard — one-way mirror sync with a dry-run conflict preview and an
// audit log, built on the same checksum-verified copy engine as CopySure.
//
// Usage:
//
//	syncguard mirror <src> <dst> [--delete] [--apply] [--log FILE]
//
// Without --apply, mirror is a dry run: it prints the plan (what would be
// copied, updated, and — only with --delete — removed from dst) and
// touches nothing. --delete is opt-in on top of --apply: a plain --apply
// run only ever adds/updates at the destination, never removes files
// there, so the two most consequential behaviors each need their own
// explicit flag rather than one big "just sync it" switch.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type auditRecord struct {
	Action  string `json:"action"` // copy | update | delete
	Path    string `json:"path"`
	TimeUTC string `json:"time_utc"`
}

func main() {
	if len(os.Args) < 2 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// questions the program needs and stay on screen. Printing usage and
		// exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}
	switch os.Args[1] {
	case "mirror":
		cmdMirror(os.Args[2:])
	case "-h", "--help", "help":
		usage()
	default:
		fmt.Fprintf(os.Stderr, "unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `SyncGuard — one-way mirror sync with a dry-run preview and audit log

Usage:
  syncguard mirror <src> <dst> [--delete] [--apply] [--log FILE]

Without --apply: dry run, prints the plan, touches nothing.
--delete removes dst files that no longer exist in src (opt-in, only
takes effect together with --apply).
`)
}

func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

func cmdMirror(args []string) {
	fs := flag.NewFlagSet("mirror", flag.ExitOnError)
	del := fs.Bool("delete", false, "remove dst files that no longer exist in src")
	apply := fs.Bool("apply", false, "actually perform the sync (default is a dry run)")
	logPath := fs.String("log", "", "write an audit log (JSON lines) to this file")
	fs.Parse(reorderFlags(args, map[string]bool{"log": true}))
	pos := fs.Args()
	if len(pos) != 2 {
		fmt.Fprintln(os.Stderr, "usage: syncguard mirror <src> <dst> [--delete] [--apply] [--log FILE]")
		os.Exit(1)
	}
	src, dst := pos[0], pos[1]

	srcFiles, err := hashTree(src)
	if err != nil {
		fmt.Fprintln(os.Stderr, "error reading src:", err)
		os.Exit(1)
	}
	dstFiles, err := hashTree(dst)
	if err != nil && !os.IsNotExist(err) {
		fmt.Fprintln(os.Stderr, "error reading dst:", err)
		os.Exit(1)
	}

	var toCopy, toUpdate, toDelete []string
	for rel, hash := range srcFiles {
		if dstHash, ok := dstFiles[rel]; !ok {
			toCopy = append(toCopy, rel)
		} else if dstHash != hash {
			toUpdate = append(toUpdate, rel)
		}
	}
	if *del {
		for rel := range dstFiles {
			if _, ok := srcFiles[rel]; !ok {
				toDelete = append(toDelete, rel)
			}
		}
	}

	var log []auditRecord
	action := func(kind, rel string) {
		fmt.Printf("  %-6s %s\n", strings.ToUpper(kind), rel)
		log = append(log, auditRecord{Action: kind, Path: rel, TimeUTC: time.Now().UTC().Format(time.RFC3339)})
	}

	fmt.Println("copy (new at dst):")
	for _, rel := range toCopy {
		if *apply {
			if err := copyFile(filepath.Join(src, rel), filepath.Join(dst, rel)); err != nil {
				fmt.Fprintf(os.Stderr, "  FAILED copy %s: %v\n", rel, err)
				continue
			}
		}
		action("copy", rel)
	}

	fmt.Println("update (changed):")
	for _, rel := range toUpdate {
		if *apply {
			if err := copyFile(filepath.Join(src, rel), filepath.Join(dst, rel)); err != nil {
				fmt.Fprintf(os.Stderr, "  FAILED update %s: %v\n", rel, err)
				continue
			}
		}
		action("update", rel)
	}

	if *del {
		fmt.Println("delete (extraneous at dst):")
		for _, rel := range toDelete {
			if *apply {
				if err := os.Remove(filepath.Join(dst, rel)); err != nil {
					fmt.Fprintf(os.Stderr, "  FAILED delete %s: %v\n", rel, err)
					continue
				}
			}
			action("delete", rel)
		}
	} else if len(dstFiles) > 0 {
		var extraneous int
		for rel := range dstFiles {
			if _, ok := srcFiles[rel]; !ok {
				extraneous++
			}
		}
		if extraneous > 0 {
			fmt.Printf("(%d file(s) at dst not in src — not shown/removed; pass --delete to mirror exactly)\n", extraneous)
		}
	}

	verb := "would copy"
	if *apply {
		verb = "copied"
	}
	fmt.Printf("\n%d %s, %d updated", len(toCopy), verb, len(toUpdate))
	if *del {
		verb2 := "would delete"
		if *apply {
			verb2 = "deleted"
		}
		fmt.Printf(", %d %s", len(toDelete), verb2)
	}
	fmt.Println()
	if !*apply {
		fmt.Println("(dry run — re-run with --apply to perform this plan)")
	}

	if *apply && *logPath != "" && len(log) > 0 {
		f, err := os.OpenFile(*logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
		if err != nil {
			fmt.Fprintln(os.Stderr, "failed to open audit log:", err)
			return
		}
		defer f.Close()
		enc := json.NewEncoder(f)
		for _, rec := range log {
			enc.Encode(rec)
		}
		fmt.Println("Audit log appended to", *logPath)
	}
}

func hashTree(root string) (map[string]string, error) {
	out := map[string]string{}
	info, err := os.Stat(root)
	if err != nil {
		return out, err
	}
	if !info.IsDir() {
		h, err := hashFile(root)
		if err == nil {
			out[filepath.Base(root)] = h
		}
		return out, nil
	}
	err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
		if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
			return nil
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return nil
		}
		h, err := hashFile(path)
		if err == nil {
			out[filepath.ToSlash(rel)] = h
		}
		return nil
	})
	return out, err
}

func hashFile(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
		return err
	}
	tmp := dst + ".syncguard-tmp"
	out, err := os.Create(tmp)
	if err != nil {
		return err
	}
	if _, err := io.Copy(out, in); err != nil {
		out.Close()
		os.Remove(tmp)
		return err
	}
	if err := out.Close(); err != nil {
		os.Remove(tmp)
		return err
	}
	return os.Rename(tmp, dst)
}
