package gitcred

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"
)

// TokenRefresher returns a repository access token and its expiry. Callers
// should return an empty expiry only for non-expiring test credentials.
type TokenRefresher func(context.Context) (string, time.Time, error)

// RefreshableTokenAuth is a go-git HTTP auth method that refreshes its
// Bearer token before each Git request when the current AGS sync lease is
// near expiry.
type RefreshableTokenAuth struct {
	RefreshBefore time.Duration
	Refresh       TokenRefresher

	mu        sync.Mutex
	token     string
	expiresAt time.Time
}

// NewRefreshableTokenAuth returns an auth method seeded with the token from
// the access:batch response. refresh is only called when the seed is missing
// or close to expiry.
func NewRefreshableTokenAuth(initialToken string, expiresAt time.Time, refresh TokenRefresher) *RefreshableTokenAuth {
	return &RefreshableTokenAuth{
		RefreshBefore: 2 * time.Minute,
		Refresh:       refresh,
		token:         strings.TrimSpace(initialToken),
		expiresAt:     expiresAt,
	}
}

func (a *RefreshableTokenAuth) Name() string {
	return "http-refreshable-token-auth"
}

func (a *RefreshableTokenAuth) String() string {
	return fmt.Sprintf("%s - *******", a.Name())
}

func (a *RefreshableTokenAuth) SetAuth(r *http.Request) {
	if a == nil || r == nil {
		return
	}
	token := a.currentToken(r.Context())
	if strings.TrimSpace(token) == "" {
		return
	}
	r.Header.Set("Authorization", "Bearer "+token)
}

func (a *RefreshableTokenAuth) currentToken(ctx context.Context) string {
	if ctx == nil {
		ctx = context.Background()
	}
	a.mu.Lock()
	defer a.mu.Unlock()

	if strings.TrimSpace(a.token) != "" && !a.shouldRefreshLocked(time.Now().UTC()) {
		return a.token
	}
	if a.Refresh == nil {
		return a.token
	}
	token, expiresAt, err := a.Refresh(ctx)
	if err != nil {
		slog.WarnContext(ctx, "git credential refresh failed", "error", err)
		return a.token
	}
	token = strings.TrimSpace(token)
	if token == "" {
		slog.WarnContext(ctx, "git credential refresh returned empty token")
		return a.token
	}
	a.token = token
	a.expiresAt = expiresAt
	return a.token
}

func (a *RefreshableTokenAuth) shouldRefreshLocked(now time.Time) bool {
	if a.expiresAt.IsZero() {
		return false
	}
	refreshBefore := a.RefreshBefore
	if refreshBefore <= 0 {
		refreshBefore = 2 * time.Minute
	}
	return !a.expiresAt.After(now.Add(refreshBefore))
}
