package secrets

import (
	"context"
	"fmt"
	"strings"
	"sync"

	infisical "github.com/infisical/go-sdk"
)

// InfisicalBackend reads secrets from an Infisical instance via machine identity auth.
type InfisicalBackend struct {
	client    infisical.InfisicalClientInterface
	cfg       InfisicalConfig
	mu        sync.Mutex
	connected bool
}

// InfisicalConfig holds connection parameters for an Infisical instance.
type InfisicalConfig struct {
	SiteURL      string // Infisical server URL (cloud or self-hosted)
	ClientID     string // Machine identity client ID
	ClientSecret string // Machine identity client secret
	ProjectID    string // Infisical project/workspace ID
	SecretPath   string // Path prefix for secrets (e.g., "/")
	Env          string // Environment slug (e.g., "dev", "prod")
}

// NewInfisicalBackend creates an Infisical backend. Connects lazily on first Get.
func NewInfisicalBackend(cfg InfisicalConfig) *InfisicalBackend {
	autoRefresh := true

	return &InfisicalBackend{
		cfg: cfg,
		client: infisical.NewInfisicalClient(context.Background(), infisical.Config{
			SiteUrl:              cfg.SiteURL,
			AutoTokenRefresh:     &autoRefresh,
			CacheExpiryInSeconds: 0, // no caching — live reads over localhost
		}),
	}
}

func (b *InfisicalBackend) Name() string { return "infisical" }

func (b *InfisicalBackend) connect() error {
	b.mu.Lock()
	defer b.mu.Unlock()

	if b.connected {
		return nil
	}

	clientID := b.cfg.ClientID
	clientSecret := b.cfg.ClientSecret
	if clientID == "" || clientSecret == "" {
		return fmt.Errorf("%w: OIKOS_INFISICAL_CLIENT_ID and OIKOS_INFISICAL_CLIENT_SECRET not set", ErrBackendUnavailable)
	}

	_, err := b.client.Auth().UniversalAuthLogin(clientID, clientSecret)
	if err != nil {
		return fmt.Errorf("%w: infisical auth: %w", ErrBackendUnavailable, err)
	}

	b.connected = true
	return nil
}

func (b *InfisicalBackend) Get(ctx context.Context, key string) (string, error) {
	if err := b.connect(); err != nil {
		return "", err
	}

	opts := infisical.ListSecretsOptions{
		ProjectID:   b.cfg.ProjectID,
		Environment: b.cfg.Env,
		SecretPath:  b.cfg.SecretPath,
	}

	secrets, err := b.client.Secrets().List(opts)
	if err != nil {
		return "", fmt.Errorf("infisical list: %w", err)
	}

	// Strip path prefix for matching
	lookupKey := strings.TrimPrefix(key, b.cfg.SecretPath)

	for _, s := range secrets {
		if s.SecretKey == lookupKey {
			return s.SecretValue, nil
		}
	}

	return "", ErrNotFound
}

func (b *InfisicalBackend) List(ctx context.Context) ([]string, error) {
	if err := b.connect(); err != nil {
		return nil, err
	}

	opts := infisical.ListSecretsOptions{
		ProjectID:   b.cfg.ProjectID,
		Environment: b.cfg.Env,
		SecretPath:  b.cfg.SecretPath,
	}

	secrets, err := b.client.Secrets().List(opts)
	if err != nil {
		return nil, fmt.Errorf("infisical list: %w", err)
	}

	keys := make([]string, len(secrets))
	for i, s := range secrets {
		keys[i] = s.SecretKey
	}
	return keys, nil
}

func (b *InfisicalBackend) Set(ctx context.Context, key string, value string) error {
	if err := b.connect(); err != nil {
		return err
	}

	// Try create first (idempotent — upserts); fall back to update on conflict.
	_, err := b.client.Secrets().Create(infisical.CreateSecretOptions{
		SecretKey:   key,
		SecretValue: value,
		Environment: b.cfg.Env,
		SecretPath:  b.cfg.SecretPath,
		ProjectID:   b.cfg.ProjectID,
		Type:        "shared",
	})
	if err != nil {
		// Create failed (key may already exist) — update instead.
		_, err = b.client.Secrets().Update(infisical.UpdateSecretOptions{
			SecretKey:      key,
			NewSecretValue: value,
			Environment:    b.cfg.Env,
			SecretPath:     b.cfg.SecretPath,
			ProjectID:      b.cfg.ProjectID,
			Type:           "shared",
		})
	}
	return err
}
