package wire

import (
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/upgrade"
)

// upgradeInfoTimeout bounds the server-driven upgrade guidance fetch so a slow
// or unreachable server cannot block CheckVersion or the post-connect path.
const upgradeInfoTimeout = 10 * time.Second

// ensureVersionPrefix adds a leading "v" if the version string does not already
// have one. The server versioning.Provider normalizes away the "v" prefix, but
// the desktop release manifest chain (manifest URL, version validation) expects
// "v"-prefixed versions. This normalizes at the boundary so the rest of the
// desktop upgrade path stays consistent.
func ensureVersionPrefix(v string) string {
	if v == "" {
		return v
	}
	if v[0] == 'v' || v[0] == 'V' {
		return v
	}
	return "v" + v
}

// serverKey returns the cache key for this channel's connected server. It is
// available before any fetch because it only depends on serverURL + workspaceID.
func (c *Protocol) serverKey() upgrade.ServerKey {
	serverURL, _, workspaceID, _, _, _ := c.identitySnapshot()
	return upgrade.NormalizeServerKey(serverURL, workspaceID)
}

// fetchUpgradeInfo calls the workspace-scoped daemon upgrade-info endpoint and
// returns the parsed recommendation. It is the concrete implementation behind
// the upgrade.RecommendationRefresher callback registered with the upgrade
// package.
//
// Best-effort: 404 (older server without the endpoint) and transient errors
// return (nil, err); the caller decides whether to keep existing cache.
func (c *Protocol) fetchUpgradeInfo(ctx context.Context, serverKey string) (*upgrade.ServerRecommendation, error) {
	// Only fetch for our own server; a stale callback from a previous server
	// key is ignored so guidance never leaks across servers.
	if upgrade.ServerKey(serverKey) != c.serverKey() {
		return nil, nil
	}

	httpClient := c.httpClient
	if httpClient == nil {
		httpClient = http.DefaultClient
	}

	serverURL, rawAPIKey, workspaceID, _, _, _ := c.identitySnapshot()
	endpoint := strings.TrimRight(serverURL, "/") + daemonUpgradeInfoPath(workspaceID)
	apiKey := strings.TrimSpace(rawAPIKey)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
	if err != nil {
		return nil, err
	}
	setRequestHeaders(req, requestHeaders{
		AuthToken:        apiKey,
		RouteWorkspaceID: workspaceID,
	})

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	switch {
	case resp.StatusCode == http.StatusNotFound:
		// Server has not deployed the upgrade-info endpoint.
		slog.Debug("AgentTeam: upgrade-info endpoint not found; server may not support server-driven upgrade guidance",
			"channel", c.id, "endpoint", endpoint)
		return nil, nil
	case resp.StatusCode >= 400:
		slog.Warn("AgentTeam: upgrade-info request failed",
			"channel", c.id, "endpoint", endpoint,
			"status", resp.StatusCode, "body", readAgentTeamErrorBody(resp.Body))
		return nil, nil
	}

	var payload struct {
		ServerVersion             string `json:"serverVersion"`
		RecommendedDesktopVersion string `json:"recommendedDesktopVersion"`
		MinDesktopVersion         string `json:"minDesktopVersion"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		slog.Warn("AgentTeam: decode upgrade-info response", "channel", c.id, "endpoint", endpoint, "error", err)
		return nil, nil
	}

	rec := &upgrade.ServerRecommendation{
		ServerVersion:             strings.TrimSpace(payload.ServerVersion),
		RecommendedDesktopVersion: ensureVersionPrefix(strings.TrimSpace(payload.RecommendedDesktopVersion)),
		MinDesktopVersion:         ensureVersionPrefix(strings.TrimSpace(payload.MinDesktopVersion)),
	}
	if rec.ServerVersion == "" && rec.RecommendedDesktopVersion == "" && rec.MinDesktopVersion == "" {
		slog.Debug("AgentTeam: upgrade-info returned no version guidance", "channel", c.id)
		return nil, nil
	}
	slog.Info("AgentTeam: fetched server-driven upgrade guidance",
		"channel", c.id,
		"serverVersion", rec.ServerVersion,
		"recommendedDesktopVersion", rec.RecommendedDesktopVersion,
		"minDesktopVersion", rec.MinDesktopVersion)
	return rec, nil
}

// prefetchUpgradeInfo triggers a non-blocking fetch of server-driven upgrade
// guidance after a successful WebSocket connect. It calls the refresher
// directly inside the exact connection-run owner instead of going through
// upgrade.EnsureServerRecommendation: Ensure is intentionally allowed to return
// on context cancellation while its internal fetch goroutine drains, which is
// incompatible with Protocol.Stop's no-subordinate-after-done contract. The
// ready handshake remains non-blocking and the cache is still keyed per server.
func (c *Protocol) prefetchUpgradeInfo(ctx context.Context) {
	if !c.upgradeInfoPrefetchEnabled {
		return
	}
	c.startConnectionRunTask(ctx, func(ctx context.Context) {
		fetchCtx, cancel := context.WithTimeout(ctx, upgradeInfoTimeout)
		defer cancel()
		key := c.serverKey()
		recommendation, err := c.fetchUpgradeInfo(fetchCtx, string(key))
		if err != nil {
			slog.Warn("AgentTeam: prefetch upgrade-info failed", "channel", c.id, "error", err)
			return
		}
		if recommendation != nil {
			upgrade.SetServerRecommendation(key, recommendation)
		}
	})
}

// onServerConnect registers the active server key and prefetches upgrade
// guidance. Called after a successful WebSocket connection.
func (c *Protocol) onServerConnect(ctx context.Context) {
	upgrade.SetActiveServer(c.serverKey())
	c.prefetchUpgradeInfo(ctx)
}

// onServerDisconnect clears the active server and its cached guidance so a
// subsequent CDN-only check cannot reuse a stale recommendation from a
// different server.
func (c *Protocol) onServerDisconnect() {
	key := c.serverKey()
	upgrade.ClearServerRecommendation(key)
	upgrade.SetActiveServer("")
}
