// Copyright 2025 Daytona Platforms Inc.
// SPDX-License-Identifier: AGPL-3.0

//	@title			Daytona Toolbox API
//	@version		v0.0.0-dev
//	@description	Daytona Toolbox API
//	@license.name	Apache-2.0
//	@license.url	https://www.apache.org/licenses/LICENSE-2.0

package toolbox

import (
	"context"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"os"
	"path"
	"time"

	common_errors "github.com/daytonaio/common-go/pkg/errors"
	common_proxy "github.com/daytonaio/common-go/pkg/proxy"
	"github.com/daytonaio/common-go/pkg/telemetry"
	"github.com/daytonaio/daemon/internal"
	"github.com/daytonaio/daemon/pkg/recording"
	session_svc "github.com/daytonaio/daemon/pkg/session"
	"github.com/daytonaio/daemon/pkg/toolbox/computeruse"
	"github.com/daytonaio/daemon/pkg/toolbox/computeruse/manager"
	recordingcontroller "github.com/daytonaio/daemon/pkg/toolbox/computeruse/recording"
	"github.com/daytonaio/daemon/pkg/toolbox/config"
	"github.com/daytonaio/daemon/pkg/toolbox/fs"
	"github.com/daytonaio/daemon/pkg/toolbox/git"
	"github.com/daytonaio/daemon/pkg/toolbox/lsp"
	"github.com/daytonaio/daemon/pkg/toolbox/port"
	"github.com/daytonaio/daemon/pkg/toolbox/process"
	"github.com/daytonaio/daemon/pkg/toolbox/process/coderun"
	"github.com/daytonaio/daemon/pkg/toolbox/process/interpreter"
	"github.com/daytonaio/daemon/pkg/toolbox/process/pty"
	"github.com/daytonaio/daemon/pkg/toolbox/process/session"
	"github.com/daytonaio/daemon/pkg/toolbox/proxy"
	sloggin "github.com/samber/slog-gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	otellog "go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/metric"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"

	"github.com/daytonaio/daemon/pkg/toolbox/docs"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	swaggerfiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
)

type ServerConfig struct {
	Logger                *slog.Logger
	WorkDir               string
	ConfigDir             string
	ComputerUse           computeruse.IComputerUse
	SandboxId             string
	OtelEndpoint          *string
	SessionService        *session_svc.SessionService
	RecordingService      *recording.RecordingService
	OrganizationId        *string
	RegionId              *string
	Snapshot              *string
	EntrypointLogFilePath string
}

func NewServer(config ServerConfig) *server {
	return &server{
		logger:                config.Logger.With(slog.String("component", "toolbox_server")),
		WorkDir:               config.WorkDir,
		SandboxId:             config.SandboxId,
		otelEndpoint:          config.OtelEndpoint,
		telemetry:             Telemetry{},
		sessionService:        config.SessionService,
		configDir:             config.ConfigDir,
		recordingService:      config.RecordingService,
		organizationId:        config.OrganizationId,
		regionId:              config.RegionId,
		snapshot:              config.Snapshot,
		entrypointLogFilePath: config.EntrypointLogFilePath,
	}
}

type server struct {
	WorkDir               string
	ComputerUse           computeruse.IComputerUse
	SandboxId             string
	logger                *slog.Logger
	otelEndpoint          *string
	authToken             string
	telemetry             Telemetry
	sessionService        *session_svc.SessionService
	configDir             string
	recordingService      *recording.RecordingService
	entrypointLogFilePath string
	entrypointLogCancel   context.CancelFunc
	httpServer            *http.Server
	organizationId        *string
	regionId              *string
	snapshot              *string
	ctx                   context.Context
	cancel                context.CancelFunc
}

type Telemetry struct {
	TracerProvider *sdktrace.TracerProvider
	MeterProvider  *metric.MeterProvider
	LoggerProvider *otellog.LoggerProvider
}

func (s *server) Start() error {
	s.ctx, s.cancel = context.WithCancel(context.Background())
	defer s.cancel()

	docs.SwaggerInfo.Description = "Daytona Toolbox API"
	docs.SwaggerInfo.Title = "Daytona Toolbox API"
	docs.SwaggerInfo.BasePath = "/"
	docs.SwaggerInfo.Version = internal.Version

	// Set Gin to release mode in production
	if os.Getenv("ENVIRONMENT") == "production" {
		gin.SetMode(gin.ReleaseMode)
	}

	otelServiceName := fmt.Sprintf("sandbox-%s", s.SandboxId)

	r := gin.New()
	r.Use(common_errors.Recovery())
	noTelemetryRouter := r.Group("/")
	r.Use(func(ctx *gin.Context) {
		if s.telemetry.TracerProvider == nil {
			ctx.Next()
			return
		}

		otelgin.Middleware(otelServiceName, otelgin.WithTracerProvider(s.telemetry.TracerProvider))(ctx)
		ctx.Next()
	})
	r.Use(sloggin.New(s.logger))
	errMiddleware := common_errors.NewErrorMiddleware(func(ctx *gin.Context, err error) common_errors.ErrorResponse {
		return common_errors.ErrorResponse{
			StatusCode: http.StatusInternalServerError,
			Message:    err.Error(),
		}
	})

	noTelemetryRouter.Use(sloggin.New(s.logger))
	r.Use(errMiddleware)
	noTelemetryRouter.Use(errMiddleware)
	binding.Validator = new(DefaultValidator)

	// Add swagger UI in development mode
	if os.Getenv("ENVIRONMENT") != "production" {
		r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
	}

	r.POST("/init", s.Initialize(otelServiceName, s.entrypointLogFilePath, s.organizationId, s.regionId, s.snapshot))

	r.GET("/version", s.GetVersion)

	// keep /project-dir old behavior for backward compatibility
	r.GET("/project-dir", s.GetUserHomeDir)
	r.GET("/user-home-dir", s.GetUserHomeDir)
	r.GET("/work-dir", s.GetWorkDir)

	fsController := r.Group("/files")
	{
		// read operations
		fsController.GET("/", fs.ListFiles)
		fsController.GET("/download", fs.DownloadFile)
		fsController.POST("/bulk-download", fs.DownloadFiles)
		fsController.GET("/find", fs.FindInFiles)
		fsController.GET("/info", fs.GetFileInfo)
		fsController.GET("/search", fs.SearchFiles)

		// create/modify operations
		fsController.POST("/folder", fs.CreateFolder)
		fsController.POST("/move", fs.MoveFile)
		fsController.POST("/permissions", fs.SetFilePermissions)
		fsController.POST("/replace", fs.ReplaceInFiles)
		fsController.POST("/upload", fs.UploadFile)
		fsController.POST("/bulk-upload", fs.UploadFiles)

		// delete operations
		fsController.DELETE("/", fs.DeleteFile)
	}

	processLogger := s.logger.With(slog.String("component", "process_controller"))
	processController := r.Group("/process")
	{
		processController.POST("/execute", process.ExecuteCommand(processLogger))
		processController.POST("/code-run", coderun.CodeRun(processLogger))

		sessionController := session.NewSessionController(s.logger, s.configDir, s.sessionService)
		sessionGroup := processController.Group("/session")
		{
			sessionGroup.GET("", sessionController.ListSessions)
			sessionGroup.POST("", sessionController.CreateSession)
			sessionGroup.GET("/entrypoint", sessionController.GetEntrypointSession)
			sessionGroup.GET("/entrypoint/logs", sessionController.GetEntrypointLogs)
			sessionGroup.POST("/:sessionId/exec", sessionController.SessionExecuteCommand)
			sessionGroup.GET("/:sessionId", sessionController.GetSession)
			sessionGroup.DELETE("/:sessionId", sessionController.DeleteSession)
			sessionGroup.GET("/:sessionId/command/:commandId", sessionController.GetSessionCommand)
			sessionGroup.POST("/:sessionId/command/:commandId/input", sessionController.SendInput)
			sessionGroup.GET("/:sessionId/command/:commandId/logs", sessionController.GetSessionCommandLogs)
		}

		// PTY endpoints
		ptyController := pty.NewPTYController(s.logger, s.WorkDir)
		ptyGroup := processController.Group("/pty")
		{
			ptyGroup.GET("", ptyController.ListPTYSessions)
			ptyGroup.POST("", ptyController.CreatePTYSession)
			ptyGroup.GET("/:sessionId", ptyController.GetPTYSession)
			ptyGroup.DELETE("/:sessionId", ptyController.DeletePTYSession)
			ptyGroup.GET("/:sessionId/connect", ptyController.ConnectPTYSession)
			ptyGroup.POST("/:sessionId/resize", ptyController.ResizePTYSession)
		}

		// Interpreter endpoints
		interpreterController := interpreter.NewInterpreterController(s.logger, s.WorkDir)
		interpreterGroup := processController.Group("/interpreter")
		{
			interpreterGroup.POST("/context", interpreterController.CreateContext)
			interpreterGroup.GET("/context", interpreterController.ListContexts)
			interpreterGroup.DELETE("/context/:id", interpreterController.DeleteContext)
			interpreterGroup.GET("/execute", interpreterController.Execute)
		}
	}

	gitController := r.Group("/git")
	{
		gitController.GET("/branches", git.ListBranches)
		gitController.GET("/history", git.GetCommitHistory)
		gitController.GET("/status", git.GetStatus)

		gitController.POST("/add", git.AddFiles)
		gitController.POST("/branches", git.CreateBranch)
		gitController.POST("/checkout", git.CheckoutBranch)
		gitController.DELETE("/branches", git.DeleteBranch)
		gitController.POST("/clone", git.CloneRepository)
		gitController.POST("/commit", git.CommitChanges)
		gitController.POST("/pull", git.PullChanges)
		gitController.POST("/push", git.PushChanges)
	}

	lspLogger := s.logger.With(slog.String("component", "lsp_service"))
	lspController := r.Group("/lsp")
	{
		//	server process
		lspController.POST("/start", lsp.Start(lspLogger))
		lspController.POST("/stop", lsp.Stop(lspLogger))

		//	lsp operations
		lspController.POST("/completions", lsp.Completions(lspLogger))
		lspController.POST("/did-open", lsp.DidOpen(lspLogger))
		lspController.POST("/did-close", lsp.DidClose(lspLogger))

		lspController.GET("/document-symbols", lsp.DocumentSymbols(lspLogger))
		lspController.GET("/workspacesymbols", lsp.WorkspaceSymbols(lspLogger))
	}

	lazyCU := computeruse.NewLazyComputerUse()
	s.ComputerUse = lazyCU

	go func() {
		// Initialize plugin-based computer use lazily in a background goroutine
		pluginPath := "/usr/local/lib/daytona-computer-use"
		// Fallback to local config directory for development
		if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
			pluginPath = path.Join(s.configDir, "daytona-computer-use")
		}

		impl, err := manager.GetComputerUse(s.logger, pluginPath)
		if err != nil {
			s.logger.Error("Computer-Use error", "error", err)
			s.logger.Info("Continuing without computer-use functionality...")
			return
		}
		lazyCU.Set(impl)
		s.logger.Info("Computer-use plugin loaded successfully")
	}()

	// Register computer-use endpoints with lazy check middleware
	computerUseController := r.Group("/computeruse")
	{
		computerUseHandler := computeruse.Handler{
			ComputerUse: lazyCU,
		}

		cuRoutes := computerUseController.Group("/", computeruse.LazyCheckMiddleware(lazyCU))

		// Computer use status endpoint
		cuRoutes.GET("/status", computeruse.WrapStatusHandler(lazyCU.GetStatus))

		// Computer use management endpoints
		cuRoutes.POST("/start", computerUseHandler.StartComputerUse)
		cuRoutes.POST("/stop", computerUseHandler.StopComputerUse)
		cuRoutes.GET("/process-status", computerUseHandler.GetComputerUseStatus)
		cuRoutes.GET("/process/:processName/status", computerUseHandler.GetProcessStatus)
		cuRoutes.POST("/process/:processName/restart", computerUseHandler.RestartProcess)
		cuRoutes.GET("/process/:processName/logs", computerUseHandler.GetProcessLogs)
		cuRoutes.GET("/process/:processName/errors", computerUseHandler.GetProcessErrors)

		// Screenshot endpoints
		cuRoutes.GET("/screenshot", computeruse.WrapScreenshotHandler(lazyCU.TakeScreenshot))
		cuRoutes.GET("/screenshot/region", computeruse.WrapRegionScreenshotHandler(lazyCU.TakeRegionScreenshot))
		cuRoutes.GET("/screenshot/compressed", computeruse.WrapCompressedScreenshotHandler(lazyCU.TakeCompressedScreenshot))
		cuRoutes.GET("/screenshot/region/compressed", computeruse.WrapCompressedRegionScreenshotHandler(lazyCU.TakeCompressedRegionScreenshot))

		// Mouse control endpoints
		cuRoutes.GET("/mouse/position", computeruse.WrapMousePositionHandler(lazyCU.GetMousePosition))
		cuRoutes.POST("/mouse/move", computeruse.WrapMoveMouseHandler(lazyCU.MoveMouse))
		cuRoutes.POST("/mouse/click", computeruse.WrapClickHandler(lazyCU.Click))
		cuRoutes.POST("/mouse/drag", computeruse.WrapDragHandler(lazyCU.Drag))
		cuRoutes.POST("/mouse/scroll", computeruse.WrapScrollHandler(lazyCU.Scroll))

		// Keyboard control endpoints
		cuRoutes.POST("/keyboard/type", computeruse.WrapTypeTextHandler(lazyCU.TypeText))
		cuRoutes.POST("/keyboard/key", computeruse.WrapPressKeyHandler(lazyCU.PressKey))
		cuRoutes.POST("/keyboard/hotkey", computeruse.WrapPressHotkeyHandler(lazyCU.PressHotkey))

		// Display info endpoints
		cuRoutes.GET("/display/info", computeruse.WrapDisplayInfoHandler(lazyCU.GetDisplayInfo))
		cuRoutes.GET("/display/windows", computeruse.WrapWindowsHandler(lazyCU.GetWindows))

		// Accessibility (AT-SPI) endpoints
		cuRoutes.GET("/a11y/tree", computeruse.WrapGetAccessibilityTreeHandler(lazyCU.GetAccessibilityTree))
		cuRoutes.POST("/a11y/find", computeruse.WrapFindAccessibilityNodesHandler(lazyCU.FindAccessibilityNodes))
		cuRoutes.POST("/a11y/node/focus", computeruse.WrapFocusAccessibilityNodeHandler(lazyCU.FocusAccessibilityNode))
		cuRoutes.POST("/a11y/node/invoke", computeruse.WrapInvokeAccessibilityNodeHandler(lazyCU.InvokeAccessibilityNode))
		cuRoutes.POST("/a11y/node/value", computeruse.WrapSetAccessibilityNodeValueHandler(lazyCU.SetAccessibilityNodeValue))
	}

	// Recording endpoints - always registered, independent of computer-use plugin
	recordingController := recordingcontroller.NewRecordingController(s.recordingService)
	recordingsGroup := computerUseController.Group("/recordings")
	{
		recordingsGroup.POST("/start", recordingController.StartRecording)
		recordingsGroup.POST("/stop", recordingController.StopRecording)
		recordingsGroup.GET("", recordingController.ListRecordings)
		recordingsGroup.GET("/:id", recordingController.GetRecording)
		recordingsGroup.GET("/:id/download", recordingController.DownloadRecording)
		recordingsGroup.DELETE("/:id", recordingController.DeleteRecording)
	}

	portDetector := port.NewPortsDetector()

	portController := r.Group("/port")
	{
		portController.GET("", portDetector.GetPorts)
		portController.GET("/:port/in-use", portDetector.IsPortInUse)
	}

	proxyController := noTelemetryRouter.Group("/proxy")
	{
		proxyController.Any("/:port/*path", common_proxy.NewProxyRequestHandler(proxy.GetProxyTarget, nil))
	}

	go portDetector.Start(context.Background())

	s.httpServer = &http.Server{
		Addr:    fmt.Sprintf(":%d", config.TOOLBOX_API_PORT),
		Handler: r,
	}

	// Print to stdout so the runner can know that the daemon is ready
	fmt.Println("Starting toolbox server on port", config.TOOLBOX_API_PORT)

	listener, err := net.Listen("tcp", s.httpServer.Addr)
	if err != nil {
		return err
	}

	return s.httpServer.Serve(listener)
}

func (s *server) Shutdown() {
	s.logger.Info("Shutting down toolbox server")

	// Stop accepting new requests and drain in-flight ones
	if s.httpServer != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := s.httpServer.Shutdown(ctx); err != nil {
			s.logger.Error("toolbox HTTP server shutdown error", "error", err)
		}
	}

	// Stop computer use if running
	if s.ComputerUse != nil {
		s.logger.Info("Stopping computer use...")
		_, err := s.ComputerUse.Stop()
		if err != nil {
			s.logger.Error("Failed to stop computer use", "error", err)
		}
	}

	// Flush telemetry
	if s.telemetry.TracerProvider != nil {
		s.logger.Info("Shutting down tracer provider")
		telemetry.ShutdownTracer(s.logger, s.telemetry.TracerProvider)
	}

	if s.telemetry.MeterProvider != nil {
		s.logger.Info("Shutting down meter provider")
		telemetry.ShutdownMeter(s.logger, s.telemetry.MeterProvider)
	}

	if s.telemetry.LoggerProvider != nil {
		s.logger.Info("Shutting down logger provider")
		telemetry.ShutdownLogger(s.logger, s.telemetry.LoggerProvider)
	}
}
