// Package observe contains observer-pattern primitives shared across the SCM
// and Tracker observation lanes. The pieces here are deliberately
// provider-agnostic: a polling goroutine supervisor, a lazy credential gate,
// and a bounded FIFO cache helper. Provider-specific normalization,
// persistence, and lifecycle reactions live in the sibling packages
// (observe/scm, future observe/tracker).
package observe

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

// StartPollLoop launches a goroutine that calls poll immediately, then on every
// tick interval until ctx is done. The returned channel closes when the
// goroutine exits; callers wait on it during shutdown.
//
// The immediate first poll inside the goroutine (rather than before the ticker
// loop) keeps daemon startup non-blocking: callers see Start return after the
// goroutine is launched, not after the first network call.
//
// poll errors other than context.Canceled are logged via logger with name as a
// prefix, e.g. name="scm observer" -> "scm observer: initial poll failed".
func StartPollLoop(ctx context.Context, tick time.Duration, poll func(context.Context) error, logger *slog.Logger, name string) <-chan struct{} {
	if logger == nil {
		logger = slog.Default()
	}
	done := make(chan struct{})
	go func() {
		defer close(done)
		if err := poll(ctx); err != nil && !errors.Is(err, context.Canceled) {
			logger.Error(name+": initial poll failed", "err", err)
		}
		t := time.NewTicker(tick)
		defer t.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-t.C:
				if err := poll(ctx); err != nil && !errors.Is(err, context.Canceled) {
					logger.Error(name+": poll failed", "err", err)
				}
			}
		}
	}()
	return done
}

// CredentialProbe checks whether the observer's provider has usable credentials.
// Implementations return (false, nil) for a transient failure the observer
// should retry on the next tick, (false, non-nil) for an error the caller
// should surface, and (true, nil) when credentials are available.
type CredentialProbe func(ctx context.Context) (available bool, err error)

// CheckCredentialsOnce runs probe at most once. The caller owns checked/disabled
// state via pointer so the observer struct keeps a single source of truth.
//
// State transitions:
//   - probe == nil           → *checked = true, returns (true, nil). No gate.
//   - probe returns err      → state unchanged, returns (false, nil). Retried next tick.
//   - probe returns false    → *checked = true, *disabled = true, returns (false, nil). Observer stays disabled.
//   - probe returns true     → *checked = true, returns (true, nil). Subsequent calls bypass the probe.
//
// Subsequent calls after the probe has run return (!*disabled, nil) so the
// disabled verdict is honoured on every poll, not just the first one.
//
// A context-cancellation before the probe returns (false, ctx.Err()).
func CheckCredentialsOnce(ctx context.Context, probe CredentialProbe, checked, disabled *bool, logger *slog.Logger, name string) (bool, error) {
	if *checked {
		return !*disabled, nil
	}
	if err := ctx.Err(); err != nil {
		return false, err
	}
	if logger == nil {
		logger = slog.Default()
	}
	if probe == nil {
		*checked = true
		return true, nil
	}
	available, err := probe(ctx)
	if err != nil {
		logger.Warn(name+" credentials check failed; will retry", "err", err)
		return false, nil
	}
	*checked = true
	if !available {
		*disabled = true
		logger.Warn(name + " disabled: provider credentials unavailable")
		return false, nil
	}
	return true, nil
}

// CacheSet writes value to m[key] and tracks insertion order in *order for
// bounded FIFO eviction. If the bucket already had key, order is left
// unchanged; otherwise key is appended. When len(*order) exceeds maxEntries,
// the oldest keys are evicted from both order and m.
//
// maxEntries <= 0 disables eviction; callers that want bounded behavior must
// pass a positive value. The generic shape lets the same helper serve
// string-, time-, and bool-valued caches without per-type duplication.
func CacheSet[V any](m map[string]V, order *[]string, maxEntries int, key string, value V) {
	if _, ok := m[key]; !ok {
		*order = append(*order, key)
	}
	m[key] = value
	if maxEntries <= 0 {
		return
	}
	for len(*order) > maxEntries {
		evict := (*order)[0]
		*order = (*order)[1:]
		delete(m, evict)
	}
}

// CacheDelete removes key from m and the matching slot from *order. It is a
// no-op when key is absent.
func CacheDelete[V any](m map[string]V, order *[]string, key string) {
	if _, ok := m[key]; !ok {
		return
	}
	delete(m, key)
	dst := (*order)[:0]
	for _, cachedKey := range *order {
		if cachedKey != key {
			dst = append(dst, cachedKey)
		}
	}
	*order = dst
}
