package escrow

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// pbcBinary is the PBS client CLI the escrow shells out to (the PBS-native key+passphrase KDF).
var pbcBinary = "/usr/bin/proxmox-backup-client"

// Posture is the customer's key-custody posture (doc 03 §8a). Only the zero-knowledge DEFAULT is
// implemented this slice; the other postures are documented (doc 03 §8a) and implemented only when
// a customer chooses them.
type Posture string

const (
	// PostureZeroKnowledge: Felhom storage + customer-only key. The default. Felhom holds the
	// opaque R-wrapped blob (cannot open it) and the customer holds R.
	PostureZeroKnowledge Posture = "zero_knowledge"
)

// DefaultPosture is the posture when none is configured.
const DefaultPosture = PostureZeroKnowledge

// CreateOptions parameterizes one escrow creation.
type CreateOptions struct {
	// KeyPath is the live PBS client encryption key K (e.g. /etc/pve/priv/storage/felhom-pbs.enc).
	// It is read (copied) but NEVER modified.
	KeyPath string
	// Posture is recorded for display/audit; this slice implements only zero-knowledge.
	Posture Posture
	// WantOfflineCopy: opt-in (b) — also return the wrapped blob for the customer to print
	// (still two-factor: useless without R). No extra trust.
	WantOfflineCopy bool
	// WantPaperkey: opt-in (a) — also return the RAW-key paperkey. Single-factor + unrevocable;
	// the caller must surface the loud caveat. Off by default.
	WantPaperkey bool
	// IdentityBundle (slice 10D.1), when set, is ALSO wrapped under the SAME R (via age) → an
	// IdentityBlob in the result. Additive: the K-escrow path is unchanged when nil.
	IdentityBundle *IdentityBundle
}

// CreateResult is the non-secret output of escrow creation. NOTE: the recovery code R is returned
// SEPARATELY (not in this struct) so it can never be accidentally logged via the result.
type CreateResult struct {
	Blob           []byte  // the opaque R-wrapped escrow blob → hub (Phase C). Ciphertext, not K.
	KeyFingerprint string  // the PBS key fingerprint (identifies the key; safe to display)
	Posture        Posture // the posture used
	EntropyBits    float64 // R's approximate entropy (for display; never R itself)
	OfflineCopy    []byte  // (b) the same wrapped blob, if WantOfflineCopy (for the customer to print)
	Paperkey       string  // (a) raw paperkey text, if WantPaperkey — SECRET-adjacent (single factor)
	IdentityBlob   []byte  // (10D.1) the age-wrapped identity bundle under the same R, if IdentityBundle set
}

// Create generates a recovery code R, produces the R-wrapped escrow blob from the live key, and
// SELF-VERIFIES it is recoverable (unwraps a copy with R and checks the key fingerprint matches —
// "an escrow you haven't recovered isn't an escrow"). It returns R SEPARATELY: the caller surfaces
// R to the customer exactly once and must not log it. The live key file is byte-unchanged.
func Create(ctx context.Context, opts CreateOptions) (recoveryCode string, res CreateResult, err error) {
	if opts.KeyPath == "" {
		return "", CreateResult{}, fmt.Errorf("escrow: KeyPath (the live PBS key) is required")
	}
	posture := opts.Posture
	if posture == "" {
		posture = DefaultPosture
	}
	if posture != PostureZeroKnowledge {
		return "", CreateResult{}, fmt.Errorf("escrow: posture %q not implemented this slice (only %q)", posture, PostureZeroKnowledge)
	}

	keyFP, err := KeyFingerprint(ctx, opts.KeyPath)
	if err != nil {
		return "", CreateResult{}, fmt.Errorf("escrow: read key fingerprint: %w", err)
	}

	R, err := GenerateRecoveryCode()
	if err != nil {
		return "", CreateResult{}, err
	}

	work, err := os.MkdirTemp("", "felhom-escrow-")
	if err != nil {
		return "", CreateResult{}, fmt.Errorf("escrow: tempdir: %w", err)
	}
	defer os.RemoveAll(work) // the blob bytes are returned in memory; no plaintext-K temp lingers
	blobPath := filepath.Join(work, "escrow.blob")

	if err := Wrap(ctx, opts.KeyPath, blobPath, R); err != nil {
		return "", CreateResult{}, err
	}

	// Self-verify recoverability: unwrap a COPY with R and confirm the recovered key's fingerprint
	// matches the original. Never ship a blob we can't recover.
	verifyPath := filepath.Join(work, "verify.blob")
	if err := copyFile(blobPath, verifyPath, 0o600); err != nil {
		return "", CreateResult{}, fmt.Errorf("escrow: verify copy: %w", err)
	}
	if err := Unwrap(ctx, verifyPath, R); err != nil {
		return "", CreateResult{}, fmt.Errorf("escrow: self-verify unwrap: %w", err)
	}
	recFP, err := KeyFingerprint(ctx, verifyPath)
	if err != nil {
		return "", CreateResult{}, fmt.Errorf("escrow: self-verify fingerprint: %w", err)
	}
	if recFP != keyFP {
		return "", CreateResult{}, fmt.Errorf("escrow: self-verify FAILED — recovered key fingerprint mismatch (blob is not recoverable)")
	}

	blob, err := os.ReadFile(blobPath)
	if err != nil {
		return "", CreateResult{}, fmt.Errorf("escrow: read blob: %w", err)
	}
	res = CreateResult{Blob: blob, KeyFingerprint: keyFP, Posture: posture, EntropyBits: RecoveryCodeEntropyBits()}
	if opts.WantOfflineCopy {
		res.OfflineCopy = blob // same two-factor blob; customer prints it
	}
	if opts.WantPaperkey {
		pk, err := Paperkey(ctx, opts.KeyPath)
		if err != nil {
			return "", CreateResult{}, fmt.Errorf("escrow: paperkey: %w", err)
		}
		res.Paperkey = pk
	}
	// Slice 10D.1: ALSO wrap the identity bundle under the SAME R (via age), so DR can recover the
	// box's identity with the one recovery code. Self-verify it round-trips before shipping.
	if opts.IdentityBundle != nil {
		idBlob, err := WrapIdentityBundle(ctx, *opts.IdentityBundle, R)
		if err != nil {
			return "", CreateResult{}, fmt.Errorf("escrow: identity wrap: %w", err)
		}
		if _, err := UnwrapIdentityBundle(ctx, idBlob, R); err != nil {
			return "", CreateResult{}, fmt.Errorf("escrow: identity self-verify (not recoverable): %w", err)
		}
		res.IdentityBlob = idBlob
	}
	return R, res, nil
}

// Wrap produces the R-wrapped escrow blob from the live PBS key file: copy K → blob, then re-key
// the copy to kdf=scrypt under R via a pty (spike F-A1/F-A2). The live key file is NOT modified.
func Wrap(ctx context.Context, keyPath, blobPath, recoveryCode string) error {
	if err := copyFile(keyPath, blobPath, 0o600); err != nil {
		return fmt.Errorf("escrow: copy key: %w", err)
	}
	// `key change-passphrase --kdf scrypt` prompts New + Verify → feed R twice.
	if err := runWithPassphrase(ctx, recoveryCode, 2, pbcBinary, "key", "change-passphrase", blobPath, "--kdf", "scrypt"); err != nil {
		_ = os.Remove(blobPath)
		return fmt.Errorf("escrow: wrap: %w", err)
	}
	return nil
}

// Unwrap recovers the unencrypted key from an R-wrapped blob, in place: re-key to kdf=none under R
// via a pty (one prompt). Used by Create's self-verify; the real recovery is slice 10.
func Unwrap(ctx context.Context, blobPath, recoveryCode string) error {
	// `key change-passphrase --kdf none` prompts the Encryption Key Password → feed R once.
	if err := runWithPassphrase(ctx, recoveryCode, 1, pbcBinary, "key", "change-passphrase", blobPath, "--kdf", "none"); err != nil {
		return fmt.Errorf("escrow: unwrap: %w", err)
	}
	return nil
}

// Paperkey emits the RAW-key paperkey (opt-in (a)) as text. Single-factor + unrevocable — the
// caller surfaces the caveat. SECRET-adjacent: do not log it.
func Paperkey(ctx context.Context, keyPath string) (string, error) {
	out, err := exec.CommandContext(ctx, pbcBinary, "key", "paperkey", keyPath, "--output-format", "text").Output()
	if err != nil {
		return "", fmt.Errorf("escrow: paperkey: %w", err)
	}
	return string(out), nil
}

// KeyFingerprint returns a PBS key file's fingerprint (identifies the key; safe to display).
func KeyFingerprint(ctx context.Context, keyPath string) (string, error) {
	out, err := exec.CommandContext(ctx, pbcBinary, "key", "show", keyPath, "--output-format", "json").Output()
	if err != nil {
		return "", fmt.Errorf("escrow: key show: %w", err)
	}
	var meta struct {
		Fingerprint string `json:"fingerprint"`
	}
	if err := json.Unmarshal(out, &meta); err != nil {
		return "", fmt.Errorf("escrow: parse key show: %w", err)
	}
	return meta.Fingerprint, nil
}

// copyFile copies src→dst with the given mode (0600 for key material). Reads the whole file
// (PBS key files are tiny).
func copyFile(src, dst string, mode os.FileMode) error {
	b, err := os.ReadFile(src)
	if err != nil {
		return err
	}
	return os.WriteFile(dst, b, mode)
}
