// Package localruntime implements the stable Desktop bootstrap and
// window-local active-server control plane.
package localruntime

import (
	"context"
	"log/slog"
	"net/http"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/auth"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/uisession"
	"github.com/tidbcloud/AgentTeam/desktop/internal/version"
)

type ServerRegistry interface {
	Get(id string) (profile.AgentTeamServer, bool)
}

type ServerCandidateLister interface {
	All() []profile.AgentTeamServer
}

type serverRuntimeAvailability interface {
	ServerAvailableForSelection(id string) bool
}

type activeTargetHistory interface {
	LastActiveTarget() (string, bool)
	SetLastActiveTarget(targetID string) error
	ClearLastActiveTarget(targetID string) error
}

type firstMachineBindingEnsurer interface {
	EnsureFirstBinding(ctx context.Context, targetID string) (profile.MachineBinding, int, error)
}

type Dependencies struct {
	Logger           *slog.Logger
	NativeWindow     func() bool
	Platform         string
	Servers          ServerRegistry
	ServerCandidates ServerCandidateLister
	Sessions         *uisession.Store
	TargetHistory    activeTargetHistory
	MachineSetup     firstMachineBindingEnsurer
}

type capabilitiesResponse struct {
	LocalDaemon  bool `json:"localDaemon"`
	NativeWindow bool `json:"nativeWindow"`
}

type buildInfoResponse struct {
	Version string `json:"version"`
	Commit  string `json:"commit"`
	BuildAt string `json:"buildAt"`
}

type runtimeResponse struct {
	Kind         string               `json:"kind"`
	Platform     string               `json:"platform"`
	Capabilities capabilitiesResponse `json:"capabilities"`
	BuildInfo    buildInfoResponse    `json:"buildInfo"`
}

type activeServerRequest struct {
	ServerID string `json:"serverId" binding:"required"`
}

type serverCandidateResponse struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	WorkspaceID string `json:"workspaceId"`
}

type activeServerResponse struct {
	Server     activeServerPublicResponse  `json:"server"`
	Binding    activeServerBindingResponse `json:"binding"`
	Generation uint64                      `json:"generation"`
}

type activeServerPublicResponse struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

type activeServerBindingResponse struct {
	WorkspaceID string `json:"workspaceId"`
	MachineID   string `json:"machineId"`
}

func RegisterRoutes(routes gin.IRoutes, dependencies Dependencies) {
	routes.GET("/runtime", NewRuntimeHandler(dependencies.Platform, dependencies.NativeWindow))
	routes.POST("/client-observations", newClientObservationHandler(dependencies.Logger))
	// Register the literal route before /servers/:id at the composition root.
	routes.GET("/servers/available", newListAvailableServersHandler(
		dependencies.ServerCandidates,
		dependencies.Servers,
	))
	routes.GET("/servers/active", newGetActiveServerHandler(
		dependencies.Sessions,
		dependencies.Servers,
		dependencies.TargetHistory,
		dependencies.MachineSetup,
	))
	routes.PUT("/servers/active", newPutActiveServerHandler(dependencies.Sessions, dependencies.Servers, dependencies.TargetHistory))
	routes.DELETE("/servers/active", newDeleteActiveServerHandler(dependencies.Sessions, dependencies.TargetHistory))
}

func newListAvailableServersHandler(candidates ServerCandidateLister, servers ServerRegistry) gin.HandlerFunc {
	return func(c *gin.Context) {
		if candidates == nil || servers == nil {
			internalUnavailable(c)
			return
		}
		result := make([]serverCandidateResponse, 0)
		for _, candidate := range candidates.All() {
			server, available := availableServer(servers, candidate.ID)
			if !available {
				continue
			}
			result = append(result, serverCandidateResponse{
				ID:          server.ID,
				Name:        server.Name,
				Description: server.Description,
				WorkspaceID: server.WorkspaceID,
			})
		}
		sort.Slice(result, func(i, j int) bool {
			if result[i].Name != result[j].Name {
				return result[i].Name < result[j].Name
			}
			return result[i].ID < result[j].ID
		})
		c.JSON(http.StatusOK, gin.H{"servers": result})
	}
}

// RegisterActiveSelectionStreamRoute registers the browser WebSocket route
// outside the header-only internal auth middleware. The handler upgrades first
// and authenticates header/query session tokens itself so browser failures can
// be surfaced as the documented 4401 close code.
func RegisterActiveSelectionStreamRoute(routes gin.IRoutes, dependencies Dependencies) {
	routes.GET("/api/internal/servers/active/stream", NewActiveSelectionStreamHandler(StreamHandlerDeps{
		Provider: dependencies.Sessions,
		Servers:  dependencies.Servers,
	}))
}

func NewRuntimeHandler(platform string, nativeWindow func() bool) gin.HandlerFunc {
	platform = strings.TrimSpace(platform)
	if platform == "" {
		platform = runtime.GOOS
	}
	return func(c *gin.Context) {
		hasNativeWindow := false
		if nativeWindow != nil {
			hasNativeWindow = nativeWindow()
		}
		c.JSON(http.StatusOK, runtimeResponse{
			Kind:     "desktop",
			Platform: platform,
			Capabilities: capabilitiesResponse{
				LocalDaemon:  true,
				NativeWindow: hasNativeWindow,
			},
			BuildInfo: buildInfoResponse{
				Version: version.BuildVersion(),
				Commit:  version.Commit,
				BuildAt: version.BuildAt,
			},
		})
	}
}

func NewGetActiveServerHandler(sessions *uisession.Store, servers ServerRegistry) gin.HandlerFunc {
	return newGetActiveServerHandler(sessions, servers, nil, nil)
}

func newGetActiveServerHandler(
	sessions *uisession.Store,
	servers ServerRegistry,
	history activeTargetHistory,
	machineSetup firstMachineBindingEnsurer,
) gin.HandlerFunc {
	return func(c *gin.Context) {
		sessionID, expiresAt, ok := requireSession(c)
		if !ok {
			return
		}
		if sessions == nil || servers == nil {
			internalUnavailable(c)
			return
		}
		restoredMachineID := ""
		snapshot, ok := sessions.Get(sessionID)
		if (!ok || snapshot.ActiveServerID == "") && history != nil {
			if targetID, exists := history.LastActiveTarget(); exists {
				if _, available := availableServer(servers, targetID); available {
					if machineSetup != nil {
						binding, _, setupErr := machineSetup.EnsureFirstBinding(c.Request.Context(), targetID)
						if setupErr != nil {
							// A failed binding restore must never white-screen the
							// renderer. Drop the stale target and fall back to the
							// no-active-server contract so the UI routes to onboarding
							// instead of surfacing a 5xx.
							slog.Warn("Desktop restore machine setup failed", "error", setupErr)
							if history != nil {
								_ = history.ClearLastActiveTarget(targetID)
							}
							if c.Query("optional") == "true" {
								c.JSON(http.StatusOK, nil)
								return
							}
							activeServerRequired(c)
							return
						}
						restoredMachineID = binding.MachineID
					}
					restored, err := sessions.Select(sessionID, expiresAt, targetID)
					if err == nil {
						snapshot = restored
						ok = true
					}
				} else if err := history.ClearLastActiveTarget(targetID); err != nil {
					slog.Warn("clear unavailable Desktop restore target", "error", err)
				}
			}
		}
		if !ok || snapshot.ActiveServerID == "" {
			if c.Query("optional") == "true" {
				c.JSON(http.StatusOK, nil)
				return
			}
			activeServerRequired(c)
			return
		}
		server, ok := availableServer(servers, snapshot.ActiveServerID)
		if !ok {
			sessions.ClearIfGeneration(sessionID, snapshot.Generation)
			if history != nil {
				if err := history.ClearLastActiveTarget(snapshot.ActiveServerID); err != nil {
					slog.Warn("clear unavailable active Desktop target", "error", err)
				}
			}
			if c.Query("optional") == "true" {
				c.JSON(http.StatusOK, nil)
				return
			}
			activeServerRequired(c)
			return
		}
		if restoredMachineID != "" {
			server.MachineID = restoredMachineID
		}
		c.JSON(http.StatusOK, buildActiveServerResponse(server, snapshot.Generation))
	}
}

func NewPutActiveServerHandler(sessions *uisession.Store, servers ServerRegistry) gin.HandlerFunc {
	return newPutActiveServerHandler(sessions, servers, nil)
}

func newPutActiveServerHandler(
	sessions *uisession.Store,
	servers ServerRegistry,
	history activeTargetHistory,
) gin.HandlerFunc {
	return func(c *gin.Context) {
		sessionID, expiresAt, ok := requireSession(c)
		if !ok {
			return
		}
		if sessions == nil || servers == nil {
			internalUnavailable(c)
			return
		}
		var request activeServerRequest
		if err := c.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.ServerID) == "" {
			c.JSON(http.StatusBadRequest, gin.H{
				"code":  "INVALID_REQUEST",
				"error": "serverId is required.",
			})
			return
		}
		serverID := strings.TrimSpace(request.ServerID)
		server, exists := servers.Get(serverID)
		if !exists {
			c.JSON(http.StatusNotFound, gin.H{
				"code":  "SERVER_NOT_FOUND",
				"error": "The requested AgentTeam server is not registered.",
			})
			return
		}
		if _, available := availableServer(servers, serverID); !available {
			c.JSON(http.StatusConflict, gin.H{
				"code":  "SERVER_UNAVAILABLE",
				"error": "The requested AgentTeam server is disabled.",
			})
			return
		}
		snapshot, err := sessions.Select(sessionID, expiresAt, server.ID)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code":  "SESSION_EXPIRED",
				"error": "The Desktop session has expired.",
			})
			return
		}
		if history != nil {
			if err := history.SetLastActiveTarget(serverID); err != nil {
				sessions.ClearIfGeneration(sessionID, snapshot.Generation)
				internalPersistenceFailure(c)
				return
			}
		}
		c.JSON(http.StatusOK, buildActiveServerResponse(server, snapshot.Generation))
	}
}

func NewDeleteActiveServerHandler(sessions *uisession.Store) gin.HandlerFunc {
	return newDeleteActiveServerHandler(sessions, nil)
}

func newDeleteActiveServerHandler(
	sessions *uisession.Store,
	history activeTargetHistory,
) gin.HandlerFunc {
	return func(c *gin.Context) {
		sessionID, _, ok := requireSession(c)
		if !ok {
			return
		}
		if sessions == nil {
			internalUnavailable(c)
			return
		}
		current, _ := sessions.Get(sessionID)
		if history != nil {
			if err := history.ClearLastActiveTarget(current.ActiveServerID); err != nil {
				internalPersistenceFailure(c)
				return
			}
		}
		sessions.Clear(sessionID)
		c.Status(http.StatusNoContent)
	}
}

func internalPersistenceFailure(c *gin.Context) {
	c.JSON(http.StatusInternalServerError, gin.H{
		"code":  "DESKTOP_STATE_SAVE_FAILED",
		"error": "Desktop could not save the selected workspace.",
	})
}

func buildActiveServerResponse(server profile.AgentTeamServer, generation uint64) activeServerResponse {
	return activeServerResponse{
		Server: activeServerPublicResponse{
			ID:          server.ID,
			Name:        server.Name,
			Description: server.Description,
		},
		Binding: activeServerBindingResponse{
			WorkspaceID: server.WorkspaceID,
			MachineID:   server.MachineID,
		},
		Generation: generation,
	}
}

func requireSession(c *gin.Context) (string, time.Time, bool) {
	sessionID, expiresAt, ok := auth.SessionIdentity(c)
	if ok {
		return sessionID, expiresAt, true
	}
	c.JSON(http.StatusUnauthorized, gin.H{
		"code":  "SESSION_REQUIRED",
		"error": "An authenticated Desktop session is required.",
	})
	return "", time.Time{}, false
}

func availableServer(servers ServerRegistry, serverID string) (profile.AgentTeamServer, bool) {
	server, ok := servers.Get(serverID)
	if !ok || !server.Enabled {
		return profile.AgentTeamServer{}, false
	}
	if availability, ok := servers.(serverRuntimeAvailability); ok &&
		!availability.ServerAvailableForSelection(serverID) {
		return profile.AgentTeamServer{}, false
	}
	return server, true
}

func activeServerRequired(c *gin.Context) {
	c.JSON(http.StatusConflict, gin.H{
		"code":  "ACTIVE_SERVER_REQUIRED",
		"error": "Select a AgentTeam server to continue.",
	})
}

func internalUnavailable(c *gin.Context) {
	c.JSON(http.StatusServiceUnavailable, gin.H{
		"code":  "DESKTOP_RUNTIME_UNAVAILABLE",
		"error": "The Desktop runtime is unavailable.",
	})
}
