// ABOUTME: Authentication providers for EdgeXR Master Controller API
// ABOUTME: Supports Bearer token authentication with pluggable provider interface

package edgeconnect

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"
)

// AuthProvider interface for attaching authentication to requests
type AuthProvider interface {
	// Attach adds authentication headers to the request
	Attach(ctx context.Context, req *http.Request) error
}

// StaticTokenProvider implements Bearer token authentication with a fixed token
type StaticTokenProvider struct {
	Token string
}

// NewStaticTokenProvider creates a new static token provider
func NewStaticTokenProvider(token string) *StaticTokenProvider {
	return &StaticTokenProvider{Token: token}
}

// Attach adds the Bearer token to the request Authorization header
func (s *StaticTokenProvider) Attach(ctx context.Context, req *http.Request) error {
	if s.Token != "" {
		req.Header.Set("Authorization", "Bearer "+s.Token)
	}
	return nil
}

// UsernamePasswordProvider implements dynamic token retrieval using username/password
// This matches the existing client/client.go RetrieveToken implementation
type UsernamePasswordProvider struct {
	BaseURL    string
	Username   string
	Password   string
	HTTPClient *http.Client

	// Token caching
	mu          sync.RWMutex
	cachedToken string
	tokenExpiry time.Time
}

// NewUsernamePasswordProvider creates a new username/password auth provider
func NewUsernamePasswordProvider(baseURL, username, password string, httpClient *http.Client) *UsernamePasswordProvider {
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 30 * time.Second}
	}

	return &UsernamePasswordProvider{
		BaseURL:    strings.TrimRight(baseURL, "/"),
		Username:   username,
		Password:   password,
		HTTPClient: httpClient,
	}
}

// Attach retrieves a token (with caching) and adds it to the Authorization header
func (u *UsernamePasswordProvider) Attach(ctx context.Context, req *http.Request) error {
	token, err := u.getToken(ctx)
	if err != nil {
		return fmt.Errorf("failed to get token: %w", err)
	}

	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	return nil
}

// getToken retrieves a token, using cache if valid
func (u *UsernamePasswordProvider) getToken(ctx context.Context) (string, error) {
	// Check cache first
	u.mu.RLock()
	if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) {
		token := u.cachedToken
		u.mu.RUnlock()
		return token, nil
	}
	u.mu.RUnlock()

	// Need to retrieve new token
	u.mu.Lock()
	defer u.mu.Unlock()

	// Double-check after acquiring write lock
	if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) {
		return u.cachedToken, nil
	}

	// Retrieve token using existing RetrieveToken logic
	token, err := u.retrieveToken(ctx)
	if err != nil {
		return "", err
	}

	// Cache token with reasonable expiry (assume 1 hour, can be configurable)
	u.cachedToken = token
	u.tokenExpiry = time.Now().Add(1 * time.Hour)

	return token, nil
}

// retrieveToken implements the same logic as the existing client/client.go RetrieveToken method
func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, error) {
	// Marshal credentials - same as existing implementation
	jsonData, err := json.Marshal(map[string]string{
		"username": u.Username,
		"password": u.Password,
	})
	if err != nil {
		return "", err
	}

	// Create request - same as existing implementation
	loginURL := u.BaseURL + "/api/v1/login"
	request, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return "", err
	}
	request.Header.Set("Content-Type", "application/json")

	// Execute request
	resp, err := u.HTTPClient.Do(request)
	if err != nil {
		return "", err
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	// Read response body - same as existing implementation
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("error reading response body: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body))
	}

	// Parse JSON response - same as existing implementation
	var respData struct {
		Token string `json:"token"`
	}
	err = json.Unmarshal(body, &respData)
	if err != nil {
		return "", fmt.Errorf("error parsing JSON (status %d): %v", resp.StatusCode, err)
	}

	return respData.Token, nil
}

// InvalidateToken clears the cached token, forcing a new login on next request
func (u *UsernamePasswordProvider) InvalidateToken() {
	u.mu.Lock()
	defer u.mu.Unlock()
	u.cachedToken = ""
	u.tokenExpiry = time.Time{}
}

// NoAuthProvider implements no authentication (for testing or public endpoints)
type NoAuthProvider struct{}

// NewNoAuthProvider creates a new no-auth provider
func NewNoAuthProvider() *NoAuthProvider {
	return &NoAuthProvider{}
}

// Attach does nothing (no authentication)
func (n *NoAuthProvider) Attach(ctx context.Context, req *http.Request) error {
	return nil
}
