// SPDX-License-Identifier: GPL-3.0-or-later

// Package api provides the REST API server for frontend clients.
package api

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

// Config holds API server configuration.
type Config struct {
	Host           string
	Port           int
	ReadTimeout    time.Duration
	WriteTimeout   time.Duration
	AllowedOrigins []string

	// WebDir, when non-empty, makes the server also serve static assets
	// from that directory at the root. Requests to /api/* still hit the
	// API handlers; everything else is served from disk with SPA-style
	// index.html fallback for unmatched paths.
	WebDir string
}

// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() Config {
	return Config{
		Host:        "127.0.0.1",
		Port:        62774,
		ReadTimeout: 30 * time.Second,
		// 0 = no write deadline. This is a localhost-only API that serves long
		// restic-over-Tor operations (e.g. snapshot listing, ~240s measured) and
		// SSE job-progress streams; a fixed WriteTimeout truncated those and
		// surfaced to the client as an EOF. The caller controls its own timeout.
		WriteTimeout:   0,
		AllowedOrigins: []string{"*"},
	}
}

// Validate validates the configuration.
func (c *Config) Validate() error {
	if c.Port < 1 || c.Port > 65535 {
		return errors.New("port must be between 1 and 65535")
	}
	if c.ReadTimeout <= 0 {
		return errors.New("read timeout must be positive")
	}
	if c.WriteTimeout < 0 {
		return errors.New("write timeout must not be negative")
	}
	return nil
}

// Address returns the full address to bind to.
func (c *Config) Address() string {
	return fmt.Sprintf("%s:%d", c.Host, c.Port)
}

// Server is the REST API server.
type Server struct {
	config     Config
	router     *gin.Engine
	httpServer *http.Server
	handlers   *Handlers
}

// NewServer creates a new API server.
func NewServer(config Config, handlers *Handlers) (*Server, error) {
	if err := config.Validate(); err != nil {
		return nil, fmt.Errorf("invalid config: %w", err)
	}
	if handlers == nil {
		return nil, errors.New("handlers are required")
	}

	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	router.Use(gin.Recovery())

	s := &Server{
		config:   config,
		router:   router,
		handlers: handlers,
	}

	s.setupRoutes()

	return s, nil
}

func (s *Server) setupRoutes() {
	s.router.Use(corsMiddleware(s.config.AllowedOrigins))

	api := s.router.Group("/api/v1")
	{
		api.GET("/health", s.handlers.HealthCheck)
		api.GET("/status", s.handlers.GetStatus)
		api.GET("/status/tor", s.handlers.GetTorStatus)
		api.GET("/status/server", s.handlers.GetServerStatus)
		api.GET("/account", s.handlers.GetAccount)
		api.POST("/account/register", s.handlers.Register)

		backup := api.Group("/backup")
		{
			backup.POST("/start", s.handlers.StartBackup)
			backup.GET("/status", s.handlers.GetBackupStatus)
			backup.POST("/cancel", s.handlers.CancelBackup)
		}

		restore := api.Group("/restore")
		{
			restore.POST("/start", s.handlers.StartRestore)
			restore.GET("/status", s.handlers.GetRestoreStatus)
			restore.POST("/cancel", s.handlers.CancelRestore)
		}

		snapshots := api.Group("/snapshots")
		{
			snapshots.GET("", s.handlers.ListSnapshots)
			snapshots.GET("/:id", s.handlers.GetSnapshot)
			snapshots.GET("/:id/files", s.handlers.GetSnapshotFiles)
			snapshots.DELETE("/:id", s.handlers.DeleteSnapshot)
			snapshots.POST("/forget", s.handlers.ForgetSnapshots)
		}

		api.POST("/check", s.handlers.StartCheck)
		api.POST("/unlock", s.handlers.Unlock)
		api.POST("/account/payment", s.handlers.CreatePayment)
		api.POST("/account/payment/ltc", s.handlers.CreateLTCPayment)
		api.GET("/account/payment/ltc/status", s.handlers.GetLTCPaymentStatus)

		jobs := api.Group("/jobs")
		{
			jobs.GET("", s.handlers.ListJobs)
			jobs.GET("/:id", s.handlers.GetJob)
			jobs.GET("/:id/progress", s.handlers.StreamJobProgress)
			jobs.POST("/:id/cancel", s.handlers.CancelJob)
		}

		schedules := api.Group("/schedules")
		{
			schedules.GET("", s.handlers.ListSchedules)
			schedules.POST("", s.handlers.CreateSchedule)
			schedules.GET("/:name", s.handlers.GetSchedule)
			schedules.PUT("/:name", s.handlers.UpdateSchedule)
			schedules.DELETE("/:name", s.handlers.DeleteSchedule)
			schedules.POST("/:name/trigger", s.handlers.TriggerSchedule)
		}

		// Configuration
		config := api.Group("/config")
		{
			config.GET("", s.handlers.GetConfig)
			config.PUT("", s.handlers.UpdateConfig)
		}

		// Mode management (simple vs advanced)
		mode := api.Group("/mode")
		{
			mode.GET("", s.handlers.GetMode)
			mode.PUT("", s.handlers.UpdateMode)
		}

		// S3 credential management
		credentials := api.Group("/credentials")
		{
			credentials.GET("", s.handlers.GetCredentials)
			credentials.POST("/rotate", s.handlers.RotateCredentials)
		}

		// Backup paths management
		paths := api.Group("/paths")
		{
			paths.GET("", s.handlers.ListPaths)
			paths.POST("", s.handlers.AddPath)
			paths.DELETE("", s.handlers.RemovePath)
		}

		// Retention policy (read-only in simple mode)
		api.GET("/retention", s.handlers.GetRetention)

		// Self-update endpoints
		updates := api.Group("/updates")
		{
			updates.GET("/status", s.handlers.GetUpdateStatus)
			updates.POST("/check", s.handlers.CheckForUpdates)
			updates.GET("/config", s.handlers.GetUpdateConfig)
			updates.PUT("/config", s.handlers.UpdateUpdateConfig)
			updates.GET("/binary/:component", s.handlers.FetchUpdateBinary)
			updates.GET("/tauri-manifest", s.handlers.GetTauriManifest)
		}
	}

	// Optional static web UI. Mounted last so /api/* always wins.
	if s.config.WebDir != "" {
		s.mountWebUI(s.config.WebDir)
	}
}

// mountWebUI configures the router to serve static assets from webDir with
// SPA-style fallback to index.html for paths that don't match a file.
func (s *Server) mountWebUI(webDir string) {
	indexPath := filepath.Join(webDir, "index.html")

	// Anything under /assets/ is hashed and immutable — serve directly.
	s.router.Static("/assets", filepath.Join(webDir, "assets"))

	// Root and SPA routes — fall back to index.html so client-side routing works.
	serveIndex := func(c *gin.Context) {
		c.File(indexPath)
	}
	s.router.GET("/", serveIndex)

	s.router.NoRoute(func(c *gin.Context) {
		// Don't shadow API misses.
		if strings.HasPrefix(c.Request.URL.Path, "/api/") {
			c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
			return
		}
		// Try the exact file first (covers /favicon.ico, /robots.txt, etc).
		candidate := filepath.Join(webDir, filepath.Clean(c.Request.URL.Path))
		if rel, err := filepath.Rel(webDir, candidate); err == nil && !strings.HasPrefix(rel, "..") {
			if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
				c.File(candidate)
				return
			}
		}
		// SPA fallback: any unmatched non-API path renders the app shell.
		c.File(indexPath)
	})
}

// Start starts the API server.
func (s *Server) Start() error {
	s.httpServer = &http.Server{
		Addr:         s.config.Address(),
		Handler:      s.router,
		ReadTimeout:  s.config.ReadTimeout,
		WriteTimeout: s.config.WriteTimeout,
	}

	return s.httpServer.ListenAndServe()
}

// Stop gracefully stops the API server.
func (s *Server) Stop(ctx context.Context) error {
	if s.httpServer == nil {
		return nil
	}
	return s.httpServer.Shutdown(ctx)
}

// Router returns the underlying Gin router for testing.
func (s *Server) Router() *gin.Engine {
	return s.router
}

func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
	return func(c *gin.Context) {
		origin := c.Request.Header.Get("Origin")

		allowed := false
		for _, o := range allowedOrigins {
			if o == "*" || o == origin {
				allowed = true
				break
			}
		}

		if allowed && origin != "" {
			c.Header("Access-Control-Allow-Origin", origin)
			c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
			c.Header("Access-Control-Max-Age", "86400")
		}

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}

		c.Next()
	}
}
