package main

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

type keycloakTokenResponse struct {
	AccessToken string `json:"access_token"`
}

type keycloakTokenManager struct {
	cfg    Config
	client *http.Client

	mu        sync.Mutex
	token     string
	refreshCh chan struct{}
	lastErr   error
}

func newKeycloakTokenManager(cfg Config, client *http.Client) *keycloakTokenManager {
	return &keycloakTokenManager{
		cfg:    cfg,
		client: client,
		token:  strings.TrimSpace(cfg.AccessToken),
	}
}

func (m *keycloakTokenManager) CurrentToken() string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return strings.TrimSpace(m.token)
}

func (m *keycloakTokenManager) EnsureToken(ctx context.Context, reason string) (string, error) {
	if token := m.CurrentToken(); token != "" {
		return token, nil
	}
	return m.Refresh(ctx, reason)
}

func (m *keycloakTokenManager) Refresh(ctx context.Context, reason string) (string, error) {
	m.mu.Lock()
	if ch := m.refreshCh; ch != nil {
		m.mu.Unlock()
		select {
		case <-ctx.Done():
			return "", ctx.Err()
		case <-ch:
			m.mu.Lock()
			token := strings.TrimSpace(m.token)
			err := m.lastErr
			m.mu.Unlock()
			if err != nil {
				return "", err
			}
			if token == "" {
				return "", fmt.Errorf("keycloak token refresh completed without access_token")
			}
			return token, nil
		}
	}

	ch := make(chan struct{})
	m.refreshCh = ch
	m.mu.Unlock()

	log.Printf("[auth] keycloak_token_refresh_start reason=%s endpoint=%s", reason, m.cfg.KeycloakTokenEndpoint)
	start := time.Now()
	token, err := m.fetchToken(ctx)
	if err != nil {
		log.Printf("[auth] keycloak_token_refresh_failed reason=%s endpoint=%s err=%v", reason, m.cfg.KeycloakTokenEndpoint, err)
	} else {
		log.Printf(
			"[auth] keycloak_token_refresh_ok reason=%s endpoint=%s elapsed=%s",
			reason,
			m.cfg.KeycloakTokenEndpoint,
			time.Since(start).Truncate(time.Millisecond),
		)
	}

	m.mu.Lock()
	if err == nil {
		m.token = token
	}
	m.lastErr = err
	close(ch)
	m.refreshCh = nil
	currentToken := strings.TrimSpace(m.token)
	m.mu.Unlock()

	if err != nil {
		return "", err
	}
	return currentToken, nil
}

func (m *keycloakTokenManager) fetchToken(ctx context.Context) (string, error) {
	form := url.Values{}
	form.Set("grant_type", "password")
	form.Set("client_id", strings.TrimSpace(m.cfg.KeycloakClientID))
	form.Set("username", strings.TrimSpace(m.cfg.KeycloakUsername))
	form.Set("password", m.cfg.KeycloakPassword)
	scope := strings.TrimSpace(m.cfg.KeycloakScope)
	if scope != "" {
		form.Set("scope", scope)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(m.cfg.KeycloakTokenEndpoint), strings.NewReader(form.Encode()))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Accept", "application/json")

	resp, err := m.client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return "", fmt.Errorf("keycloak token request failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
	}

	var tokenResp keycloakTokenResponse
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return "", fmt.Errorf("keycloak token parse failed: %w", err)
	}

	token := strings.TrimSpace(tokenResp.AccessToken)
	if token == "" {
		return "", fmt.Errorf("keycloak token response missing access_token")
	}
	return token, nil
}
