// CopySure — checksum-verified file copy with resumable jobs and a diff mode.
//
// Usage:
//
//	copysure copy <src> <dst> [--workers N]     copy, verifying every file
//	                                             by SHA-256 after it lands;
//	                                             already-matching files at
//	                                             the destination are skipped
//	                                             (safe to re-run / resume)
//	copysure diff <src> <dst>                   compare two trees without
//	                                             copying anything
//
// CopySure never trusts a copy just because the OS said the syscall
// succeeded: every copied file is re-read from disk and hashed against the
// source before it's counted as done.
package main

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

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 "copy":
		cmdCopy(os.Args[2:])
	case "diff":
		cmdDiff(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, `CopySure — checksum-verified copy

Usage:
  copysure copy <src> <dst> [--workers N]
  copysure diff <src> <dst>
`)
}

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...)
}

type job struct {
	relPath string
	srcPath string
	dstPath string
}

type result struct {
	relPath string
	status  string // copied | skipped | mismatch | error
	detail  string
}

func cmdCopy(args []string) {
	fs := flag.NewFlagSet("copy", flag.ExitOnError)
	workers := fs.Int("workers", 4, "parallel copy workers")
	fs.Parse(reorderFlags(args, map[string]bool{"workers": true}))
	pos := fs.Args()
	if len(pos) != 2 {
		fmt.Fprintln(os.Stderr, "usage: copysure copy <src> <dst> [--workers N]")
		os.Exit(1)
	}
	src, dst := pos[0], pos[1]

	jobs, err := planJobs(src, dst)
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	if len(jobs) == 0 {
		fmt.Println("Nothing to copy — source has no regular files.")
		return
	}

	jobCh := make(chan job)
	resCh := make(chan result)
	var wg sync.WaitGroup
	for i := 0; i < *workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := range jobCh {
				resCh <- copyVerify(j)
			}
		}()
	}
	go func() {
		for _, j := range jobs {
			jobCh <- j
		}
		close(jobCh)
		wg.Wait()
		close(resCh)
	}()

	var copied, skipped, mismatched, failed int32
	for r := range resCh {
		switch r.status {
		case "copied":
			atomic.AddInt32(&copied, 1)
			fmt.Printf("  copied    %s\n", r.relPath)
		case "skipped":
			atomic.AddInt32(&skipped, 1)
			fmt.Printf("  unchanged %s (already verified)\n", r.relPath)
		case "mismatch":
			atomic.AddInt32(&mismatched, 1)
			fmt.Printf("  MISMATCH  %s — %s\n", r.relPath, r.detail)
		case "error":
			atomic.AddInt32(&failed, 1)
			fmt.Printf("  ERROR     %s — %s\n", r.relPath, r.detail)
		}
	}

	fmt.Printf("\n%d copied, %d already verified, %d checksum mismatches, %d errors\n", copied, skipped, mismatched, failed)
	if mismatched > 0 || failed > 0 {
		os.Exit(2)
	}
}

func planJobs(src, dst string) ([]job, error) {
	info, err := os.Stat(src)
	if err != nil {
		return nil, err
	}
	var jobs []job
	if !info.IsDir() {
		jobs = append(jobs, job{relPath: filepath.Base(src), srcPath: src, dstPath: dst})
		return jobs, nil
	}
	err = filepath.Walk(src, func(path string, fi os.FileInfo, err error) error {
		if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
			return nil
		}
		rel, err := filepath.Rel(src, path)
		if err != nil {
			return nil
		}
		jobs = append(jobs, job{relPath: rel, srcPath: path, dstPath: filepath.Join(dst, rel)})
		return nil
	})
	return jobs, err
}

func copyVerify(j job) result {
	srcHash, err := hashFile(j.srcPath)
	if err != nil {
		return result{j.relPath, "error", "reading source: " + err.Error()}
	}

	if dstHash, err := hashFile(j.dstPath); err == nil && dstHash == srcHash {
		return result{j.relPath, "skipped", ""}
	}

	if err := os.MkdirAll(filepath.Dir(j.dstPath), 0o755); err != nil {
		return result{j.relPath, "error", "creating destination dir: " + err.Error()}
	}
	if err := copyFile(j.srcPath, j.dstPath); err != nil {
		return result{j.relPath, "error", "copy failed: " + err.Error()}
	}

	dstHash, err := hashFile(j.dstPath)
	if err != nil {
		return result{j.relPath, "error", "re-reading destination: " + err.Error()}
	}
	if dstHash != srcHash {
		return result{j.relPath, "mismatch", "source and destination hashes differ after copy"}
	}
	return result{j.relPath, "copied", ""}
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	tmp := dst + ".copysure-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)
}

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 cmdDiff(args []string) {
	fs := flag.NewFlagSet("diff", flag.ExitOnError)
	fs.Parse(args)
	pos := fs.Args()
	if len(pos) != 2 {
		fmt.Fprintln(os.Stderr, "usage: copysure diff <src> <dst>")
		os.Exit(1)
	}
	if code := diffTrees(pos[0], pos[1]); code != 0 {
		os.Exit(code)
	}
}

// diffTrees compares two trees and prints the report, returning the exit code
// the command line uses: 0 when the trees match, 1 when a tree could not be
// read, 2 when they differ. Split out from cmdDiff so the guided session can
// run exactly the same read-only comparison without exiting the process and
// closing the reader's window mid-sentence.
func diffTrees(src, dst string) int {
	srcFiles, err := listRel(src)
	if err != nil {
		fmt.Fprintln(os.Stderr, "error reading src:", err)
		return 1
	}
	dstFiles, err := listRel(dst)
	if err != nil {
		fmt.Fprintln(os.Stderr, "error reading dst:", err)
		return 1
	}

	var onlySrc, onlyDst, differing, identical []string
	for rel := range srcFiles {
		if _, ok := dstFiles[rel]; !ok {
			onlySrc = append(onlySrc, rel)
			continue
		}
		sh, err1 := hashFile(filepath.Join(src, rel))
		dh, err2 := hashFile(filepath.Join(dst, rel))
		if err1 != nil || err2 != nil || sh != dh {
			differing = append(differing, rel)
		} else {
			identical = append(identical, rel)
		}
	}
	for rel := range dstFiles {
		if _, ok := srcFiles[rel]; !ok {
			onlyDst = append(onlyDst, rel)
		}
	}

	report("Only in source", onlySrc)
	report("Only in destination", onlyDst)
	report("Differ", differing)
	fmt.Printf("\n%d identical, %d only-in-source, %d only-in-destination, %d differing\n",
		len(identical), len(onlySrc), len(onlyDst), len(differing))
	if len(onlySrc)+len(onlyDst)+len(differing) > 0 {
		return 2
	}
	return 0
}

func report(label string, items []string) {
	if len(items) == 0 {
		return
	}
	fmt.Println(label + ":")
	for _, i := range items {
		fmt.Println("  " + i)
	}
}

func listRel(root string) (map[string]bool, error) {
	out := map[string]bool{}
	info, err := os.Stat(root)
	if err != nil {
		return nil, err
	}
	if !info.IsDir() {
		out[filepath.Base(root)] = true
		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 {
			out[rel] = true
		}
		return nil
	})
	return out, err
}
