package notifications

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"sync"

	webpush "github.com/SherClockHolmes/webpush-go"
)

// VAPID keys are stored in the secrets store under these canonical keys.
const (
	vapidPrivateKeyName = "push:vapid:private"
	vapidPublicKeyName  = "push:vapid:public"
)

// VAPIDKeys holds the operator's VAPID keypair. Public is the base64url
// form the browser uses when subscribing; Private is the same shape consumed
// by webpush-go's signer.
type VAPIDKeys struct {
	Public  string
	Private string
}

// SecretsAccessor mirrors the narrow Get/Set interface used by other
// internal packages — abstracted so tests can swap in a fake.
type SecretsAccessor interface {
	Get(ctx context.Context, key string) ([]byte, error)
	Set(ctx context.Context, key string, value []byte) error
}

// VAPIDStore loads-or-creates the keypair. The first call generates a fresh
// keypair and persists it; subsequent calls return the cached copy.
type VAPIDStore struct {
	secrets SecretsAccessor
	mu      sync.Mutex
	cached  *VAPIDKeys
}

// NewVAPIDStore wraps the secrets accessor.
func NewVAPIDStore(secrets SecretsAccessor) *VAPIDStore {
	return &VAPIDStore{secrets: secrets}
}

// Load returns the operator's VAPID keypair, generating one if absent.
func (v *VAPIDStore) Load(ctx context.Context) (VAPIDKeys, error) {
	v.mu.Lock()
	defer v.mu.Unlock()
	if v.cached != nil {
		return *v.cached, nil
	}

	priv, errPriv := v.secrets.Get(ctx, vapidPrivateKeyName)
	pub, errPub := v.secrets.Get(ctx, vapidPublicKeyName)
	if errPriv == nil && errPub == nil && len(priv) > 0 && len(pub) > 0 {
		v.cached = &VAPIDKeys{Public: string(pub), Private: string(priv)}
		return *v.cached, nil
	}

	keys, err := generateVAPID()
	if err != nil {
		return VAPIDKeys{}, err
	}
	if err := v.secrets.Set(ctx, vapidPrivateKeyName, []byte(keys.Private)); err != nil {
		return VAPIDKeys{}, err
	}
	if err := v.secrets.Set(ctx, vapidPublicKeyName, []byte(keys.Public)); err != nil {
		return VAPIDKeys{}, err
	}
	v.cached = &keys
	return keys, nil
}

// PublicKey is a convenience wrapper that returns just the public key
// (what the browser needs to call PushManager.subscribe()).
func (v *VAPIDStore) PublicKey(ctx context.Context) (string, error) {
	keys, err := v.Load(ctx)
	if err != nil {
		return "", err
	}
	return keys.Public, nil
}

// generateVAPID produces a fresh P-256 keypair in webpush-go's expected
// base64url format. We use webpush-go's helper when present and fall back
// to a stdlib path if the helper changes shape across versions.
func generateVAPID() (VAPIDKeys, error) {
	// webpush-go exposes GenerateVAPIDKeys() returning (privB64, pubB64, err).
	priv, pub, err := webpush.GenerateVAPIDKeys()
	if err == nil && priv != "" && pub != "" {
		return VAPIDKeys{Public: pub, Private: priv}, nil
	}
	// Fallback: hand-roll. webpush wants raw-url-base64 of the P-256 private
	// scalar (32 bytes) and the uncompressed public key (65 bytes, 0x04
	// prefix + X + Y).
	curve := elliptic.P256()
	k, err := ecdsa.GenerateKey(curve, rand.Reader)
	if err != nil {
		return VAPIDKeys{}, err
	}
	if k.D.BitLen() == 0 {
		return VAPIDKeys{}, errors.New("vapid: degenerate scalar")
	}
	privBytes := make([]byte, 32)
	k.D.FillBytes(privBytes)
	xBytes := make([]byte, 32)
	yBytes := make([]byte, 32)
	k.X.FillBytes(xBytes)
	k.Y.FillBytes(yBytes)
	pubBytes := append([]byte{0x04}, append(xBytes, yBytes...)...)
	if len(pubBytes) != 65 {
		return VAPIDKeys{}, fmt.Errorf("vapid: unexpected public length %d", len(pubBytes))
	}
	return VAPIDKeys{
		Public:  base64.RawURLEncoding.EncodeToString(pubBytes),
		Private: base64.RawURLEncoding.EncodeToString(privBytes),
	}, nil
}
