package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

// runGuided is what happens when somebody double-clicks the program instead of
// typing its name at a prompt.
//
// Without this, Explorer opens a console, main() finds no arguments, prints
// the usage text to stderr and exits — and Windows destroys the window in the
// same instant. From the other side of the screen that is indistinguishable
// from a crash. So when we know we were double-clicked, we ask the one
// question the program actually needs and stay on screen until the reader is
// done.
//
// This path is entered ONLY when there are no arguments and both ends of the
// program are a real console. Any scripted or piped use takes exactly the same
// code path it always did.
//
// AppSweep's sweep command can quarantine files. Guided mode never goes near
// it: it records a snapshot and, when there is an earlier one to compare
// against, prints the difference. Both of those only read the folder they are
// pointed at.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  AppSweep")
	fmt.Println("  Finds what an uninstaller left behind, by remembering how a folder")
	fmt.Println("  looked before.")
	fmt.Println()
	fmt.Println("  The first time you run this, it writes down every file in the folder")
	fmt.Println("  you choose. Install and later uninstall a program, run this again on")
	fmt.Println("  the same folder, and it shows you exactly which files the uninstaller")
	fmt.Println("  forgot to take with it.")
	fmt.Println()
	fmt.Println("  It only reads that folder. Nothing there is moved, changed or deleted.")
	fmt.Println()

	suggested := suggestedFolder()
	for {
		fmt.Println("  Which folder shall I keep an eye on?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Print("  > ")

		if !in.Scan() {
			// stdin closed on us; there is nothing sensible left to ask.
			return
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a folder to watch. Try again, or close this window.")
			fmt.Println()
			continue
		}

		info, err := os.Stat(answer)
		switch {
		case err != nil:
			fmt.Println()
			fmt.Printf("  I cannot find %q.\n", answer)
			fmt.Println("  Tip: you can drag a folder from Explorer onto this window to")
			fmt.Println("  paste its location, then press Enter.")
			fmt.Println()
			continue
		case !info.IsDir():
			fmt.Println()
			fmt.Printf("  %q is a file, not a folder. Give me the folder it sits in.\n", answer)
			fmt.Println()
			continue
		}

		guidedSweepCheck(answer)
		break
	}

	fmt.Println()
	fmt.Println("  Done. Run this again after you uninstall something to see the")
	fmt.Println("  difference. The command-line version has more control, including a")
	fmt.Println("  way to move leftovers into a quarantine folder: appsweep --help")
	pause(in)
}

// guidedSweepCheck records the folder as it stands now and, if this folder has
// been recorded before, reports what changed since then.
//
// It writes exactly one file: a new, uniquely named record in the reader's own
// AppSweep folder. It never overwrites an earlier record, and it never touches
// anything inside the folder being watched.
func guidedSweepCheck(dir string) {
	abs, err := filepath.Abs(dir)
	if err != nil {
		abs = dir
	}
	abs = filepath.Clean(abs)

	store, err := snapshotStore()
	if err != nil {
		fmt.Println()
		fmt.Printf("  I could not find anywhere to keep my notes: %v\n", err)
		return
	}
	previous := latestSnapshotFor(store, abs)

	fmt.Println()
	fmt.Println("  Reading. On a large folder this can take a minute.")
	fmt.Println()

	current := filepath.Join(store, snapshotName(abs, time.Now()))
	if code := cmdSnapshot([]string{"--out", current, abs}); code != 0 {
		fmt.Println()
		fmt.Println("  I could not finish reading that folder — see the message above.")
		return
	}

	if previous == "" {
		fmt.Println()
		fmt.Println("  That is the \"before\" picture, and it is saved.")
		fmt.Println()
		fmt.Println("  Nothing to compare it with yet. Install what you were going to")
		fmt.Println("  install, use it, uninstall it when you are done, then double-click")
		fmt.Println("  AppSweep again and pick the same folder. It will list every file")
		fmt.Println("  the uninstaller left behind.")
		return
	}

	fmt.Println()
	fmt.Printf("  Comparing against the picture taken %s.\n", snapshotWhen(previous))
	fmt.Println()
	if code := cmdDiff([]string{"--before", previous, "--after", current}); code != 0 {
		fmt.Println()
		fmt.Println("  I could not compare the two — see the message above.")
	}
}

// snapshotStore is where guided runs keep their records: a plainly named
// folder in the reader's home directory, so they can find, copy or delete them
// without needing this program.
func snapshotStore() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	store := filepath.Join(home, "AppSweep-snapshots")
	if err := os.MkdirAll(store, 0o755); err != nil {
		return "", err
	}
	return store, nil
}

// snapshotName builds a file name that is unique per folder and per moment, so
// a new record can never land on top of an older one.
func snapshotName(root string, at time.Time) string {
	return "snapshot-" + slug(root) + "-" + at.Format("20060102-150405") + ".json"
}

// snapshotPrefix is the part of the name shared by every record of one folder.
func snapshotPrefix(root string) string {
	return "snapshot-" + slug(root) + "-"
}

// slug turns a path into something safe to put in a file name, while staying
// recognizable to a person browsing the folder.
func slug(p string) string {
	var b strings.Builder
	for _, r := range p {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
			b.WriteRune(r)
		case r == '-', r == '_':
			b.WriteRune(r)
		default:
			b.WriteRune('_')
		}
	}
	s := strings.Trim(b.String(), "_")
	if s == "" {
		s = "folder"
	}
	if len(s) > 60 {
		s = s[len(s)-60:]
	}
	return s
}

// latestSnapshotFor returns the newest earlier record of this folder, or "" if
// this is the first time the folder has been looked at. The timestamp in the
// name sorts the same way it reads, so plain string order is date order.
func latestSnapshotFor(store, root string) string {
	entries, err := os.ReadDir(store)
	if err != nil {
		return ""
	}
	prefix := snapshotPrefix(root)
	var names []string
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if strings.HasPrefix(name, prefix) && strings.HasSuffix(name, ".json") {
			names = append(names, name)
		}
	}
	if len(names) == 0 {
		return ""
	}
	sort.Strings(names)
	return filepath.Join(store, names[len(names)-1])
}

// snapshotWhen reads the date back out of a record's file name, so the reader
// is told when the earlier picture was taken in words they can check.
func snapshotWhen(path string) string {
	base := strings.TrimSuffix(filepath.Base(path), ".json")
	if len(base) < 15 {
		return "earlier"
	}
	stamp := base[len(base)-15:]
	at, err := time.Parse("20060102-150405", stamp)
	if err != nil {
		return "earlier"
	}
	return at.Format("2 January 2006 at 15:04")
}

// suggestedFolder offers somewhere worth watching that is certain to exist, so
// the reader can get going by pressing one key.
func suggestedFolder() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	// AppData\Roaming is where Windows installers scatter the most, and it is
	// the folder uninstallers most often fail to clear out.
	for _, parts := range [][]string{
		{"AppData", "Roaming"},
		{"Library", "Application Support"},
	} {
		candidate := filepath.Join(append([]string{home}, parts...)...)
		if info, err := os.Stat(candidate); err == nil && info.IsDir() {
			return candidate
		}
	}
	return home
}

// pause keeps the console window open. Explorer closes it the moment the
// process exits, so without this the reader never sees the output.
func pause(in *bufio.Scanner) {
	fmt.Println()
	fmt.Print("  Press Enter to close this window. ")
	in.Scan()
}
