// Package escrow creates the PBS recovery-code escrow (doc 03 §8a, slice 7): an R-wrapped,
// zero-knowledge copy of the live PBS client encryption key K. It is the FIRST code that touches
// K and introduces the customer recovery code R.
//
// Secret discipline (overriding):
//   - R (recovery code) is generated with crypto/rand, ≥128 bits, word-list form; surfaced to the
//     customer EXACTLY ONCE (by the caller); NEVER logged, persisted, or returned in a struct that
//     gets logged. GenerateRecoveryCode returns it as a bare string the caller must handle with care.
//   - K (PBS key) is read by location; the live unencrypted K is NEVER modified — Wrap operates on
//     a COPY. K is never logged/printed.
//   - The wrapped blob is opaque ciphertext (a PBS scrypt key file); the hub stores it and never
//     decrypts it (it has no R).
//
// It shells out to `proxmox-backup-client key` (the PBS-native passphrase KDF — no bespoke crypto;
// see documentation/tests/slice7-escrow-spike-findings.md). That binary is a runtime dependency.
package escrow

import (
	"bufio"
	"bytes"
	"crypto/rand"
	_ "embed"
	"fmt"
	"math"
	"math/big"
	"strings"
)

//go:embed eff_large_wordlist.txt
var wordlistRaw []byte

// RecoveryCodeSep joins the words of a recovery code R. It is ALSO the reason for the
// joinSafe filter below: a word that itself contains the separator makes the joined code
// ambiguous to segment by eye, which is unaffordable in the one situation R exists for — a
// customer transcribing it during a disaster. Do not change it: R is consumed as a whole
// passphrase (see Wrap/Unwrap), so the separator is a transcription aid, not a parsed delimiter.
const RecoveryCodeSep = "-"

// wordlist is the EFF large wordlist (the diceware standard for human-transcribed passphrases),
// minus the handful of entries that contain RecoveryCodeSep. Parsed and filtered once at init.
// Sizes are asserted in wordlist_test.go so a wordlist swap cannot silently move the entropy floor.
var wordlist = joinSafe(parseWordlist(wordlistRaw))

// wordlistRawSize is the unfiltered parse length, kept for audit (see WordlistFilteredOut).
var wordlistRawSize = len(parseWordlist(wordlistRaw))

func parseWordlist(raw []byte) []string {
	var w []string
	sc := bufio.NewScanner(bytes.NewReader(raw))
	for sc.Scan() {
		if t := strings.TrimSpace(sc.Text()); t != "" {
			w = append(w, t)
		}
	}
	return w
}

// joinSafe drops every word containing RecoveryCodeSep, so that a generated code always segments
// back into exactly RecoveryCodeWords words. In the EFF large list this removes exactly 4 entries
// (drop-down, felt-tip, t-shirt, yo-yo) of 7776, costing ~0.0007 bits/word — the floor still holds
// (asserted in the tests). Generation-time only: codes already issued remain valid, because R is
// verified as a whole passphrase and is never re-split.
func joinSafe(words []string) []string {
	out := make([]string, 0, len(words))
	for _, w := range words {
		if strings.Contains(w, RecoveryCodeSep) {
			continue
		}
		out = append(out, w)
	}
	return out
}

// RecoveryCodeWords is the number of words in a recovery code R. 10 words from the filtered EFF
// list (7772 words) ≈ 129.2 bits (≥128) — the chosen entropy floor (slice7-escrow-spike-findings.md §4).
const RecoveryCodeWords = 10

// generateWords draws RecoveryCodeWords words uniformly (crypto/rand via big.Int — no modulo bias)
// from list. Split out from GenerateRecoveryCode so tests can drive an unfiltered list and prove
// the filter is what keeps a code segmentable.
func generateWords(list []string) ([]string, error) {
	if len(list) < 2 {
		return nil, fmt.Errorf("escrow: wordlist not loaded (%d words)", len(list))
	}
	n := big.NewInt(int64(len(list)))
	words := make([]string, RecoveryCodeWords)
	for i := range words {
		idx, err := rand.Int(rand.Reader, n)
		if err != nil {
			return nil, fmt.Errorf("escrow: recovery-code rng: %w", err)
		}
		words[i] = list[idx.Int64()]
	}
	return words, nil
}

// GenerateRecoveryCode returns a fresh recovery code R: RecoveryCodeWords words chosen uniformly
// from the filtered EFF large wordlist, joined with RecoveryCodeSep.
//
// SECRET: the returned string is R. Surface it to the customer exactly once; never log or persist it.
func GenerateRecoveryCode() (string, error) {
	words, err := generateWords(wordlist)
	if err != nil {
		return "", err
	}
	return strings.Join(words, RecoveryCodeSep), nil
}

// RecoveryCodeEntropyBits is the approximate entropy of a generated code, for display/audit only
// (never the code itself).
func RecoveryCodeEntropyBits() float64 {
	if len(wordlist) < 2 {
		return 0
	}
	return float64(RecoveryCodeWords) * math.Log2(float64(len(wordlist)))
}

// WordlistSize is the effective (filtered) wordlist length — the draw space. For audit/tests.
func WordlistSize() int { return len(wordlist) }

// WordlistFilteredOut is how many parsed entries joinSafe removed. For audit/tests.
func WordlistFilteredOut() int { return wordlistRawSize - len(wordlist) }
