package rotate

import (
	"context"
	"fmt"

	"incredigo/internal/discover"
	"incredigo/internal/sink"
	"incredigo/internal/vault"
)

// ExecuteResult records, without any secret, what the REAL rotation spine did for
// one credential.
type ExecuteResult struct {
	Credential discover.Credential
	Driver     string
	StorePath  string // gopass entry the new secret was written to
	Rotated    bool   // a new secret was minted at the provider
	Verified   bool   // the new secret authenticated
	Stored     bool   // the new secret was written to gopass and re-read OK
	Revoked    bool   // RevokeOld was invoked
	Skipped    bool   // gated off by the proof policy — the driver was NOT run
	SkipReason string // why it was gated (only when Skipped)
	Err        error  // first error for this credential, if any
}

// Execute performs the REAL, destructive rotation spine for every credential a
// registered driver claims, enforcing Hard Rule #2 (verify-new-before-revoke-old):
//
//	Rotate → Verify(new) → store new in gopass → re-read + Verify(stored) → RevokeOld
//
// The old credential is revoked ONLY after the new secret authenticates, is
// persisted, and is read back and re-verified. Any failure short-circuits before
// RevokeOld, leaving the old credential intact (the §3 sealed backup is the
// recovery path on top of that).
//
// The MANDATORY backup gate (Snapshot) MUST already have succeeded — Execute does
// not perform it. Each credential's StorePath is taken from c.Location (the exact
// gopass path it was sourced from), so the new secret overwrites the same entry.
//
// Proof gate (Hard Rule spirit — no unattended real cutover through an unvalidated
// path): a driver runs only if its recorded proof level is ≥ MinExecuteProof
// (LIVE-VM). Drivers proven only against an emulator/mock (MOCK-ONLY) or with no
// proof at all (UNPROVEN) are SKIPPED — their Rotate/Verify/RevokeOld are never
// called — unless the caller passes allowMock, the explicit --allow-mock-proven
// opt-in. Skipped credentials leave the old secret fully intact.
func Execute(ctx context.Context, gp *sink.Gopass, creds []discover.Credential, v *vault.Vault, allowMock bool) []ExecuteResult {
	drivers := Drivers()
	var out []ExecuteResult
	for _, c := range creds {
		var d Rotator
		for _, cand := range drivers {
			if cand.Detect(c) {
				d = cand
				break
			}
		}
		if d == nil {
			continue // no driver claims it — handled by the manual worklist, not here
		}
		res := ExecuteResult{Credential: c, Driver: d.Name(), StorePath: c.Location}

		// Proof gate: refuse to run a driver that has not been proven against real
		// target software, unless the operator explicitly opted in. This is checked
		// BEFORE any provider call, so a gated driver touches nothing.
		if lvl := ProofLevelOf(d.Name()); lvl < MinExecuteProof && !allowMock {
			res.Skipped = true
			res.SkipReason = fmt.Sprintf("proof %s < %s; pass --allow-mock-proven to override", lvl, MinExecuteProof)
			out = append(out, res)
			continue
		}

		newH, err := d.Rotate(ctx, c, v)
		if err != nil {
			res.Err = fmt.Errorf("rotate: %w", err)
			out = append(out, res)
			continue
		}
		res.Rotated = true

		// Verify the new secret authenticates BEFORE it is stored or the old revoked.
		if err := d.Verify(ctx, newH, v); err != nil {
			res.Err = fmt.Errorf("verify(new): %w", err)
			v.Forget(newH)
			out = append(out, res)
			continue
		}
		res.Verified = true

		// Persist the new secret at the SAME gopass path (overwrite).
		if err := gp.InsertAt(ctx, v, res.StorePath, newH, true); err != nil {
			res.Err = fmt.Errorf("store: %w", err)
			v.Forget(newH)
			out = append(out, res)
			continue
		}
		v.Forget(newH)

		// Re-read what was stored and prove it still authenticates. This closes the
		// gap between "Verify passed on the in-vault value" and "the bytes gopass
		// now holds are the working secret" — only then is revoke safe.
		rh, err := gp.Show(ctx, v, res.StorePath)
		if err != nil {
			res.Err = fmt.Errorf("re-read: %w", err)
			out = append(out, res)
			continue
		}
		if err := d.Verify(ctx, rh, v); err != nil {
			res.Err = fmt.Errorf("verify(stored): %w", err)
			v.Forget(rh)
			out = append(out, res)
			continue
		}
		v.Forget(rh)
		res.Stored = true

		// Only now retire the old credential.
		if err := d.RevokeOld(ctx, c, v); err != nil {
			res.Err = fmt.Errorf("revoke-old: %w", err)
			out = append(out, res)
			continue
		}
		res.Revoked = true
		out = append(out, res)
	}
	return out
}
