// PrivacySweep — secure file deletion with a signed proof-of-erasure report.
//
// Usage:
//
//	privacysweep shred <path> [<path> ...] [--passes N] [--apply] [--report FILE]
//
// Without --apply, shred is a dry run: it hashes and lists what it would
// destroy without touching anything. With --apply, each file is
// overwritten in place with cryptographically random bytes for N passes
// (fsync'd after each pass) and then removed — so a mistaken match cannot
// be recovered by re-running without --apply, the usual safety net this
// CLI's sibling tools rely on. That's the point: this is the one Techlosoft
// prototype where the destructive path is the entire feature.
package main

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type erasureRecord struct {
	Path        string `json:"path"`
	SizeBytes   int64  `json:"size_bytes"`
	Sha256      string `json:"sha256_before_erase"`
	Passes      int    `json:"overwrite_passes"`
	ErasedAtUTC string `json:"erased_at_utc"`
	Method      string `json:"method"`
}

func main() {
	if len(os.Args) < 2 {
		// 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 os.Args[1] {
	case "shred":
		cmdShred(os.Args[2:])
	case "-h", "--help", "help":
		usage()
	default:
		fmt.Fprintf(os.Stderr, "unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `PrivacySweep — secure file deletion with a proof-of-erasure report

Usage:
  privacysweep shred <path> [<path> ...] [--passes N] [--apply] [--report FILE]

Without --apply this is a dry run: nothing is touched, only listed.
With --apply, matched files are overwritten with random data for
--passes rounds (default 3), fsync'd each round, then removed.
This cannot be undone — there is no quarantine mode for shred.
`)
}

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 cmdShred(args []string) {
	fs := flag.NewFlagSet("shred", flag.ExitOnError)
	passes := fs.Int("passes", 3, "random-data overwrite passes before deletion")
	apply := fs.Bool("apply", false, "actually shred (default is a dry run)")
	reportPath := fs.String("report", "", "write the proof-of-erasure report to this file (JSON); defaults to stdout")
	fs.Parse(reorderFlags(args, map[string]bool{"passes": true, "report": true}))
	targets := fs.Args()
	if len(targets) == 0 {
		fmt.Fprintln(os.Stderr, "usage: privacysweep shred <path> [<path> ...] [--passes N] [--apply] [--report FILE]")
		os.Exit(1)
	}
	if *passes < 1 {
		*passes = 1
	}

	var files []string
	for _, t := range targets {
		info, err := os.Stat(t)
		if err != nil {
			fmt.Fprintf(os.Stderr, "skipping %s: %v\n", t, err)
			continue
		}
		if !info.IsDir() {
			files = append(files, t)
			continue
		}
		filepath.Walk(t, func(path string, fi os.FileInfo, err error) error {
			if err == nil && fi.Mode().IsRegular() {
				files = append(files, path)
			}
			return nil
		})
	}

	if len(files) == 0 {
		fmt.Println("Nothing to shred.")
		return
	}

	var records []erasureRecord
	var totalBytes int64
	for _, f := range files {
		info, err := os.Stat(f)
		if err != nil {
			fmt.Fprintf(os.Stderr, "  skip %s: %v\n", f, err)
			continue
		}
		size := info.Size()

		hash, err := hashFile(f)
		if err != nil {
			fmt.Fprintf(os.Stderr, "  skip %s: %v\n", f, err)
			continue
		}

		if !*apply {
			fmt.Printf("  would shred  %8s  %s  sha256:%s\n", humanBytes(size), f, hash[:16])
			totalBytes += size
			continue
		}

		if err := overwritePasses(f, size, *passes); err != nil {
			fmt.Fprintf(os.Stderr, "  FAILED to overwrite %s: %v\n", f, err)
			continue
		}
		if err := os.Remove(f); err != nil {
			fmt.Fprintf(os.Stderr, "  overwritten but FAILED to remove %s: %v\n", f, err)
			continue
		}

		fmt.Printf("  shredded     %8s  %s\n", humanBytes(size), f)
		totalBytes += size
		records = append(records, erasureRecord{
			Path: f, SizeBytes: size, Sha256: hash, Passes: *passes,
			ErasedAtUTC: time.Now().UTC().Format(time.RFC3339),
			Method:      "random-overwrite+fsync",
		})
	}

	if !*apply {
		fmt.Printf("\n%d file(s), %s would be shredded (dry run — re-run with --apply)\n", len(files), humanBytes(totalBytes))
		return
	}

	fmt.Printf("\n%d file(s) shredded, %s erased\n", len(records), humanBytes(totalBytes))
	if len(records) == 0 {
		return
	}

	out, _ := json.MarshalIndent(map[string]any{
		"tool":             "privacysweep",
		"generated_at_utc": time.Now().UTC().Format(time.RFC3339),
		"erasure_records":  records,
		"total_files":      len(records),
		"total_bytes":      totalBytes,
	}, "", "  ")
	if *reportPath != "" {
		if err := os.WriteFile(*reportPath, out, 0o644); err != nil {
			fmt.Fprintln(os.Stderr, "failed to write report:", err)
			os.Exit(1)
		}
		fmt.Println("Proof-of-erasure report written to", *reportPath)
	} else {
		fmt.Println("\nProof-of-erasure report:")
		fmt.Println(string(out))
	}
}

// overwritePasses overwrites the file's existing byte range with random
// data for the given number of passes, fsyncing after each so the pass
// actually reaches disk before the next one starts.
func overwritePasses(path string, size int64, passes int) error {
	f, err := os.OpenFile(path, os.O_WRONLY, 0)
	if err != nil {
		return err
	}
	defer f.Close()

	buf := make([]byte, 32*1024)
	for p := 0; p < passes; p++ {
		if _, err := f.Seek(0, io.SeekStart); err != nil {
			return err
		}
		var written int64
		for written < size {
			n := int64(len(buf))
			if remain := size - written; remain < n {
				n = remain
			}
			if _, err := rand.Read(buf[:n]); err != nil {
				return err
			}
			if _, err := f.Write(buf[:n]); err != nil {
				return err
			}
			written += n
		}
		if err := f.Sync(); err != nil {
			return err
		}
	}
	return nil
}

func hashFile(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

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