package escrow

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// Consume is the slice-10C production escrow-consumption path — the throwaway 10C spike harness
// turned into a real, tested function. It recovers the PBS client encryption key `K` from an
// R-wrapped escrow `blob`, GATES it on the expected key fingerprint, and installs it at `keyDest`
// for the PBS restore path. It is the inverse of Create: Create wraps `K` under `R`; Consume
// unwraps it back. The DR orchestration around it (re-enroll in restore mode, source the directive
// from the hub, decide which guests to restore) is slice 10D — Consume takes its four inputs as
// PARAMETERS so it stays standalone-testable (exactly as the spike harness was).
//
// The four inputs (the consumption contract — spike findings):
//   - blob:                the opaque R-wrapped escrow blob (hub-served restore directive in 10D).
//   - recoveryCode (R):    BY HAND from the customer — NEVER hub-sourced (zero-knowledge holds: a
//                          hub compromise alone cannot decrypt). A secret: never logged/persisted.
//   - expectedFingerprint: hub-served — the gate target (F-C4).
//   - keyDest:             where the PBS restore reads the key (`--keyfile` or the default
//                          `$XDG_CONFIG_HOME/proxmox-backup/encryption-key.json`).
//
// Order (spike F-C2/F-C3/F-C4/F-C6): Unwrap → fingerprint-gate → install. On ANY failure there is
// NO partial install (nothing at keyDest) and no key material leaks: the recovered key lives only
// in a 0600 tempdir that is always removed; `R` and key bytes are never logged (only fingerprint
// prefixes); the input `blob` is read-only, so a failed Consume is RETRYABLE.
func Consume(ctx context.Context, blob []byte, recoveryCode, expectedFingerprint, keyDest string) error {
	if len(blob) == 0 {
		return fmt.Errorf("escrow: Consume needs a non-empty blob")
	}
	if recoveryCode == "" {
		return fmt.Errorf("escrow: Consume needs the recovery code (R)")
	}
	if expectedFingerprint == "" {
		// The fingerprint gate is mandatory — without it we'd install whatever a (wrong-but-valid)
		// unwrap produced. There is no "skip the gate" path.
		return fmt.Errorf("escrow: Consume needs the expected key fingerprint (the gate target)")
	}
	if keyDest == "" {
		return fmt.Errorf("escrow: Consume needs a key destination path")
	}

	// Work in a private 0700 tempdir; RemoveAll always runs, so the recovered key never lingers.
	work, err := os.MkdirTemp("", "felhom-consume-")
	if err != nil {
		return fmt.Errorf("escrow: tempdir: %w", err)
	}
	defer os.RemoveAll(work)
	recovered := filepath.Join(work, "recovered.key")

	// Operate on a COPY of the blob (F-C6: the input blob is read-only → a failed Consume is
	// retryable). Unwrap re-keys this copy in place.
	if err := os.WriteFile(recovered, blob, 0o600); err != nil {
		return fmt.Errorf("escrow: staging blob: %w", err)
	}

	// 1. Unwrap with R. A WRONG R fails closed at the scrypt KDF (F-C3): nonzero exit, no key
	// emitted. Surface a clear, R-free error; nothing is written to keyDest.
	if err := Unwrap(ctx, recovered, recoveryCode); err != nil {
		return fmt.Errorf("escrow: the recovery code did not unwrap the escrow (wrong recovery code, or a corrupt blob): %w", err)
	}

	// 2. Fingerprint gate (F-C4) — the cheap correctness check, BEFORE any install or multi-GB
	// restore. A mismatch means a wrong escrow/datastore (or a recovery code that, against the odds,
	// produced a different key): fail fast + loud, no install.
	recFP, err := KeyFingerprint(ctx, recovered)
	if err != nil {
		return fmt.Errorf("escrow: reading recovered key fingerprint: %w", err)
	}
	if !fingerprintsEqual(recFP, expectedFingerprint) {
		return fmt.Errorf("escrow: recovered key fingerprint %s != expected %s — wrong escrow/datastore (not installing)",
			shortFP(recFP), shortFP(expectedFingerprint))
	}

	// 3. Install atomically at keyDest (0600) — the only thing Consume writes outside its tempdir.
	// A failure here leaves nothing partial at keyDest (the temp sibling is removed).
	if err := installKey(recovered, keyDest); err != nil {
		return fmt.Errorf("escrow: installing recovered key: %w", err)
	}
	return nil
}

// installKey atomically places the recovered key at dest with 0600: write a sibling temp on the
// SAME filesystem, fsync, then rename (atomic). The parent dir is created 0700 if missing. On any
// error the temp is removed so dest is never left partial.
func installKey(recovered, dest string) error {
	if err := os.MkdirAll(filepath.Dir(dest), 0o700); err != nil {
		return fmt.Errorf("key dir: %w", err)
	}
	b, err := os.ReadFile(recovered)
	if err != nil {
		return err
	}
	var rnd [8]byte
	_, _ = rand.Read(rnd[:])
	tmp := dest + ".tmp-" + hex.EncodeToString(rnd[:])
	f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
	if err != nil {
		return err
	}
	if _, err := f.Write(b); err != nil {
		f.Close()
		os.Remove(tmp)
		return err
	}
	if err := f.Sync(); err != nil {
		f.Close()
		os.Remove(tmp)
		return err
	}
	if err := f.Close(); err != nil {
		os.Remove(tmp)
		return err
	}
	if err := os.Rename(tmp, dest); err != nil {
		os.Remove(tmp)
		return err
	}
	return nil
}

// fingerprintsEqual compares two PBS key fingerprints tolerant of formatting (case + ':'/' '
// separators) — the hub-stored value and `key show`'s output may differ only cosmetically.
func fingerprintsEqual(a, b string) bool {
	return normFP(a) == normFP(b) && normFP(a) != ""
}

func normFP(s string) string {
	return strings.ToLower(strings.NewReplacer(":", "", " ", "", "\t", "", "\n", "").Replace(strings.TrimSpace(s)))
}

// shortFP returns a log-safe truncated fingerprint prefix (a key fingerprint is an identifier, not
// the key — but we still show only a prefix in logs/errors).
func shortFP(s string) string {
	s = strings.TrimSpace(s)
	if len(s) > 23 { // ~ first 8 colon-separated octets
		return s[:23] + "…"
	}
	return s
}
