package secrets

import (
	"context"
	"errors"
	"log/slog"
	"sync"
	"time"

	"github.com/dtoro/oikos/internal/core/ports"
)

var ErrNotFound = errors.New("secret not found")
var ErrBackendUnavailable = errors.New("secret backend unavailable")

// Backend is the secrets port. The interface lives in core/ports (ADR 0016);
// this alias keeps existing call sites working while implementations
// (Infisical, SOPS, Manager) stay in this package.
type Backend = ports.Secrets

// Manager holds a primary and fallback backend. If the primary fails,
// it falls back to the secondary. Supports periodic background refresh
// of cached secrets from the primary backend.
type Manager struct {
	primary     Backend
	fallback    Backend
	cache       map[string]cachedSecret
	mu          sync.RWMutex
	cacheTTL    time.Duration
	refreshMu   sync.Mutex
	lastRefresh time.Time
}

type cachedSecret struct {
	value     string
	expiresAt time.Time
}

// ManagerRefreshInterval controls how often cached secrets are re-fetched
// from the primary backend in the background. Zero disables background refresh.
var ManagerRefreshInterval = 5 * time.Minute

// NewManager creates a secret manager with primary and fallback backends.
func NewManager(primary, fallback Backend) *Manager {
	return &Manager{
		primary:     primary,
		fallback:    fallback,
		cache:       make(map[string]cachedSecret),
		cacheTTL:    5 * time.Minute,
		lastRefresh: time.Now(),
	}
}

// StartRefreshLoop starts a background goroutine that periodically refreshes
// cached secrets from the primary backend. Call from the server's main
// goroutine. The loop runs until ctx is cancelled.
func (m *Manager) StartRefreshLoop(ctx context.Context) {
	if ManagerRefreshInterval <= 0 {
		return
	}
	slog.Info("secrets: background refresh loop started",
		"interval", ManagerRefreshInterval)
	ticker := time.NewTicker(ManagerRefreshInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			slog.Info("secrets: refresh loop stopped")
			return
		case <-ticker.C:
			m.refreshAll(ctx)
		}
	}
}

// refreshAll re-fetches all cached secrets from the primary backend.
// Keys not found in the primary are left cached (they may be in fallback).
// Logs a summary line.
func (m *Manager) refreshAll(ctx context.Context) {
	m.refreshMu.Lock()
	defer m.refreshMu.Unlock()

	if m.primary == nil {
		return
	}

	m.mu.RLock()
	keys := make([]string, 0, len(m.cache))
	for k := range m.cache {
		keys = append(keys, k)
	}
	m.mu.RUnlock()

	if len(keys) == 0 {
		return
	}

	refreshed, stale, failed := 0, 0, 0
	for _, key := range keys {
		val, err := m.primary.Get(ctx, key)
		if err != nil {
			failed++
			continue
		}
		m.mu.RLock()
		cached, ok := m.cache[key]
		m.mu.RUnlock()
		if !ok || val != cached.value {
			m.mu.Lock()
			m.cache[key] = cachedSecret{value: val, expiresAt: time.Now().Add(m.cacheTTL)}
			m.mu.Unlock()
			refreshed++
		} else {
			stale++
		}
	}

	m.lastRefresh = time.Now()
	slog.Info("secrets: background refresh complete",
		"refreshed", refreshed, "stale", stale, "failed", failed,
		"cached", len(keys))
}

// Get retrieves a secret from primary, falling back to secondary on error.
func (m *Manager) Get(ctx context.Context, key string) (string, error) {
	m.mu.RLock()
	if cached, ok := m.cache[key]; ok && time.Now().Before(cached.expiresAt) {
		m.mu.RUnlock()
		slog.Debug("secret: cache hit", "key", key)
		return cached.value, nil
	}
	m.mu.RUnlock()

	val, err := m.primary.Get(ctx, key)
	if err == nil {
		m.mu.Lock()
		m.cache[key] = cachedSecret{value: val, expiresAt: time.Now().Add(m.cacheTTL)}
		m.mu.Unlock()
		slog.Debug("secret: fetched from primary", "key", key)
		return val, nil
	}

	if m.fallback != nil {
		val, fallbackErr := m.fallback.Get(ctx, key)
		if fallbackErr == nil {
			slog.Warn("secret: primary failed, using fallback",
				"key", key, "primary_error", err)
			return val, nil
		}
	}

	slog.Warn("secret: not found in any backend", "key", key, "error", err)
	return "", err
}

// List returns all keys from the primary backend.
func (m *Manager) List(ctx context.Context) ([]string, error) {
	keys, err := m.primary.List(ctx)
	if err != nil && m.fallback != nil {
		return m.fallback.List(ctx)
	}
	return keys, err
}

// Set stores a secret in the primary backend and invalidates the cache.
func (m *Manager) Set(ctx context.Context, key string, value string) error {
	m.InvalidateCache()
	return m.primary.Set(ctx, key, value)
}

// Name returns the primary backend name.
func (m *Manager) Name() string {
	return m.primary.Name()
}

// PrimaryName returns the name of the primary backend.
func (m *Manager) PrimaryName() string {
	return m.primary.Name()
}

// InvalidateCache clears cached secrets.
func (m *Manager) InvalidateCache() {
	m.mu.Lock()
	m.cache = make(map[string]cachedSecret)
	m.mu.Unlock()
}

// LastRefresh returns the timestamp of the last background refresh.
func (m *Manager) LastRefresh() time.Time {
	return m.lastRefresh
}

// secretOverlay maps an Infisical key to a config setter function.
type secretOverlay struct {
	infisicalKey string
	apply        func(value string)
}
