package main

import (
	"bufio"
	"encoding/json"
	"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 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.
//
// Guided mode only reads and hashes. It writes no manifest file and changes
// nothing it looks at.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  BootForge")
	fmt.Println("  Proves a download or a burned copy is the file you think it is.")
	fmt.Println()
	fmt.Println("  Point it at a disc image or a folder and it works out the SHA-256")
	fmt.Println("  fingerprint of everything inside. Compare that against the one the")
	fmt.Println("  publisher lists and you know your copy arrived whole and unaltered.")
	fmt.Println()
	fmt.Println("  If BootForge has a stored list of fingerprints for what you pick, it")
	fmt.Println("  checks against that instead and tells you what has changed since.")
	fmt.Println()
	fmt.Println("  It only reads. Nothing is written, moved or changed.")
	fmt.Println()

	suggested := suggestedTarget()
	for {
		fmt.Println("  Which file or folder shall I check?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Println("  Tip: you can drag a file or a folder from Explorer onto this")
		fmt.Println("  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 something to check. Try again, or close this window.")
			fmt.Println()
			continue
		}

		if _, err := os.Stat(answer); err != nil {
			fmt.Println()
			fmt.Printf("  I cannot find %q.\n", answer)
			fmt.Println("  Tip: you can drag a file or a folder from Explorer onto this")
			fmt.Println("  window to paste its location, then press Enter.")
			fmt.Println()
			continue
		}

		fmt.Println()
		fmt.Println("  Reading and hashing. A DVD-sized image takes a minute or two.")
		fmt.Println()
		guidedCheck(answer)
		break
	}

	fmt.Println()
	fmt.Println("  Done. To save a fingerprint list you can check against later, the")
	fmt.Println("  command-line version writes one: bootforge help")
	pause(in)
}

// guidedCheck hashes what the reader chose and either compares it against a
// stored manifest sitting next to it, or simply shows the fingerprints.
//
// Both halves are done here rather than by calling cmdVerify, because that
// function ends in os.Exit on a mismatch — which would close the window on the
// exact moment the reader most needs to read.
func guidedCheck(target string) {
	current, err := scanPath(target)
	if err != nil {
		fmt.Printf("  I could not read through %s: %v\n", target, err)
		return
	}
	if len(current) == 0 {
		fmt.Println("  There are no ordinary files in there to fingerprint.")
		return
	}

	var total int64
	for _, e := range current {
		total += e.Size
	}

	if m := loadNearbyManifest(target); m != nil {
		compareAgainst(m, current)
		return
	}

	fmt.Printf("  %d file(s), %s in total.\n", len(current), humanBytes(total))
	fmt.Println()
	sort.Slice(current, func(i, j int) bool { return current[i].Path < current[j].Path })
	const shown = 40
	for i, e := range current {
		if i == shown {
			fmt.Printf("    ... and %d more\n", len(current)-shown)
			break
		}
		fmt.Printf("    %s\n", e.SHA256)
		fmt.Printf("      %s  (%s)\n", e.Path, humanBytes(e.Size))
	}
	fmt.Println()
	fmt.Println("  Compare a fingerprint above with the one on the page you downloaded")
	fmt.Println("  from. If they match character for character, your copy is sound.")
}

// compareAgainst reports how what is on disk now differs from a stored list.
func compareAgainst(m *Manifest, current []FileEntry) {
	expected := make(map[string]FileEntry, len(m.Files))
	for _, e := range m.Files {
		expected[e.Path] = e
	}
	found := make(map[string]FileEntry, len(current))
	for _, e := range current {
		found[e.Path] = e
	}

	var okCount int
	var mismatched, missing, extra []string

	expPaths := make([]string, 0, len(expected))
	for p := range expected {
		expPaths = append(expPaths, p)
	}
	sort.Strings(expPaths)
	for _, p := range expPaths {
		got, ok := found[p]
		switch {
		case !ok:
			missing = append(missing, p)
		case got.SHA256 != expected[p].SHA256:
			mismatched = append(mismatched, p)
		default:
			okCount++
		}
	}
	for p := range found {
		if _, ok := expected[p]; !ok {
			extra = append(extra, p)
		}
	}
	sort.Strings(extra)

	if m.GeneratedAtUTC != "" {
		fmt.Printf("  Checked against the list recorded %s.\n", m.GeneratedAtUTC)
	}
	fmt.Println()
	for _, p := range mismatched {
		fmt.Printf("    CHANGED  %s\n", p)
	}
	for _, p := range missing {
		fmt.Printf("    GONE     %s\n", p)
	}
	for _, p := range extra {
		fmt.Printf("    NEW      %s\n", p)
	}
	if len(mismatched) > 0 || len(missing) > 0 || len(extra) > 0 {
		fmt.Println()
	}
	fmt.Printf("  %d unchanged, %d changed, %d gone, %d new.\n",
		okCount, len(mismatched), len(missing), len(extra))
	fmt.Println()
	switch {
	case len(mismatched) > 0 || len(missing) > 0:
		fmt.Println("  Something is not as it was. A changed or missing file in a boot")
		fmt.Println("  image means the copy is no longer trustworthy — fetch it again.")
	case len(extra) > 0:
		fmt.Println("  Everything recorded is still intact; there are simply files here")
		fmt.Println("  that were not in the list.")
	default:
		fmt.Println("  Everything matches. Nothing has changed since that list was made.")
	}
}

// loadNearbyManifest finds and reads a fingerprint list belonging to target,
// or returns nil if there is not one to be found.
func loadNearbyManifest(target string) *Manifest {
	var candidates []string
	candidates = append(candidates, target+".bfm")
	if info, err := os.Stat(target); err == nil && info.IsDir() {
		candidates = append(candidates,
			filepath.Join(target, "manifest.bfm"),
			filepath.Join(target, "bootforge.bfm"))
	} else {
		dir := filepath.Dir(target)
		base := strings.TrimSuffix(filepath.Base(target), filepath.Ext(target))
		candidates = append(candidates, filepath.Join(dir, base+".bfm"))
	}

	for _, c := range candidates {
		data, err := os.ReadFile(c)
		if err != nil {
			continue
		}
		var m Manifest
		if err := json.Unmarshal(data, &m); err != nil {
			continue
		}
		if m.Files == nil {
			continue
		}
		fmt.Printf("  Found a stored fingerprint list: %s\n", c)
		return &m
	}
	return nil
}

// suggestedTarget offers something worth checking that is certain to exist, so
// the reader can get going by pressing one key: a disc image in one of the
// usual places, or failing that the folder those images land in.
func suggestedTarget() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	dirs := []string{
		filepath.Join(home, "Downloads"),
		filepath.Join(home, "Documents"),
		filepath.Join(home, "Desktop"),
	}
	for _, dir := range dirs {
		if img := imageInDir(dir); img != "" {
			return img
		}
	}
	for _, dir := range dirs {
		if info, err := os.Stat(dir); err == nil && info.IsDir() {
			return dir
		}
	}
	return home
}

// imageInDir returns the first disc image directly inside dir, or "".
func imageInDir(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		switch strings.ToLower(filepath.Ext(e.Name())) {
		case ".iso", ".img", ".wim", ".esd":
			names = append(names, e.Name())
		}
	}
	if len(names) == 0 {
		return ""
	}
	sort.Strings(names)
	return filepath.Join(dir, names[0])
}

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