package main

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

// archiveExtensions are the names an archive usually goes by. zipdock detects
// the real format from the file's own magic bytes, not its name, so this list
// is only used to offer a sensible default and to recognise an archive inside a
// folder the reader dragged in.
var archiveExtensions = []string{
	".zip", ".tar", ".tgz", ".gz", ".bz2", ".tbz", ".tbz2", ".jar",
}

// 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.
//
// zipdock is read-only by design: it never extracts and never modifies an
// archive, so the guided session runs its two most useful commands in full.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  ZipDock")
	fmt.Println("  Look inside a zip or other archive before you open it.")
	fmt.Println()
	fmt.Println("  It says what the archive really is, what is in it and how big it")
	fmt.Println("  unpacks to, then checks it for the tricks a malicious archive uses:")
	fmt.Println("  files that explode to fill your disk, entries that try to write")
	fmt.Println("  outside the folder you unpack into, and names Windows cannot handle.")
	fmt.Println()
	fmt.Println("  Nothing is extracted and nothing is changed. It only reads.")
	fmt.Println()

	suggested := suggestedArchive()
	for {
		fmt.Println("  Which archive shall I look inside?")
		fmt.Println("  (a .zip, .tar, .tar.gz, .gz or .bz2 file)")
		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 an archive to look at. 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 archive — 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 := newestArchiveIn(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I cannot see an archive in it.\n", answer)
				fmt.Println("  Give me the archive file itself.")
				fmt.Println()
				continue
			}
			fmt.Printf("\n  Using the archive I found in that folder: %s\n", found)
			answer = found
		case info.Size() == 0:
			fmt.Println()
			fmt.Printf("  %q is empty — zero bytes. Whatever download or copy\n", answer)
			fmt.Println("  produced it did not finish.")
			fmt.Println()
			continue
		}

		fmt.Println()
		fmt.Println("  Reading the archive. A big one takes a moment.")
		fmt.Println()
		if code := cmdInfo([]string{answer}); code == exitError {
			// cmdInfo has already explained what it could not read. Ask again
			// rather than leaving the reader staring at a dead end.
			fmt.Println()
			fmt.Println("  I could not recognise that file as an archive. zipdock reads")
			fmt.Println("  zip, tar, tar.gz, gzip, tar.bz2 and bzip2 files.")
			fmt.Println()
			continue
		}

		fmt.Println()
		fmt.Println("  ---- safety check ----")
		fmt.Println()
		cmdScan([]string{answer})
		break
	}

	fmt.Println()
	fmt.Println("  Done. Nothing was extracted — this was a look, not an unpack.")
	fmt.Println("  There is a command-line version too, which can list every entry")
	fmt.Println("  and verify the checksums: zipdock --help")
	pause(in)
}

// suggestedArchive offers an archive the reader plausibly has in mind, so the
// common case is one keypress. Downloads first: an archive somebody wants
// checked before opening has almost always just been downloaded.
func suggestedArchive() 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,
			filepath.Join(home, "Downloads"),
			filepath.Join(home, "Desktop"),
			filepath.Join(home, "Documents"),
			home)
	}
	for _, dir := range dirs {
		if found := newestArchiveIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// newestArchiveIn returns the most recently modified archive 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 newestArchiveIn(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() || !isArchiveName(e.Name()) {
			continue
		}
		info, err := e.Info()
		if err != nil || info.Size() == 0 {
			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
}

func isArchiveName(name string) bool {
	ext := strings.ToLower(filepath.Ext(name))
	for _, want := range archiveExtensions {
		if ext == want {
			return true
		}
	}
	return false
}

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