// StorageLens is a multi-target, read-only storage audit tool.
//
// It scans several independent directories (targets) and produces a single
// consolidated report ranked by total size, so an IT admin can quickly see
// which of their monitored locations is closest to being a capacity
// problem. Unlike a single-target interactive space map, StorageLens is
// built for surveying many locations at once and exporting the result.
package main

import (
	"encoding/csv"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const toolName = "storagelens"

func main() {
	args := os.Args[1:]
	if len(args) == 0 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// one question the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}

	switch args[0] {
	case "-h", "--help", "help":
		printHelp()
		return
	case "audit":
		runAudit(args[1:])
	default:
		fmt.Fprintf(os.Stderr, "%s: unknown command %q\n\n", toolName, args[0])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprintf(os.Stderr, `usage: %s audit <target1> [<target2> ...] [--top-files N] [--csv report.csv] [--json]

Run '%s help' for details.
`, toolName, toolName)
}

func printHelp() {
	fmt.Printf(`StorageLens - multi-target storage audit for capacity planning

USAGE:
    %s audit <target1> [<target2> ...] [flags]

DESCRIPTION:
    Recursively scans each <target> directory, computing its total size,
    total file count, and largest individual files. Targets are reported
    in a single table sorted by total size descending (biggest storage
    consumer first), followed by a grand total across all targets. The
    top 3 targets by size additionally get a detailed largest-files
    breakdown. StorageLens is read-only: it never deletes or modifies
    anything on disk.

FLAGS:
    --top-files N     Number of largest files to track per target
                       (default 5).
    --csv <file>       Write a CSV report (one row per target) to <file>.
    --json              Print a full JSON report to stdout, including the
                       complete largest-files list for every target (not
                       just the console's top-3-targets-only detail).
    -h, --help          Show this help.

EXAMPLES:
    %s audit C:\Shares\Finance C:\Shares\Eng D:\Backups
    %s audit ./data1 ./data2 ./data3 --top-files 10 --csv report.csv
    %s audit ./data1 ./data2 --json
`, toolName, toolName, toolName, toolName)
}

// reorderFlags works around the stdlib flag package's behavior of stopping
// flag parsing at the first positional argument.
func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

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])
}

// FileEntry is a single file's path and size, used for largest-files lists.
type FileEntry struct {
	Path      string `json:"path"`
	SizeBytes int64  `json:"size_bytes"`
	HumanSize string `json:"human_readable_size"`
}

// TargetResult holds the audit outcome for a single target directory.
type TargetResult struct {
	Path         string      `json:"path"`
	Status       string      `json:"status"` // "ok" or "error"
	Error        string      `json:"error,omitempty"`
	TotalBytes   int64       `json:"total_bytes"`
	HumanSize    string      `json:"human_readable_size"`
	FileCount    int64       `json:"file_count"`
	LargestFiles []FileEntry `json:"largest_files"`
}

func runAudit(args []string) {
	if len(args) == 0 {
		fmt.Fprintln(os.Stderr, "storagelens audit: at least one target directory is required")
		usage()
		os.Exit(1)
	}

	valueFlags := map[string]bool{"top-files": true, "csv": true}
	args = reorderFlags(args, valueFlags)

	flagSet := flag.NewFlagSet("audit", flag.ExitOnError)
	topFiles := flagSet.Int("top-files", 5, "number of largest files to track per target")
	csvPath := flagSet.String("csv", "", "write CSV report to this file")
	jsonOut := flagSet.Bool("json", false, "print full JSON report")
	help := flagSet.Bool("help", false, "show help")

	if err := flagSet.Parse(args); err != nil {
		os.Exit(1)
	}
	if *help {
		printHelp()
		return
	}

	targets := flagSet.Args()
	if len(targets) == 0 {
		fmt.Fprintln(os.Stderr, "storagelens audit: at least one target directory is required")
		usage()
		os.Exit(1)
	}
	if *topFiles < 0 {
		*topFiles = 0
	}

	results := make([]*TargetResult, 0, len(targets))
	for _, t := range targets {
		results = append(results, scanTarget(t, *topFiles))
	}

	// Sort: successful targets by total size descending; error targets
	// sink to the bottom, in their original input order among themselves.
	sort.SliceStable(results, func(i, j int) bool {
		ri, rj := results[i], results[j]
		if ri.Status == "ok" && rj.Status != "ok" {
			return true
		}
		if ri.Status != "ok" && rj.Status == "ok" {
			return false
		}
		if ri.Status == "ok" && rj.Status == "ok" {
			return ri.TotalBytes > rj.TotalBytes
		}
		return false // keep original relative order for two errors
	})

	// When --json is requested, stdout carries ONLY the JSON document so it
	// stays machine-parseable; the human-readable table and any status
	// notes go to stderr instead.
	if *jsonOut {
		printConsoleReportTo(os.Stderr, results, *topFiles)
	} else {
		printConsoleReportTo(os.Stdout, results, *topFiles)
	}

	if *csvPath != "" {
		if err := writeCSVReport(*csvPath, results); err != nil {
			fmt.Fprintf(os.Stderr, "storagelens: failed to write CSV report: %v\n", err)
			os.Exit(1)
		}
		fmt.Fprintf(os.Stderr, "\nCSV report written to %s\n", *csvPath)
	}

	if *jsonOut {
		printJSONReport(results, *topFiles)
	}
}

// scanTarget walks a single target directory and computes its totals and
// largest-files list. It never returns a Go error; scan failures are
// captured in the returned TargetResult so the caller can keep going.
func scanTarget(path string, topN int) *TargetResult {
	res := &TargetResult{Path: path}

	root, err := os.Stat(path)
	if err != nil {
		res.Status = "error"
		res.Error = err.Error()
		return res
	}
	if !root.IsDir() {
		res.Status = "error"
		res.Error = fmt.Sprintf("%s is not a directory", path)
		return res
	}

	var largest []FileEntry
	var totalBytes int64
	var fileCount int64

	walkErr := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
		if err != nil {
			// Skip unreadable entries (e.g. permission denied) but keep
			// scanning the rest of the tree.
			return nil
		}
		if d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			return nil
		}
		size := info.Size()
		totalBytes += size
		fileCount++
		if topN > 0 {
			largest = insertTopFile(largest, FileEntry{Path: p, SizeBytes: size}, topN)
		}
		return nil
	})
	if walkErr != nil {
		res.Status = "error"
		res.Error = walkErr.Error()
		return res
	}

	for i := range largest {
		largest[i].HumanSize = humanBytes(largest[i].SizeBytes)
	}

	res.Status = "ok"
	res.TotalBytes = totalBytes
	res.HumanSize = humanBytes(totalBytes)
	res.FileCount = fileCount
	res.LargestFiles = largest
	return res
}

// insertTopFile keeps `list` sorted descending by size with at most `max`
// entries, inserting `entry` in the correct position if it belongs.
func insertTopFile(list []FileEntry, entry FileEntry, max int) []FileEntry {
	if max <= 0 {
		return list
	}
	idx := sort.Search(len(list), func(i int) bool { return list[i].SizeBytes < entry.SizeBytes })
	if idx >= max {
		return list
	}
	if len(list) < max {
		list = append(list, FileEntry{})
	}
	copy(list[idx+1:], list[idx:len(list)-1])
	list[idx] = entry
	return list
}

func printConsoleReportTo(w io.Writer, results []*TargetResult, topFiles int) {
	fmt.Fprintf(w, "StorageLens audit - %d target(s)\n\n", len(results))

	// Column widths.
	pathW := len("Target")
	for _, r := range results {
		if len(r.Path) > pathW {
			pathW = len(r.Path)
		}
	}

	fmt.Fprintf(w, "%-4s %-*s %12s %10s  %s\n", "Rank", pathW, "Target", "Size", "Files", "Status")
	fmt.Fprintln(w, strings.Repeat("-", 4+1+pathW+1+12+1+10+2+6))

	var grandBytes int64
	var grandFiles int64
	var okCount, errCount int

	for i, r := range results {
		rank := fmt.Sprintf("%d", i+1)
		if r.Status == "ok" {
			okCount++
			grandBytes += r.TotalBytes
			grandFiles += r.FileCount
			fmt.Fprintf(w, "%-4s %-*s %12s %10d  OK\n", rank, pathW, r.Path, r.HumanSize, r.FileCount)
		} else {
			errCount++
			fmt.Fprintf(w, "%-4s %-*s %12s %10s  ERROR: %s\n", rank, pathW, r.Path, "-", "-", r.Error)
		}
	}

	fmt.Fprintln(w, strings.Repeat("-", 4+1+pathW+1+12+1+10+2+6))
	fmt.Fprintf(w, "Grand total: %s across %d file(s) in %d target(s) (%d OK, %d failed)\n",
		humanBytes(grandBytes), grandFiles, len(results), okCount, errCount)

	// Detail breakdown for the top 3 successful targets by size.
	detailCount := 0
	for _, r := range results {
		if r.Status != "ok" {
			continue
		}
		if detailCount >= 3 {
			break
		}
		detailCount++
		fmt.Fprintf(w, "\n--- Largest files in %s (top %d of %d) ---\n", r.Path, len(r.LargestFiles), r.FileCount)
		if len(r.LargestFiles) == 0 {
			fmt.Fprintln(w, "  (no files, or --top-files is 0)")
			continue
		}
		for i, f := range r.LargestFiles {
			fmt.Fprintf(w, "  %2d. %10s  %s\n", i+1, f.HumanSize, f.Path)
		}
	}
	_ = topFiles
}

func writeCSVReport(path string, results []*TargetResult) error {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()

	w := csv.NewWriter(f)
	defer w.Flush()

	if err := w.Write([]string{"path", "total_bytes", "human_readable_size", "file_count", "status"}); err != nil {
		return err
	}

	for _, r := range results {
		if r.Status == "ok" {
			row := []string{
				r.Path,
				fmt.Sprintf("%d", r.TotalBytes),
				r.HumanSize,
				fmt.Sprintf("%d", r.FileCount),
				"ok",
			}
			if err := w.Write(row); err != nil {
				return err
			}
		} else {
			row := []string{
				r.Path,
				"0",
				"",
				"0",
				"error: " + r.Error,
			}
			if err := w.Write(row); err != nil {
				return err
			}
		}
	}

	w.Flush()
	return w.Error()
}

// jsonReport is the top-level structure printed by --json. It intentionally
// includes every target's complete largest-files list, not just the
// console's top-3-targets-only detail view.
type jsonReport struct {
	Targets             []*TargetResult `json:"targets"`
	TopFilesLimit       int             `json:"top_files_limit"`
	GrandTotalBytes     int64           `json:"grand_total_bytes"`
	GrandTotalHuman     string          `json:"grand_total_human_readable"`
	GrandTotalFileCount int64           `json:"grand_total_file_count"`
	TargetCount         int             `json:"target_count"`
	SuccessCount        int             `json:"success_count"`
	ErrorCount          int             `json:"error_count"`
}

func printJSONReport(results []*TargetResult, topFiles int) {
	report := jsonReport{
		Targets:       results,
		TopFilesLimit: topFiles,
		TargetCount:   len(results),
	}
	for _, r := range results {
		if r.Status == "ok" {
			report.SuccessCount++
			report.GrandTotalBytes += r.TotalBytes
			report.GrandTotalFileCount += r.FileCount
		} else {
			report.ErrorCount++
		}
	}
	report.GrandTotalHuman = humanBytes(report.GrandTotalBytes)

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(report); err != nil {
		fmt.Fprintf(os.Stderr, "storagelens: failed to encode JSON report: %v\n", err)
		os.Exit(1)
	}
}
