package callbacksimp

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"path"
	"strings"
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"github.com/tech/sendico/pkg/vault/kv"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type signingSecretManager interface {
	Provision(ctx context.Context, organizationRef, callbackRef bson.ObjectID) (secretRef string, generatedSecret string, err error)
}

type vaultSigningSecretManager struct {
	logger       mlogger.Logger
	store        kv.Client
	pathPrefix   string
	field        string
	secretLength int
}

const (
	metricsResultSuccess = "success"
	metricsResultError   = "error"
)

var (
	signingSecretMetricsOnce sync.Once
	signingSecretStatus      *prometheus.CounterVec
	signingSecretLatency     *prometheus.HistogramVec
)

func ensureSigningSecretMetrics() {
	signingSecretMetricsOnce.Do(func() {
		signingSecretStatus = promauto.NewCounterVec(prometheus.CounterOpts{
			Namespace: "sendico",
			Subsystem: "bff_callbacks",
			Name:      "signing_secret_provision_total",
			Help:      "Total callback signing secret provisioning attempts.",
		}, []string{"result"})
		signingSecretLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
			Namespace: "sendico",
			Subsystem: "bff_callbacks",
			Name:      "signing_secret_provision_duration_seconds",
			Help:      "Duration of callback signing secret provisioning attempts.",
			Buckets:   prometheus.DefBuckets,
		}, []string{"result"})
	})
}

func newSigningSecretManager(logger mlogger.Logger, cfg callbacksConfig) (signingSecretManager, error) {
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	if logger == nil {
		logger = zap.NewNop()
	}

	manager := &vaultSigningSecretManager{
		logger:       logger.Named("callbacks_secrets"),
		pathPrefix:   strings.Trim(strings.TrimSpace(cfg.SecretPathPrefix), "/"),
		field:        strings.TrimSpace(cfg.SecretField),
		secretLength: cfg.SecretLengthBytes,
	}
	if manager.pathPrefix == "" {
		manager.pathPrefix = defaultSigningSecretPathPrefix
	}
	if manager.field == "" {
		manager.field = defaultSigningSecretField
	}

	if isVaultConfigEmpty(cfg.Vault) {
		manager.logger.Warn("Callbacks Vault config is not set; hmac signing secret generation is disabled")
		ensureSigningSecretMetrics()
		return manager, nil
	}

	store, err := kv.New(kv.Options{
		Logger: manager.logger,
		Config: kv.Config{
			Address:      strings.TrimSpace(cfg.Vault.Address),
			TokenEnv:     strings.TrimSpace(cfg.Vault.TokenEnv),
			TokenFileEnv: strings.TrimSpace(cfg.Vault.TokenFileEnv),
			TokenFile:    strings.TrimSpace(cfg.Vault.TokenFile),
			Namespace:    strings.TrimSpace(cfg.Vault.Namespace),
			MountPath:    strings.TrimSpace(cfg.Vault.MountPath),
		},
		Component: "bff callbacks signing secret manager",
	})
	if err != nil {
		return nil, err
	}
	manager.store = store
	ensureSigningSecretMetrics()

	return manager, nil
}

func (m *vaultSigningSecretManager) Provision(
	ctx context.Context,
	organizationRef,
	callbackRef bson.ObjectID,
) (string, string, error) {
	start := time.Now()
	result := metricsResultSuccess
	defer func() {
		signingSecretStatus.WithLabelValues(result).Inc()
		signingSecretLatency.WithLabelValues(result).Observe(time.Since(start).Seconds())
	}()

	if organizationRef.IsZero() {
		result = metricsResultError
		return "", "", merrors.InvalidArgument("organization reference is required", "organizationRef")
	}
	if callbackRef.IsZero() {
		result = metricsResultError
		return "", "", merrors.InvalidArgument("callback reference is required", "callbackRef")
	}
	if m.store == nil {
		result = metricsResultError
		return "", "", merrors.InvalidArgument("callbacks vault config is required to generate signing secrets", "api.callbacks.vault")
	}

	secret, err := generateSigningSecret(m.secretLength)
	if err != nil {
		result = metricsResultError
		return "", "", err
	}

	secretPath := path.Join(m.pathPrefix, organizationRef.Hex(), callbackRef.Hex())
	payload := map[string]interface{}{
		m.field:            secret,
		"organization_ref": organizationRef.Hex(),
		"callback_ref":     callbackRef.Hex(),
		"updated_at":       time.Now().UTC().Format(time.RFC3339Nano),
	}
	if err := m.store.Put(ctx, secretPath, payload); err != nil {
		result = metricsResultError
		m.logger.Warn("Failed to store callback signing secret", zap.String("path", secretPath), zap.Error(err))
		return "", "", err
	}

	secretRef := "vault:" + secretPath + "#" + m.field
	m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), mzap.ObjRef("callback_ref", callbackRef))

	return secretRef, secret, nil
}

func isVaultConfigEmpty(cfg VaultConfig) bool {
	return strings.TrimSpace(cfg.Address) == "" &&
		strings.TrimSpace(cfg.TokenEnv) == "" &&
		strings.TrimSpace(cfg.TokenFileEnv) == "" &&
		strings.TrimSpace(cfg.TokenFile) == "" &&
		strings.TrimSpace(cfg.MountPath) == "" &&
		strings.TrimSpace(cfg.Namespace) == ""
}

func generateSigningSecret(length int) (string, error) {
	if length <= 0 {
		return "", merrors.InvalidArgument("secret length must be greater than zero", "secret_length")
	}
	raw := make([]byte, length)
	if _, err := rand.Read(raw); err != nil {
		return "", merrors.Internal("failed to generate signing secret: " + err.Error())
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}
