package main

import (
	crand "crypto/rand"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"

	"quantum-passgen/internal/entropy"
	"quantum-passgen/internal/passgen"
)

func main() {
	if len(os.Args) > 1 && os.Args[1] == "entropy" {
		runEntropy(os.Args[2:])
		return
	}
	runGenerate(os.Args[1:])
}

func runGenerate(args []string) {
	opts := passgen.DefaultOptions()
	fs := flag.NewFlagSet("qpass", flag.ExitOnError)
	count := fs.Int("count", 1, "number of passwords to generate")
	stats := fs.Bool("stats", false, "print alphabet size and estimated entropy to stderr")
	rngMode := fs.String("rng", "os", "random engine: os or hybrid")
	entropyFile := fs.String("entropy-file", "", "external entropy file/device; use - for stdin")
	entropyDevice := fs.String("entropy-device", "", "external entropy device path")
	entropyStdin := fs.Bool("entropy-stdin", false, "read external entropy from stdin")
	quantumFile := fs.String("quantum-file", "", "deprecated alias for -entropy-file")
	requireExternal := fs.Bool("require-external", false, "fail if external entropy cannot provide enough bytes")
	replaceGlobal := fs.Bool("replace-global-rand", false, "advanced: install selected engine as crypto/rand.Reader for this process")

	fs.IntVar(&opts.Length, "length", opts.Length, "password length")
	fs.BoolVar(&opts.UseLower, "lower", opts.UseLower, "include lowercase letters")
	fs.BoolVar(&opts.UseUpper, "upper", opts.UseUpper, "include uppercase letters")
	fs.BoolVar(&opts.UseDigits, "digits", opts.UseDigits, "include digits")
	fs.BoolVar(&opts.UseSymbols, "symbols", opts.UseSymbols, "include symbols")
	fs.Parse(args)

	if *count <= 0 {
		exitf("count must be positive")
	}

	random, hybrid, cleanup := configureRandom(randomConfig{
		mode:            *rngMode,
		entropyFile:     *entropyFile,
		entropyDevice:   *entropyDevice,
		entropyStdin:    *entropyStdin,
		quantumFile:     *quantumFile,
		requireExternal: *requireExternal,
		rngFlagSet:      flagWasSet(fs, "rng"),
	})
	defer cleanup()
	opts.Random = random

	if *replaceGlobal {
		restore := entropy.InstallGlobal(random)
		defer restore()
	}

	for i := 0; i < *count; i++ {
		password, err := passgen.Generate(opts)
		if err != nil {
			exitf("%v", err)
		}
		fmt.Println(password)
	}

	if *stats {
		alphabetSize := passgen.AlphabetSize(opts)
		entropyBits, err := passgen.EntropyBits(opts.Length, alphabetSize)
		if err != nil {
			exitf("%v", err)
		}
		fmt.Fprintf(os.Stderr, "alphabet=%d estimated_entropy=%.1f bits\n", alphabetSize, entropyBits)
	}

	if hybrid != nil && hybrid.Degraded() {
		fmt.Fprintf(os.Stderr, "qpass: warning: external entropy degraded; continued with OS CSPRNG: %v\n", hybrid.DegradeError())
	}
}

type randomConfig struct {
	mode            string
	entropyFile     string
	entropyDevice   string
	entropyStdin    bool
	quantumFile     string
	requireExternal bool
	rngFlagSet      bool
}

func configureRandom(cfg randomConfig) (io.Reader, *entropy.HybridReader, func()) {
	cleanup := func() {}
	source, closeSource := openExternalSource(cfg)
	if closeSource != nil {
		cleanup = closeSource
	}

	mode := strings.ToLower(strings.TrimSpace(cfg.mode))
	if mode == "" {
		mode = "os"
	}
	if source != nil && mode == "os" && !cfg.rngFlagSet {
		mode = "hybrid"
	}

	switch mode {
	case "os":
		if source != nil {
			cleanup()
			exitf("-rng os cannot be combined with an external entropy source")
		}
		return crand.Reader, nil, cleanup
	case "hybrid":
		if source == nil {
			cleanup()
			exitf("-rng hybrid requires -entropy-file, -entropy-device, -entropy-stdin, or -quantum-file")
		}
		reader, err := entropy.NewHybridReader(crand.Reader, source, entropy.HybridOptions{
			RequireExternal: cfg.requireExternal,
		})
		if err != nil {
			cleanup()
			exitf("configure hybrid entropy: %v", err)
		}
		return reader, reader, cleanup
	default:
		cleanup()
		exitf("unknown rng mode %q", cfg.mode)
		return nil, nil, cleanup
	}
}

func openExternalSource(cfg randomConfig) (io.Reader, func()) {
	sources := 0
	sourceLabel := ""
	sourcePath := ""

	if cfg.quantumFile != "" {
		sources++
		sourceLabel = "-quantum-file"
		sourcePath = cfg.quantumFile
	}
	if cfg.entropyFile != "" {
		sources++
		sourceLabel = "-entropy-file"
		sourcePath = cfg.entropyFile
	}
	if cfg.entropyDevice != "" {
		sources++
		sourceLabel = "-entropy-device"
		sourcePath = cfg.entropyDevice
	}
	if cfg.entropyStdin {
		sources++
		sourceLabel = "-entropy-stdin"
		sourcePath = "-"
	}

	if sources == 0 {
		return nil, nil
	}
	if sources > 1 {
		exitf("choose only one external entropy source")
	}
	if sourcePath == "-" {
		return os.Stdin, nil
	}

	file, err := os.Open(sourcePath)
	if err != nil {
		exitf("open %s: %v", sourceLabel, err)
	}
	return file, func() {
		if err := file.Close(); err != nil {
			fmt.Fprintf(os.Stderr, "qpass: warning: close %s: %v\n", sourceLabel, err)
		}
	}
}

func flagWasSet(fs *flag.FlagSet, name string) bool {
	found := false
	fs.Visit(func(f *flag.Flag) {
		if f.Name == name {
			found = true
		}
	})
	return found
}

func runEntropy(args []string) {
	if len(args) == 0 {
		exitf("usage: qpass entropy status|test")
	}

	switch args[0] {
	case "status":
		runEntropyStatus(args[1:])
	case "test":
		runEntropyTest(args[1:])
	default:
		exitf("unknown entropy command %q", args[0])
	}
}

func runEntropyStatus(args []string) {
	fs := flag.NewFlagSet("qpass entropy status", flag.ExitOnError)
	fs.Parse(args)

	var b [1]byte
	if _, err := io.ReadFull(crand.Reader, b[:]); err != nil {
		exitf("OS CSPRNG unavailable: %v", err)
	}

	fmt.Println("os_csprng=available")
	fmt.Println("external_source=not_configured")
	fmt.Println("global_override=disabled_by_default")
}

func runEntropyTest(args []string) {
	fs := flag.NewFlagSet("qpass entropy test", flag.ExitOnError)
	entropyFile := fs.String("entropy-file", "", "external entropy file/device; use - for stdin")
	entropyDevice := fs.String("entropy-device", "", "external entropy device path")
	entropyStdin := fs.Bool("entropy-stdin", false, "read external entropy from stdin")
	quantumFile := fs.String("quantum-file", "", "deprecated alias for -entropy-file")
	requireExternal := fs.Bool("require-external", false, "fail if external entropy cannot provide enough bytes")
	bytesToRead := fs.Int("bytes", 64, "number of bytes to read without printing them")
	fs.Parse(args)

	if *bytesToRead <= 0 {
		exitf("bytes must be positive")
	}

	random, hybrid, cleanup := configureRandom(randomConfig{
		mode:            "hybrid",
		entropyFile:     *entropyFile,
		entropyDevice:   *entropyDevice,
		entropyStdin:    *entropyStdin,
		quantumFile:     *quantumFile,
		requireExternal: *requireExternal,
	})
	defer cleanup()

	buf := make([]byte, *bytesToRead)
	if _, err := io.ReadFull(random, buf); err != nil {
		exitf("entropy test failed: %v", err)
	}

	fmt.Printf("mode=hybrid\n")
	fmt.Printf("read_bytes=%d\n", len(buf))
	fmt.Printf("external_degraded=%t\n", hybrid.Degraded())
	if hybrid.Degraded() {
		fmt.Printf("degrade_error=%v\n", hybrid.DegradeError())
	}
}

func exitf(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "qpass: "+format+"\n", args...)
	os.Exit(1)
}
