package wire

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

const (
	// sessionTokenTTL is how long the cached user session token is considered
	// fresh before a re-exchange is required.
	sessionTokenTTL = 24 * time.Hour
	// sessionTokenMaxFailures is the threshold after which exchange attempts
	// enter bounded backoff instead of immediately hitting the server again.
	sessionTokenMaxFailures = 3
	sessionTokenRetryMin    = 5 * time.Second
	sessionTokenRetryMax    = 5 * time.Minute
)

// UserToken returns a cached user session token for this channel, exchanging
// the machine API key for one on first call or after the cache expires.
//
// On 401 from downstream callers, use InvalidateUserToken then call again to
// force a single rotation. After sessionTokenMaxFailures consecutive exchange
// failures, requests are throttled with bounded exponential backoff; a later
// caller retries automatically without requiring a WebSocket reconnect.
func (c *Protocol) UserToken(ctx context.Context) (string, error) {
	c.userTokenMu.Lock()
	defer c.userTokenMu.Unlock()
	return c.userTokenLocked(ctx)
}

func (c *Protocol) userTokenLocked(ctx context.Context) (string, error) {
	now := c.userTokenNowLocked()
	if c.userTokenFailures >= sessionTokenMaxFailures && now.Before(c.userTokenRetryAt) {
		return "", fmt.Errorf("agentteam: user session token exchange retrying after backoff")
	}

	if c.userToken != "" && now.Sub(c.userTokenIssuedAt) < sessionTokenTTL {
		return c.userToken, nil
	}

	token, err := c.exchangeSessionTokenLocked(ctx)
	completedAt := c.userTokenNowLocked()
	if err != nil {
		c.userTokenFailures++
		if c.userTokenFailures >= sessionTokenMaxFailures {
			c.userTokenRetryAt = completedAt.Add(c.userTokenRetryDelayLocked())
		}
		return "", err
	}

	c.userToken = token
	c.userTokenIssuedAt = completedAt
	c.userTokenFailures = 0
	c.userTokenRetryAt = time.Time{}
	return token, nil
}

// RefreshUserToken coalesces concurrent recovery from a stale user token. If
// another caller already installed a different live token, that token is
// reused instead of exchanging again.
func (c *Protocol) RefreshUserToken(ctx context.Context, staleToken string) (string, error) {
	c.userTokenMu.Lock()
	defer c.userTokenMu.Unlock()

	staleToken = strings.TrimSpace(staleToken)
	if c.userToken != "" && c.userToken != staleToken &&
		c.userTokenNowLocked().Sub(c.userTokenIssuedAt) < sessionTokenTTL {
		return c.userToken, nil
	}
	c.userToken = ""
	c.userTokenIssuedAt = time.Time{}
	return c.userTokenLocked(ctx)
}

// InvalidateUserToken clears the cached token so the next UserToken call
// re-exchanges. Call this when a 401 is received using the token.
func (c *Protocol) InvalidateUserToken() {
	c.userTokenMu.Lock()
	c.userToken = ""
	c.userTokenIssuedAt = time.Time{}
	c.userTokenMu.Unlock()
}

// ResetUserTokenFailures clears the failure counter. Called on successful
// WS reconnect to give the channel a fresh start.
func (c *Protocol) ResetUserTokenFailures() {
	c.userTokenMu.Lock()
	c.userTokenFailures = 0
	c.userTokenRetryAt = time.Time{}
	c.userTokenMu.Unlock()
}

func (c *Protocol) userTokenNowLocked() time.Time {
	if c.userTokenNow != nil {
		return c.userTokenNow().UTC()
	}
	return time.Now().UTC()
}

func (c *Protocol) userTokenRetryDelayLocked() time.Duration {
	minDelay := c.userTokenRetryMin
	if minDelay <= 0 {
		minDelay = sessionTokenRetryMin
	}
	maxDelay := c.userTokenRetryMax
	if maxDelay <= 0 {
		maxDelay = sessionTokenRetryMax
	}
	if maxDelay < minDelay {
		maxDelay = minDelay
	}
	attempt := c.userTokenFailures - sessionTokenMaxFailures
	return boundedExponentialDelay(minDelay, maxDelay, attempt)
}

func (c *Protocol) exchangeSessionTokenLocked(ctx context.Context) (string, error) {
	httpClient := c.httpClient
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	serverURL, apiKey, workspaceID, _, _, _ := c.identitySnapshot()
	endpoint := strings.TrimRight(serverURL, "/") + daemonSessionTokenPath(workspaceID)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, http.NoBody)
	if err != nil {
		return "", fmt.Errorf("agentteam exchange session token: %w", err)
	}
	setRequestHeaders(req, requestHeaders{
		AuthToken:        strings.TrimSpace(apiKey),
		RouteWorkspaceID: workspaceID,
	})

	resp, err := httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("agentteam exchange session token request: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode >= 400 {
		return "", fmt.Errorf("agentteam exchange session token failed: HTTP %d: %s (endpoint: %s)", resp.StatusCode, readAgentTeamErrorBody(resp.Body), endpoint)
	}

	var payload struct {
		Token string `json:"token"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return "", fmt.Errorf("agentteam exchange session token decode: %w", err)
	}
	token := strings.TrimSpace(payload.Token)
	if token == "" {
		return "", fmt.Errorf("agentteam exchange session token: empty token")
	}
	return token, nil
}
