package main

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

// 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 questions
// 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.
//
// ArchiveGuard's seal command can securely erase original files. Guided mode
// does not go near it. It runs the checking half only: open a vault's lid,
// confirm the password works, and list what is inside. Nothing is written,
// extracted or erased.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  ArchiveGuard")
	fmt.Println("  Checks that an encrypted vault is still intact and still opens.")
	fmt.Println()
	fmt.Println("  Give it a vault file and its password and it proves the vault can")
	fmt.Println("  be unlocked and lists everything inside, with each file's size and")
	fmt.Println("  fingerprint — so you know your cold storage is really still there.")
	fmt.Println()
	fmt.Println("  Nothing is extracted, written or erased. This only looks inside.")
	fmt.Println()

	vault := askForVault(in)
	if vault == "" {
		return
	}

	fmt.Println()
	fmt.Println("  The password is typed in plain view — nobody should be reading over")
	fmt.Println("  your shoulder.")
	fmt.Println()

	for {
		fmt.Printf("  Password for %s\n", filepath.Base(vault))
		fmt.Print("  > ")
		if !in.Scan() {
			return
		}
		password := strings.TrimSpace(in.Text())
		if password == "" {
			fmt.Println()
			fmt.Println("  A vault needs its password. Type it, or close this window.")
			fmt.Println()
			continue
		}

		fmt.Println()
		fmt.Println("  Unlocking. Checking the password takes a few seconds by design.")
		fmt.Println()
		if guidedVerify(vault, password) {
			break
		}
		fmt.Println("  Type the password again, or close this window.")
		fmt.Println()
	}

	fmt.Println()
	fmt.Println("  Done. Nothing was changed. To take files back out of a vault, or to")
	fmt.Println("  put new ones in, use the command-line version: archiveguard help")
	pause(in)
}

// askForVault gets a readable vault file out of the reader, retrying until it
// has one. It returns "" only when stdin has gone away.
func askForVault(in *bufio.Scanner) string {
	suggested := suggestedVault()
	for {
		fmt.Println("  Which vault shall I check?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		} else {
			fmt.Println("  Tip: you can drag the vault file, or the folder holding it, from")
			fmt.Println("  Explorer onto this window to paste its location.")
		}
		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 vault file to check. 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 the vault file, or the folder holding it, from")
			fmt.Println("  Explorer onto this window to paste its location, then press Enter.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged folder is a fair guess at where the vault lives.
			found := vaultInDir(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I found no vault inside it.\n", answer)
				fmt.Println("  Drag the vault file itself onto this window instead.")
				fmt.Println()
				continue
			}
			fmt.Println()
			fmt.Printf("  That is a folder. Using the vault inside it: %s\n", found)
			return found
		}
		return answer
	}
}

// guidedVerify does exactly what the verify command does — authenticate the
// vault and list its contents — but reports trouble as a sentence and returns
// false instead of calling os.Exit, which would slam the window shut on the
// commonest mistake there is: a mistyped password.
func guidedVerify(vaultPath, password string) bool {
	plaintext, err := decryptVault(vaultPath, password)
	if err != nil {
		fmt.Println("  That did not open the vault.")
		fmt.Println()
		fmt.Println("  Either the password is wrong, or this file is not an ArchiveGuard")
		fmt.Println("  vault, or it has been damaged since it was written. The vault")
		fmt.Println("  itself has not been altered by this attempt.")
		fmt.Println()
		return false
	}
	files, err := untarGz(plaintext)
	if err != nil {
		fmt.Println("  The password was right, but the contents would not unpack:")
		fmt.Printf("  %v\n", err)
		fmt.Println()
		fmt.Println("  That points at a damaged vault rather than a wrong password.")
		fmt.Println()
		return false
	}

	sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
	var total int
	for _, vf := range files {
		total += len(vf.Data)
	}
	fmt.Printf("  Vault OK: password accepted, %d file(s), %s in total.\n", len(files), humanBytes(int64(total)))
	fmt.Println()
	for _, vf := range files {
		fmt.Printf("    %-12s %10s  %s\n", sha256Hex(vf.Data)[:12], humanBytes(int64(len(vf.Data))), vf.Name)
	}
	return true
}

// humanBytes keeps the listing readable for people rather than for scripts.
func humanBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

// vaultInDir returns the first vault file directly inside dir, or "".
func vaultInDir(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		if strings.EqualFold(filepath.Ext(e.Name()), ".vz") {
			names = append(names, e.Name())
		}
	}
	if len(names) == 0 {
		return ""
	}
	sort.Strings(names)
	return filepath.Join(dir, names[0])
}

// suggestedVault offers a vault that is certain to exist, so the reader can
// get going by pressing one key. It returns "" when there is nothing nearby,
// and the prompt asks for a path instead.
func suggestedVault() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	// The folder the program was double-clicked in is the other likely home
	// for a vault somebody wants checked.
	if exe, err := os.Executable(); err == nil {
		dirs = append(dirs, filepath.Dir(exe))
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, home,
			filepath.Join(home, "Documents"),
			filepath.Join(home, "Desktop"),
			filepath.Join(home, "Downloads"))
	}
	for _, dir := range dirs {
		if found := vaultInDir(dir); found != "" {
			return found
		}
	}
	return ""
}

// 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()
}
