package main

import (
	"bufio"
	"crypto/rand"
	"fmt"
	"html/template"
	"log"
	"math"
	"math/big"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
	"unicode"
)

type WordData struct {
	ByLen map[int][]string
}

var words WordData

// Symbol alphabets
const (
	fullSymbolAlphabet    = "!@#$%^&*()-+',?"
	reducedSymbolAlphabet = "@#!"
)

type PageData struct {
	// Form values
	WordCount   int
	MinLen      int
	MaxLen      int
	NumLen      int
	SymbolCount int    // symbols per separator
	SymbolSet   string // "full" or "reduced"
	RandCaps    string

	// Stats
	TotalWords        int
	AverageWordLength float64
	BitsPerWord       float64
	BitsPerSeparator  float64
	BitsForCapsTotal  float64
	EffectiveBits     int
	TotalCombinations string

	Error            string
	GeneratedPhrases []string
}

// Single-file HTML template
var pageTmpl = template.Must(template.New("page").Parse(`
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Secure Passphrase Generator</title>
  <style>
    body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; background: #f6f7fb; }
    h1 { margin-bottom: 0.5rem; }
	h2 { margin-top: 0; font-size: 1.2rem; }
    form { background: #fff; padding: 1rem 1.5rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); max-width: 650px; }
    label { display: inline-block; width: 180px; }
    input, select { margin-bottom: 0.5rem; padding: 0.25rem 0.4rem; }
    button { padding: 0.4rem 0.8rem; border-radius: 4px; border: 1px solid #0d6efd; background: #0d6efd; color: #fff; cursor: pointer; }
    button:hover { background: #0b5ed7; }
    table { border-collapse: collapse; margin-top: 1rem; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
    th, td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #eee; }
    th { background: #f0f2f8; text-align: left; }
    tr:last-child td { border-bottom: none; }
    tt { font-family: "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
	ul { padding-left: 1.2rem; margin: 0; }
	li { margin: 0.25rem 0; }
    .error { color: #b00020; margin-top: 0.5rem; }
	.notes { margin-top: 2rem; background: #fff; padding: 1rem 1.5rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); max-width: 750px; }
  </style>
</head>
<body>
  <h1>Secure Passphrase Generator</h1>

  <form method="GET" action="/">
    <div>
      <label for="wordcount">Word count</label>
      <input type="number" id="wordcount" name="wordcount" min="1" value="{{.WordCount}}">
    </div>
    <div>
      <label for="minlen">Min word length</label>
      <input type="number" id="minlen" name="minlen" min="1" value="{{.MinLen}}">
    </div>
    <div>
      <label for="maxlen">Max word length</label>
      <input type="number" id="maxlen" name="maxlen" min="1" value="{{.MaxLen}}">
    </div>
    <div>
      <label for="numlen">Digits per separator</label>
      <input type="number" id="numlen" name="numlen" min="0" value="{{.NumLen}}">
    </div>
    <div>
      <label for="symcount">Symbols per separator</label>
      <input type="number" id="symcount" name="symcount" min="0" value="{{.SymbolCount}}">
    </div>
    <div>
      <label for="symset">Symbol set</label>
      <select id="symset" name="symset">
        <option value="full" {{if or (eq .SymbolSet "") (eq .SymbolSet "full")}}selected{{end}}>
          Full (!@#$%^&*()-+',?)
        </option>
        <option value="reduced" {{if eq .SymbolSet "reduced"}}selected{{end}}>
          Reduced (@#!)
        </option>
      </select>
    </div>	
    <div>
      <label for="randcaps">Capitalization</label>
      <select id="randcaps" name="randcaps">
        <option value="first" {{if eq .RandCaps "first"}}selected{{end}}>Random first letter</option>
        <option value="one"   {{if eq .RandCaps "one"}}selected{{end}}>Random one letter</option>
        <option value="all"   {{if eq .RandCaps "all"}}selected{{end}}>Randomize all letters</option>
        <option value="none"  {{if eq .RandCaps "none"}}selected{{end}}>None</option>
      </select>
    </div>
    <div style="margin-top: 0.5rem;">
      <button type="submit">Generate Passphrase</button>
    </div>
    {{if .Error}}
      <div class="error">{{.Error}}</div>
    {{end}}
  </form>

  {{if not .Error}}
  <table>
    <tr><th colspan="2">Statistics</th></tr>
    <tr><td>Word count</td><td>{{.TotalWords}}</td></tr>
    <tr><td>Average word length</td><td>{{printf "%.2f" .AverageWordLength}}</td></tr>
    <tr><td>Bits per word</td><td>{{printf "%.2f" .BitsPerWord}}</td></tr>
    <tr><td>Bits per separator</td><td>{{printf "%.2f" .BitsPerSeparator}}</td></tr>
    <tr><td>Bits for capitalization</td><td>{{printf "%.2f" .BitsForCapsTotal}}</td></tr>
    <tr><td>Effective passphrase bits</td><td>{{.EffectiveBits}}</td></tr>
    <tr><td>Total possible combinations</td><td>{{.TotalCombinations}}</td></tr>
  </table>

  <table>
    <tr><th>Passphrase</th></tr>
    {{range .GeneratedPhrases}}
      <tr><td><tt>{{.}}</tt></td></tr>
    {{end}}
  </table>
  <div class="notes">
    <h2>Notes</h2>
    <ul>
      <li>
        No data generated by this page is stored on the server at any time. Your passphrases are <strong>never logged or recorded</strong>.
      </li>
      <li>
	    All randomness is generated using Go's crypto/rand package, which uses the Linux kernel's secure random generator (same source as /dev/urandom).
      </li>
      <li>
	    Source code for this generator is available at:<br><a href="https://git.coadcorp.com/nathan/ppgen" target="_blank" style="text-decoration:none; color:#0d6efd;">https://git.coadcorp.com/nathan/ppgen</a>
	  </li>	  
    </ul>
  </div>
  {{end}}  
</body>
</html>
`))

func main() {
	wordlistPath := os.Getenv("WORDLIST")
	if wordlistPath == "" {
		wordlistPath = "words.txt"
	}

	log.Printf("Loading wordlist from %s", wordlistPath)
	wd, err := loadWordlist(wordlistPath)
	if err != nil {
		log.Fatalf("failed to load wordlist: %v", err)
	}
	words = wd
	log.Printf("Loaded %d distinct word lengths", len(words.ByLen))

	// Wrap handler with common log middleware
	http.Handle("/", commonLogMiddleware(http.HandlerFunc(handlePassphrase)))

	port := os.Getenv("PORT")
	if port == "" {
		port = "8000"
	}
	addr := ":" + port
	log.Printf("Listening on %s", addr)
	log.Fatal(http.ListenAndServe(addr, nil))
}

func loadWordlist(path string) (WordData, error) {
	f, err := os.Open(path)
	if err != nil {
		return WordData{}, err
	}
	defer f.Close()

	byLen := make(map[int][]string)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		w := strings.TrimSpace(scanner.Text())
		if w == "" {
			continue
		}
		// Keep only alphabetic words
		ok := true
		for _, r := range w {
			if !unicode.IsLetter(r) {
				ok = false
				break
			}
		}
		if !ok {
			continue
		}
		w = strings.ToLower(w)
		byLen[len(w)] = append(byLen[len(w)], w)
	}
	if err := scanner.Err(); err != nil {
		return WordData{}, err
	}
	return WordData{ByLen: byLen}, nil
}

func handlePassphrase(w http.ResponseWriter, r *http.Request) {
	wordcount := parseIntQuery(r, "wordcount", 3)
	minlen := parseIntQuery(r, "minlen", 1)
	maxlen := parseIntQuery(r, "maxlen", 8)
	numlen := parseIntQuery(r, "numlen", 2)
	symbolCount := parseIntQuery(r, "symcount", 1)
	randcaps := r.URL.Query().Get("randcaps")
	if randcaps == "" {
		randcaps = "first"
	}
	symbolSet := r.URL.Query().Get("symset")
	if symbolSet == "" {
		symbolSet = "full"
	}

	if minlen < 1 {
		minlen = 1
	}
	if maxlen < minlen {
		maxlen = minlen
	}
	if wordcount < 1 {
		wordcount = 1
	}
	if numlen < 0 {
		numlen = 0
	}
	if symbolCount < 0 {
		symbolCount = 0
	}

	data := PageData{
		WordCount:   wordcount,
		MinLen:      minlen,
		MaxLen:      maxlen,
		NumLen:      numlen,
		SymbolCount: symbolCount,
		SymbolSet:   symbolSet,
		RandCaps:    randcaps,
	}

	usewords, totalLen := collectWordsInRange(words.ByLen, minlen, maxlen)
	if len(usewords) == 0 {
		data.Error = "No words available for the specified length range."
		_ = pageTmpl.Execute(w, data)
		return
	}

	totalWords := len(usewords)
	avgLen := float64(totalLen) / float64(totalWords)
	wordBits := math.Log2(float64(totalWords))

	// Determine symbol alphabet based on dropdown & symbolCount
	symAlphabet := ""
	if symbolCount > 0 {
		if symbolSet == "reduced" {
			symAlphabet = reducedSymbolAlphabet
		} else {
			symAlphabet = fullSymbolAlphabet
		}
	}
	symAlphabetSize := len(symAlphabet)

	// Caps
	var capBitsPerWord float64
	var capMult float64
	switch randcaps {
	case "first":
		capBitsPerWord = 1
		capMult = 2
	case "one":
		capBitsPerWord = math.Log2(avgLen + 1)
		capMult = avgLen + 1
	case "all":
		capBitsPerWord = avgLen
		capMult = math.Pow(2, avgLen)
	default:
		capBitsPerWord = 0
		capMult = 1
	}

	// Separators: after every word if any digits or symbols are enabled
	segmentsEnabled := (numlen > 0 || (symbolCount > 0 && symAlphabetSize > 0))
	segments := 0
	if segmentsEnabled {
		segments = wordcount
	}

	// Bits per separator segment
	separatorBitsPerSegment := 0.0
	if segmentsEnabled {
		separatorBitsPerSegment = separatorBits(numlen, symbolCount, symAlphabetSize)
	}

	// Total bits
	passBits := (wordBits+capBitsPerWord)*float64(wordcount) +
		separatorBitsPerSegment*float64(segments)

	// Total combinations ≈
	combosPerWord := math.Pow(float64(totalWords), float64(wordcount))
	combosCaps := math.Pow(capMult, float64(wordcount))
	segmentCombos := 1.0
	if segmentsEnabled {
		segmentCombos = math.Pow(separatorCombos(numlen, symbolCount, symAlphabetSize), float64(segments))
	}
	combos := combosPerWord * combosCaps * segmentCombos

	combosStr := fmt.Sprintf("%.3e", combos)
	if !math.IsInf(combos, 0) && combos < 9.999e15 {
		combosStr = commafyInt(int64(combos + 0.5))
	}

	data.TotalWords = totalWords
	data.AverageWordLength = avgLen
	data.BitsPerWord = wordBits
	data.BitsPerSeparator = separatorBitsPerSegment
	data.BitsForCapsTotal = capBitsPerWord * float64(wordcount)
	data.EffectiveBits = int(passBits + 0.5)
	data.TotalCombinations = combosStr

	data.GeneratedPhrases = generatePassphrases(usewords, wordcount, numlen, symbolCount, symAlphabet, randcaps)

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := pageTmpl.Execute(w, data); err != nil {
		log.Printf("template error: %v", err)
	}
}

func parseIntQuery(r *http.Request, name string, def int) int {
	s := r.URL.Query().Get(name)
	if s == "" {
		return def
	}
	n, err := strconv.Atoi(s)
	if err != nil {
		return def
	}
	return n
}

// collectWordsInRange flattens words[length] into a single slice for [minLen, maxLen].
func collectWordsInRange(byLen map[int][]string, minLen, maxLen int) ([]string, int) {
	var all []string
	totalLen := 0
	for l := minLen; l <= maxLen; l++ {
		ws := byLen[l]
		if len(ws) == 0 {
			continue
		}
		totalLen += len(ws) * l
		all = append(all, ws...)
	}
	return all, totalLen
}

// separatorCombos returns the number of possible distinct separator segments.
func separatorCombos(numlen, symCountPerSeg, symAlphabetSize int) float64 {
	// No separators
	if numlen <= 0 && symCountPerSeg <= 0 {
		return 1
	}
	// Only symbols
	if numlen <= 0 && symCountPerSeg > 0 {
		return math.Pow(float64(symAlphabetSize), float64(symCountPerSeg))
	}
	// Only digits
	if numlen > 0 && symCountPerSeg <= 0 {
		return math.Pow(10, float64(numlen))
	}
	// Mixed: exactly symCountPerSeg symbols and numlen digits
	totalLen := numlen + symCountPerSeg
	return comb(totalLen, symCountPerSeg) *
		math.Pow(float64(symAlphabetSize), float64(symCountPerSeg)) *
		math.Pow(10, float64(numlen))
}

// separatorBits = log2(combos) for a single segment
func separatorBits(numlen, symCountPerSeg, symAlphabetSize int) float64 {
	combos := separatorCombos(numlen, symCountPerSeg, symAlphabetSize)
	if combos <= 0 {
		return 0
	}
	return math.Log2(combos)
}

// generatePassphrases builds N phrases with separators after each word when enabled.
func generatePassphrases(usewords []string, wordcount, numlen, symCountPerSeg int, symAlphabet string, randcaps string) []string {
	const numPhrases = 10
	if len(usewords) == 0 {
		return nil
	}
	segmentsEnabled := (numlen > 0 || (symCountPerSeg > 0 && len(symAlphabet) > 0))
	symRunes := []rune(symAlphabet)

	phrases := make([]string, 0, numPhrases)
	for i := 0; i < numPhrases; i++ {
		var b strings.Builder
		for j := 0; j < wordcount; j++ {
			word := usewords[secureRandInt(len(usewords))]
			word = applyRandCaps(word, randcaps)
			b.WriteString(word)

			if segmentsEnabled {
				segment := makeSeparatorSegment(numlen, symCountPerSeg, symRunes)
				b.WriteString(segment)
			}
		}
		phrases = append(phrases, b.String())
	}
	return phrases
}

// makeSeparatorSegment creates one separator segment.
// Cases:
// - numlen <= 0, symCountPerSeg <= 0: empty
// - numlen <= 0, symCountPerSeg > 0: symbols only
// - numlen > 0, symCountPerSeg <= 0: digits only
// - numlen > 0, symCountPerSeg > 0: mixed with exactly symCountPerSeg symbols.
func makeSeparatorSegment(numlen, symCountPerSeg int, symRunes []rune) string {
	digits := "0123456789"
	symAlphabetSize := len(symRunes)

	if numlen <= 0 && symCountPerSeg <= 0 {
		return ""
	}
	if numlen <= 0 && symCountPerSeg > 0 {
		// Only symbols
		runes := make([]rune, symCountPerSeg)
		for i := 0; i < symCountPerSeg; i++ {
			runes[i] = symRunes[secureRandInt(symAlphabetSize)]
		}
		return string(runes)
	}
	if numlen > 0 && symCountPerSeg <= 0 {
		// Only digits
		var b strings.Builder
		for i := 0; i < numlen; i++ {
			b.WriteByte(digits[secureRandInt(10)])
		}
		return b.String()
	}

	// Mixed: exactly symCountPerSeg symbols and numlen digits
	totalLen := numlen + symCountPerSeg
	runes := make([]rune, 0, totalLen)

	// Add required number of symbols
	for i := 0; i < symCountPerSeg; i++ {
		runes = append(runes, symRunes[secureRandInt(symAlphabetSize)])
	}
	// Add required number of digits
	for i := 0; i < numlen; i++ {
		runes = append(runes, rune(digits[secureRandInt(10)]))
	}

	// Shuffle runes
	shuffleRunes(runes)
	return string(runes)
}

func secureRandInt(n int) int {
	if n <= 0 {
		return 0
	}
	max := big.NewInt(int64(n))
	v, err := rand.Int(rand.Reader, max)
	if err != nil {
		return 0
	}
	return int(v.Int64())
}

func shuffleRunes(r []rune) {
	for i := len(r) - 1; i > 0; i-- {
		j := secureRandInt(i + 1)
		r[i], r[j] = r[j], r[i]
	}
}

func applyRandCaps(word, mode string) string {
	if word == "" {
		return word
	}
	switch mode {
	case "first":
		if secureRandInt(2) == 1 {
			return strings.ToUpper(word[:1]) + word[1:]
		}
	case "one":
		k := secureRandInt(len(word) + 1)
		if k < len(word) {
			return word[:k] + strings.ToUpper(word[k:k+1]) + word[k+1:]
		}
	case "all":
		var out []rune
		for _, r := range word {
			if secureRandInt(2) == 1 {
				out = append(out, unicode.ToUpper(r))
			} else {
				out = append(out, unicode.ToLower(r))
			}
		}
		return string(out)
	}
	return word
}

// comb computes "n choose k" as float64 (safe for small n).
func comb(n, k int) float64 {
	if k < 0 || k > n {
		return 0
	}
	if k == 0 || k == n {
		return 1
	}
	if k > n-k {
		k = n - k
	}
	result := 1.0
	for i := 1; i <= k; i++ {
		result *= float64(n - k + i)
		result /= float64(i)
	}
	return result
}

func commafyInt(n int64) string {
	sign := ""
	if n < 0 {
		sign = "-"
		n = -n
	}
	s := strconv.FormatInt(n, 10)
	if len(s) <= 3 {
		return sign + s
	}
	var parts []string
	for len(s) > 3 {
		parts = append([]string{s[len(s)-3:]}, parts...)
		s = s[:len(s)-3]
	}
	if len(s) > 0 {
		parts = append([]string{s}, parts...)
	}
	return sign + strings.Join(parts, ",")
}

// commonLogMiddleware logs requests in Apache/Nginx-style log format
func commonLogMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		lrw := &loggingResponseWriter{ResponseWriter: w, status: 200}
		start := time.Now()
		next.ServeHTTP(lrw, r)
		duration := time.Since(start)

		ip := r.RemoteAddr
		if xf := r.Header.Get("X-Forwarded-For"); xf != "" {
			ip = xf
		}

		log.Printf(`%s - - [%s] "%s %s %s" %d %d "%s" "%s" %v`,
			ip,
			time.Now().Format("02/Jan/2006:15:04:05 -0700"),
			r.Method,
			r.RequestURI,
			r.Proto,
			lrw.status,
			lrw.bytes,
			r.Referer(),
			r.UserAgent(),
			duration,
		)
	})
}

type loggingResponseWriter struct {
	http.ResponseWriter
	status int
	bytes  int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
	lrw.status = code
	lrw.ResponseWriter.WriteHeader(code)
}

func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
	n, err := lrw.ResponseWriter.Write(b)
	lrw.bytes += n
	return n, err
}
