package secrets

import (
	"context"
	"log/slog"
)

// NewManagerFromConfig creates a secrets Manager from the Infisical connection
// parameters in cfg, with an optional SOPS fallback from cfg.SecretsDir.
// Returns nil if Infisical is not configured.
func NewManagerFromConfig(siteURL, clientID, clientSecret, projectID, env, secretsDir string) *Manager {
	if siteURL == "" {
		return nil
	}
	infCfg := InfisicalConfig{
		SiteURL:      siteURL,
		ClientID:     clientID,
		ClientSecret: clientSecret,
		ProjectID:    projectID,
		SecretPath:   "/",
		Env:          env,
	}
	if infCfg.Env == "" {
		infCfg.Env = "dev"
	}
	primary := NewInfisicalBackend(infCfg)
	var fallback Backend
	if secretsDir != "" {
		fallback = NewSOPSBackend(secretsDir)
	}
	return NewManager(primary, fallback)
}

// VerifyExpectedSecrets checks that a list of expected keys are present
// in the backend. Logs a summary and returns the count of missing keys.
// Use at startup to detect incomplete Infisical migration.
func VerifyExpectedSecrets(ctx context.Context, sec Backend, expected []string) int {
	keys, err := sec.List(ctx)
	if err != nil {
		slog.Warn("secrets: cannot verify expected secrets, list failed", "error", err)
		return len(expected)
	}
	keySet := make(map[string]struct{}, len(keys))
	for _, k := range keys {
		keySet[k] = struct{}{}
	}
	missing := 0
	for _, exp := range expected {
		if _, ok := keySet[exp]; !ok {
			missing++
			slog.Warn("secrets: expected key missing from Infisical", "key", exp)
		}
	}
	if missing == 0 {
		slog.Info("secrets: all expected keys present", "count", len(expected))
	} else {
		slog.Warn("secrets: some expected keys missing from Infisical",
			"missing", missing, "total", len(expected))
	}
	return missing
}

// OverlayConfig fetches secrets from the backend and returns a function that
// applies them to config fields. Each entry maps an Infisical key to a setter;
// if the key is found and non-empty, the setter is called; if not found or
// empty, the env-derived value is left unchanged and a warning is logged.
// Returns the number of secrets resolved from Infisical (useful for logging).
func OverlayConfig(ctx context.Context, sec Backend, overlays []secretOverlay) int {
	resolved := 0
	for _, o := range overlays {
		val, err := sec.Get(ctx, o.infisicalKey)
		if err != nil {
			slog.Warn("secret not resolved from Infisical, using env fallback",
				"key", o.infisicalKey, "error", err)
			continue
		}
		if val == "" {
			slog.Warn("Infisical returned empty value, keeping env-derived value",
				"key", o.infisicalKey)
			continue
		}
		o.apply(val)
		resolved++
	}
	return resolved
}

// ConfigOverlays returns the standard set of Infisical → config overlays for
// the oikos binary. Each overlay is attempted at startup; if the key exists
// in Infisical, it overrides the env-derived value.
func ConfigOverlays(cfg map[string]func(string)) []secretOverlay {
	overlays := make([]secretOverlay, 0, len(cfg))
	for key, setter := range cfg {
		overlays = append(overlays, secretOverlay{infisicalKey: key, apply: setter})
	}
	return overlays
}

// ResolveSecret attempts to fetch a single secret from the backend. Returns
// the Infisical value if found and non-empty, otherwise falls back to the
// env-derived value. Warnings are logged for failures.
func ResolveSecret(ctx context.Context, sec Backend, infisicalKey, fallback string) string {
	if sec == nil {
		return fallback
	}
	val, err := sec.Get(ctx, infisicalKey)
	if err != nil {
		slog.Warn("secret not resolved from Infisical, using env fallback",
			"key", infisicalKey, "error", err)
		return fallback
	}
	if val == "" {
		slog.Warn("Infisical returned empty value, keeping env-derived value",
			"key", infisicalKey)
		return fallback
	}
	return val
}
