package authz

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"golang.org/x/crypto/ssh"
)

// Namespace is the FIXED SSHSIG domain separator. It is a package constant, never
// caller-supplied (phase4 §2.2): a signature minted for any other namespace must
// not verify.
const Namespace = "felhom-op-v1"

// DefaultClockSkew tolerates operator/host clock drift on the not-yet-valid check
// only (issued_at may be up to this far in the future). Expiry is NOT extended —
// the validity window stays an honest upper bound.
const DefaultClockSkew = 2 * time.Minute

// KeyRole tags a pinned operator key (doc 04 §3 two-key model).
type KeyRole string

const (
	// RoleOperational signs ordinary destructive ops (the "master stamp").
	RoleOperational KeyRole = "operational"
	// RoleRecovery is the cold key; authorizes ONLY key-rotation/break-glass ops.
	// Role-scoping is enforced by the consuming layer (slice 4), not here.
	RoleRecovery KeyRole = "recovery"
)

// AllowedSigner is one pinned operator public key.
type AllowedSigner struct {
	KeyID     string
	Role      KeyRole
	PublicKey ssh.PublicKey // parsed; allow-list match is by PublicKey.Marshal()
	Comment   string        // from the authorized_keys line, if any
}

// NewAllowedSigner parses a standard authorized_keys line ("ssh-ed25519 AAAA… [comment]"
// or "sk-ssh-ed25519@openssh.com AAAA… …") into an AllowedSigner with the given id+role.
func NewAllowedSigner(keyID string, role KeyRole, authorizedKeyLine string) (AllowedSigner, error) {
	pub, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKeyLine))
	if err != nil {
		return AllowedSigner{}, fmt.Errorf("authz: parsing pinned key %q: %w", keyID, err)
	}
	if role != RoleOperational && role != RoleRecovery {
		return AllowedSigner{}, fmt.Errorf("authz: pinned key %q has invalid role %q", keyID, role)
	}
	return AllowedSigner{KeyID: keyID, Role: role, PublicKey: pub, Comment: comment}, nil
}

// NonceStore records seen nonces for anti-replay. SeenOrRecord reports whether the
// nonce was already recorded; if not, it records it (durably, in the host impl)
// before returning false. See noncestore.go.
type NonceStore interface {
	SeenOrRecord(nonce string, exp time.Time) (seen bool)
}

// Verifier authenticates operator-signed destructive ops. Construct with New.
type Verifier struct {
	signers []AllowedSigner
	store   NonceStore
	hostID  string

	// ClockSkew tolerance for the not-yet-valid check (default DefaultClockSkew).
	ClockSkew time.Duration
	// Logger, if set, emits a warning when a blob's advisory key_id disagrees with
	// the matched signer. Never affects the verdict.
	Logger *slog.Logger

	now func() time.Time // injectable for tests
}

// New builds a Verifier over the pinned signer set, a nonce store, and this box's
// host id. allowedSigners is a set (single signer today; quorum is just sizing).
func New(signers []AllowedSigner, store NonceStore, hostID string) *Verifier {
	return &Verifier{
		signers:   signers,
		store:     store,
		hostID:    hostID,
		ClockSkew: DefaultClockSkew,
		now:       func() time.Time { return time.Now().UTC() },
	}
}

// Verify runs the LOCKED pipeline (phase4 §4 / doc 04 §2.3) and returns the
// authenticated op. Order is load-bearing and each post-crypto stage rejects even
// with an otherwise-valid signature:
//
//	parse armor → namespace → parse pubkey → allow-list (by key MATERIAL, not
//	key_id) → crypto verify (over the RAW received blob bytes) → parse blob →
//	target → time window → nonce SeenOrRecord (LAST)
//
// The nonce is recorded last, so an invalid signature can never consume a nonce
// (DoS / replay-priming safe). Errors wrap the typed sentinels in errors.go.
func (v *Verifier) Verify(blob, sigArmored []byte) (*VerifiedOp, error) {
	// 1. parse armor
	sb, err := parseArmoredSSHSIG(sigArmored)
	if err != nil {
		return nil, err
	}

	// 2. namespace (fixed domain separator)
	if sb.Namespace != Namespace {
		return nil, fmt.Errorf("%w: got %q want %q", ErrNamespace, sb.Namespace, Namespace)
	}

	// 3. parse the embedded public key
	pub, err := ssh.ParsePublicKey([]byte(sb.PublicKey))
	if err != nil {
		return nil, fmt.Errorf("%w: parsing signature public key: %v", ErrMalformed, err)
	}

	// 4. allow-list match by KEY MATERIAL (pub.Marshal equality) — NOT by key_id
	matched, ok := v.matchSigner(pub)
	if !ok {
		return nil, ErrUnknownSigner
	}

	// 5. crypto verify over the RAW received bytes (never re-serialized)
	signed, err := signedData(sb, blob)
	if err != nil {
		return nil, err
	}
	var inner ssh.Signature
	if err := ssh.Unmarshal([]byte(sb.Signature), &inner); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrMalformed, err)
	}
	if err := pub.Verify(signed, &inner); err != nil { // dispatches on the key's algorithm
		return nil, fmt.Errorf("%w: %v", ErrBadSignature, err)
	}

	// 6. parse the (now authenticated) blob bytes
	var op OpBlob
	if err := json.Unmarshal(blob, &op); err != nil {
		return nil, fmt.Errorf("%w: decoding op blob: %v", ErrMalformed, err)
	}

	// 7. target binding — host must be this box. guest_id is surfaced, not matched
	// here (the verifier doesn't enumerate guests; the caller routes by it).
	if op.Target.HostID != v.hostID {
		return nil, fmt.Errorf("%w: blob host_id=%q this=%q", ErrTarget, op.Target.HostID, v.hostID)
	}

	// 8. time window (clock-skew tolerance on not-yet-valid only)
	now := v.now()
	if now.Before(op.IssuedAt.Add(-v.ClockSkew)) {
		return nil, fmt.Errorf("%w: issued_at=%s now=%s", ErrNotYetValid, op.IssuedAt, now)
	}
	if now.After(op.ExpiresAt) {
		return nil, fmt.Errorf("%w: expires_at=%s now=%s", ErrExpired, op.ExpiresAt, now)
	}

	// 9. nonce LAST — only now is it durably recorded.
	if v.store.SeenOrRecord(op.Nonce, op.ExpiresAt) {
		return nil, fmt.Errorf("%w: nonce %s", ErrReplay, op.Nonce)
	}

	// advisory key_id audit (never a verdict input)
	keyIDMatches := op.KeyID == matched.KeyID
	if !keyIDMatches && v.Logger != nil {
		v.Logger.Warn("authz: blob key_id does not match the matched signer (advisory)",
			"blob_key_id", op.KeyID, "matched_signer", matched.KeyID)
	}

	return &VerifiedOp{
		Op:                 op.Op,
		HostID:             op.Target.HostID,
		GuestID:            op.Target.GuestID,
		Params:             op.Params,
		Nonce:              op.Nonce,
		IssuedAt:           op.IssuedAt,
		ExpiresAt:          op.ExpiresAt,
		KeyID:              op.KeyID,
		Signer:             matched,
		KeyIDMatchesSigner: keyIDMatches,
	}, nil
}

func (v *Verifier) matchSigner(pub ssh.PublicKey) (AllowedSigner, bool) {
	pm := pub.Marshal()
	for _, s := range v.signers {
		if s.PublicKey != nil && bytes.Equal(s.PublicKey.Marshal(), pm) {
			return s, true
		}
	}
	return AllowedSigner{}, false
}
