package main

import (
	"bufio"
	"crypto/aes"
	"crypto/cipher"
	"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 two things
// 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.
//
// Of VaultZip's three commands, this session runs only "verify". Packing
// writes a new vault and unpacking writes files all over a folder; neither is
// something to start on somebody's behalf because they double-clicked an icon.
// Verify opens nothing, extracts nothing and writes nothing — it proves the
// password is right and the contents are intact, which is exactly the question
// somebody has when they are looking at a vault they made months ago.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  VaultZip")
	fmt.Println("  Checks a password-protected vault: that the password is right, and")
	fmt.Println("  that everything inside is still intact and undamaged.")
	fmt.Println()
	fmt.Println("  Nothing is unpacked and nothing is written. This only reads the")
	fmt.Println("  vault file and tells you what is in it.")
	fmt.Println()

	suggested := suggestedVault()
	var vault string
	for {
		fmt.Println("  Which vault file shall I check?")
		fmt.Println("  (a .vz file made by VaultZip)")
		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 vault file to check. Vaults are made with the")
			fmt.Println("  command-line version: vaultzip --help")
			fmt.Println("  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 vault file — or the folder holding it —")
			fmt.Println("  from Explorer onto this window to paste its location, then")
			fmt.Println("  press Enter.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged-in folder is a common answer; look inside it rather
			// than sending the reader away to find the file themselves.
			found := newestVaultIn(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I cannot see a .vz vault in it.\n", answer)
				fmt.Println("  Give me the vault file itself.")
				fmt.Println()
				continue
			}
			fmt.Printf("\n  Using the vault I found in that folder: %s\n", found)
			answer = found
		}

		if !looksLikeVault(answer) {
			fmt.Println()
			fmt.Printf("  %q does not look like a VaultZip vault — the file does not\n", answer)
			fmt.Println("  start with VaultZip's marker. Vaults usually end in .vz.")
			fmt.Println()
			continue
		}

		vault = answer
		break
	}

	for {
		fmt.Println()
		fmt.Println("  What is the vault's password?")
		fmt.Println("  (it will be visible as you type — nobody is looking over your")
		fmt.Println("  shoulder, I hope)")
		fmt.Print("  > ")

		if !in.Scan() {
			return
		}
		password := strings.TrimSpace(in.Text())
		if password == "" {
			fmt.Println()
			fmt.Println("  A vault always has a password. Try again, or close this window.")
			continue
		}

		fmt.Println()
		fmt.Println("  Checking. Unlocking a vault is deliberately slow — a few seconds.")
		fmt.Println()
		if err := vaultUnlocks(vault, password); err != nil {
			fmt.Printf("  %s\n", err)
			fmt.Println("  Passwords are case-sensitive. Try again, or close this window.")
			continue
		}

		// The password is known good, so the real verify can no longer fail on
		// it and take the window down with it.
		cmdUnpack([]string{vault, "--password", password}, false)
		break
	}

	fmt.Println()
	fmt.Println("  Done. Nothing was extracted — this was a check, not an unpack.")
	fmt.Println("  The command-line version can make new vaults and unpack this one:")
	fmt.Println("  vaultzip --help")
	pause(in)
}

// vaultUnlocks answers "will this password open this vault" without printing a
// Go error at the reader and, more importantly, without the process exiting on
// a wrong password — which would slam the console window shut mid-question.
// It uses exactly the check the real verify uses: AES-GCM authentication.
func vaultUnlocks(path, password string) error {
	raw, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("I could not read %s.", filepath.Base(path))
	}
	if len(raw) < len(magic)+saltLen+nonceLen || string(raw[:len(magic)]) != magic {
		return fmt.Errorf("%s is not a VaultZip vault.", filepath.Base(path))
	}
	off := len(magic)
	salt := raw[off : off+saltLen]
	off += saltLen
	nonce := raw[off : off+nonceLen]
	off += nonceLen

	block, err := aes.NewCipher(stretchKey(password, salt))
	if err != nil {
		return fmt.Errorf("I could not set up the decryption for %s.", filepath.Base(path))
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return fmt.Errorf("I could not set up the decryption for %s.", filepath.Base(path))
	}
	if _, err := gcm.Open(nil, nonce, raw[off:], nil); err != nil {
		return fmt.Errorf("That password does not open this vault (or the vault has been damaged).")
	}
	return nil
}

// looksLikeVault checks the file's own marker rather than its name, so a vault
// that was renamed is still recognised and an ordinary file is turned away
// before it reaches code that would exit on it.
func looksLikeVault(path string) bool {
	f, err := os.Open(path)
	if err != nil {
		return false
	}
	defer f.Close()
	head := make([]byte, len(magic))
	if _, err := f.Read(head); err != nil {
		return false
	}
	return string(head) == magic
}

// suggestedVault offers a vault the reader is likely to have in mind, so the
// common case is one keypress. It returns "" when there is nothing to offer,
// and the prompt copes with that.
func suggestedVault() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, home,
			filepath.Join(home, "Downloads"),
			filepath.Join(home, "Documents"),
			filepath.Join(home, "Desktop"))
	}
	for _, dir := range dirs {
		if found := newestVaultIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// newestVaultIn returns the most recently modified .vz file directly inside
// dir, or "" if there is none. Only the top level is searched: a guided
// default should be instant, not a disk crawl.
func newestVaultIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	type candidate struct {
		path  string
		mtime int64
	}
	var found []candidate
	for _, e := range entries {
		if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".vz") {
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		found = append(found, candidate{filepath.Join(dir, e.Name()), info.ModTime().UnixNano()})
	}
	if len(found) == 0 {
		return ""
	}
	sort.Slice(found, func(i, j int) bool { return found[i].mtime > found[j].mtime })
	return found[0].path
}

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