// API 服务组装模块，负责中间件、路由、静态资源和 HTTP 服务参数配置。
package server

import (
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"juhe-factory/api/internal/config"
	"juhe-factory/api/internal/handler"

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

func New(cfg config.Config, health *handler.Health, adminAuth *handler.AdminAuth, adminData *handler.AdminData, web *handler.Web, creative *handler.Creative, productImage *handler.ProductImage) *http.Server {
	if !cfg.Debug {
		gin.SetMode(gin.ReleaseMode)
	}
	router := gin.New()
	router.Use(traceID())
	router.Use(requestLogger())
	router.Use(gin.Recovery(), cors(cfg.CORSOrigins))

	registerRoutes(router, health, adminAuth, adminData, web, creative, productImage)
	registerStatic(router, cfg.StaticDirectory)

	return &http.Server{
		Addr:              cfg.ServerAddress,
		Handler:           router,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      5 * time.Minute,
		IdleTimeout:       60 * time.Second,
	}
}

// requestLogger 记录 HTTP 请求及其追踪 ID，便于通过审计记录关联服务器日志。
// 正常响应（2xx）不输出，仅在出错（4xx/5xx）时记录，避免前端轮询请求刷屏。
func requestLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		startedAt := time.Now()
		c.Next()
		if c.Writer.Status() < 400 {
			return
		}
		slog.InfoContext(c.Request.Context(), "http request",
			"trace_id", c.GetString("trace_id"),
			"method", c.Request.Method,
			"path", c.Request.URL.Path,
			"status", c.Writer.Status(),
			"latency_ms", time.Since(startedAt).Milliseconds(),
			"response_bytes", c.Writer.Size(),
		)
	}
}

func registerRoutes(router *gin.Engine, health *handler.Health, auth *handler.AdminAuth, data *handler.AdminData, web *handler.Web, creative *handler.Creative, productImage *handler.ProductImage) {
	router.GET("/health", health.Check)
	webRoutes := router.Group("/api/web")
	webRoutes.GET("/health", health.Check)
	webRoutes.POST("/auth/login", web.Login)
	webRoutes.POST("/auth/refresh", web.Refresh)
	webRoutes.POST("/auth/logout", web.Logout)
	webProtected := webRoutes.Group("")
	webProtected.Use(web.Middleware())
	webProtected.GET("/account", web.Account)
	webProtected.PATCH("/account/profile", web.UpdateProfile)
	webProtected.POST("/account/avatar", web.UploadAvatar)
	webProtected.PUT("/account/password", web.ChangePassword)
	webProtected.GET("/usage/30-days", web.Usage30Days)
	webProtected.GET("/usage/consumption-records", web.ConsumptionRecords)
	webProtected.POST("/redemption/redeem", web.Redeem)
	webProtected.GET("/user/prompts/:prompt_type", creative.ListUserPrompts)
	webProtected.PUT("/user/prompts/:prompt_type/select", creative.SelectUserPrompt)
	webProtected.POST("/user/custom-prompts", creative.CreateCustomPrompt)
	webProtected.PUT("/user/custom-prompts/:id", creative.UpdateCustomPrompt)
	webProtected.DELETE("/user/custom-prompts/:id", creative.DeleteCustomPrompt)
	webProtected.GET("/creative/options", creative.ListOptions)
	webProtected.POST("/product-images/generations", productImage.Generate)
	webProtected.GET("/product-images/generations", productImage.ListGenerations)
	webProtected.DELETE("/product-images/generations/:task_id", productImage.DeleteGeneration)
	webProtected.GET("/user/model-configs/:scope", creative.ListUserModelPreferences)
	webProtected.PUT("/user/model-configs/:scope/:model_type", creative.SaveUserModelPreference)
	webProtected.GET("/script-analyses", creative.ListScriptAnalyses)
	webProtected.POST("/script-analyses", creative.CreateScriptAnalysis)
	webProtected.GET("/script-analyses/import-projects", creative.ListScriptAnalysisImportProjects)
	webProtected.GET("/script-analyses/:script_id", creative.GetScriptAnalysis)
	webProtected.PUT("/script-analyses/:script_id", creative.UpdateScriptAnalysis)
	webProtected.POST("/script-analyses/:script_id/import-project", creative.ImportScriptAnalysisProject)
	webProtected.POST("/script-analyses/:script_id/import-file", creative.ImportScriptAnalysisFile)
	webProtected.POST("/script-analyses/:script_id/analyze", creative.QueueScriptAnalysis)
	webProtected.POST("/script-analyses/:script_id/cancel", creative.CancelScriptAnalysis)
	webProtected.DELETE("/script-analyses/:script_id", creative.DeleteScriptAnalysis)
	webProtected.GET("/media/:media_id", creative.GetMedia)
	webProtected.GET("/projects", creative.ListProjects)
	webProtected.POST("/projects", creative.CreateProject)
	webProtected.GET("/projects/:project_id", creative.GetProject)
	webProtected.PUT("/projects/:project_id", creative.UpdateProject)
	webProtected.DELETE("/projects/:project_id", creative.DeleteProject)
	webProtected.PUT("/projects/:project_id/model-configs", creative.SaveModelConfigs)
	webProtected.POST("/projects/:project_id/episodes", creative.CreateEpisode)
	webProtected.PUT("/projects/:project_id/episodes/:episode_id", creative.UpdateEpisodeName)
	webProtected.POST("/projects/:project_id/episodes/import/preview", creative.PreviewDramaImport)
	webProtected.POST("/projects/:project_id/episodes/import/confirm", creative.ConfirmDramaImport)
	webProtected.DELETE("/projects/:project_id/episodes/:episode_id", creative.DeleteEpisode)
	webProtected.GET("/projects/:project_id/episodes/:episode_id/text-source", creative.GetDramaEpisodeSource)
	webProtected.PUT("/projects/:project_id/episodes/:episode_id/text-source", creative.SaveDramaEpisodeSource)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/parse", creative.QueueDramaParse)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/reparse", creative.ReparseDrama)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/storyboards", creative.CreateDramaStoryboard)
	webProtected.GET("/projects/:project_id/parse-tasks", creative.ListDramaParseTasks)
	webProtected.POST("/projects/:project_id/parse-tasks/:task_id/cancel", creative.CancelDramaParse)
	webProtected.GET("/projects/:project_id/episodes/:episode_id/workbench", creative.GetWorkbench)
	webProtected.GET("/projects/:project_id/episodes/:episode_id/videos.zip", creative.DownloadEpisodeVideos)
	webProtected.GET("/projects/:project_id/episodes/:episode_id/redraw-workbench", creative.GetRedrawEpisodeWorkbench)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/redraw-script", creative.QueueRedrawEpisodeScript)
	webProtected.PUT("/projects/:project_id/episodes/:episode_id/redraw-script", creative.SaveRedrawEpisodeScript)
	webProtected.GET("/projects/:project_id/redraw-workbench", creative.GetRedrawWorkbench)
	webProtected.POST("/projects/:project_id/redraw-source", creative.UploadRedrawSource)
	webProtected.POST("/projects/:project_id/redraw-subtitle", creative.UploadRedrawSubtitle)
	webProtected.POST("/projects/:project_id/redraw-analyze", creative.QueueRedrawAnalysis)
	webProtected.POST("/projects/:project_id/redraw-reanalyze", creative.ReanalyzeRedraw)
	webProtected.POST("/projects/:project_id/redraw-script", creative.QueueRedrawScript)
	webProtected.PUT("/projects/:project_id/redraw-analysis-settings", creative.UpdateRedrawAnalysisSettings)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/source", creative.UploadEpisodeSource)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/subtitle", creative.UploadEpisodeSubtitle)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/analyze", creative.QueueAnalysis)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/reanalyze", creative.Reanalyze)
	webProtected.PUT("/projects/:project_id/episodes/:episode_id/analysis-settings", creative.UpdateEpisodeAnalysisSettings)
	webProtected.POST("/projects/:project_id/assets", creative.CreateAsset)
	webProtected.PUT("/projects/:project_id/assets/:asset_id", creative.UpdateAsset)
	webProtected.DELETE("/projects/:project_id/assets/:asset_id", creative.DeleteAsset)
	webProtected.POST("/projects/:project_id/assets/:asset_id/image", creative.UploadAssetImage)
	webProtected.DELETE("/projects/:project_id/assets/:asset_id/image", creative.RemoveAssetImage)
	webProtected.POST("/projects/:project_id/assets/:asset_id/audio", creative.UploadAssetAudio)
	webProtected.PUT("/projects/:project_id/storyboards/:storyboard_id", creative.UpdateStoryboard)
	webProtected.POST("/projects/:project_id/storyboards/:storyboard_id/insert", creative.InsertStoryboard)
	webProtected.DELETE("/projects/:project_id/storyboards/:storyboard_id", creative.DeleteStoryboard)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/storyboards/:storyboard_id/reinfer", creative.QueueStoryboardAnalysis)
	webProtected.POST("/projects/:project_id/episodes/:episode_id/storyboards/:storyboard_id/generate", creative.QueueGeneration)
	webProtected.POST("/projects/:project_id/storyboards/:storyboard_id/redraw-reinfer", creative.QueueRedrawStoryboardAnalysis)
	webProtected.GET("/projects/:project_id/tasks", creative.ListTasks)
	webProtected.POST("/projects/:project_id/tasks/:task_id/cancel", creative.CancelTask)
	webProtected.GET("/projects/:project_id/storyboards/:storyboard_id/outputs", creative.ListStoryboardOutputs)
	webProtected.DELETE("/projects/:project_id/storyboards/:storyboard_id/image", creative.RemoveStoryboardImage)
	webProtected.PUT("/projects/:project_id/storyboards/:storyboard_id/outputs/:output_id/activate", creative.ActivateStoryboardOutput)
	webProtected.PUT("/projects/:project_id/storyboards/:storyboard_id/outputs/:output_id/candidate", creative.AddStoryboardCandidate)
	webProtected.DELETE("/projects/:project_id/storyboards/:storyboard_id/outputs/:output_id/candidate", creative.RemoveStoryboardCandidate)
	webProtected.DELETE("/projects/:project_id/storyboards/:storyboard_id/outputs/:output_id", creative.DeleteStoryboardOutput)
	webProtected.GET("/projects/:project_id/assets/:asset_id/outputs", creative.ListAssetOutputs)
	webProtected.PUT("/projects/:project_id/assets/:asset_id/outputs/:output_id/activate", creative.ActivateAssetOutput)
	webProtected.DELETE("/projects/:project_id/assets/:asset_id/outputs/:output_id", creative.DeleteAssetOutput)
	admin := router.Group("/api/admin")
	admin.GET("/health", health.Check)
	admin.POST("/auth/login", auth.Login)
	admin.POST("/auth/refresh", auth.Refresh)
	admin.POST("/auth/logout", auth.Logout)
	protected := admin.Group("")
	protected.Use(auth.Middleware())
	protected.PUT("/auth/password", auth.ChangePassword)
	protected.GET("/audit-logs", data.ListAuditLogs)
	protected.GET("/users", data.ListUsers)
	protected.POST("/users", data.CreateUser)
	protected.POST("/users/batch", data.BatchCreateUsers)
	protected.PATCH("/users/batch", data.BatchUpdateUsers)
	protected.DELETE("/users/batch", data.BatchDeleteUsers)
	protected.POST("/users/:id/points", data.GrantUserPoints)
	protected.GET("/models", data.ListModels)
	protected.POST("/models", data.SaveModel)
	protected.PUT("/models/:id", data.SaveModel)
	protected.GET("/prompts", data.ListPrompts)
	protected.POST("/prompts", data.SavePromptSimple)
	protected.PUT("/prompts/:id", data.SavePromptSimple)
	protected.DELETE("/prompts/:id", data.DeletePromptSimple)
	protected.POST("/prompts/:id/actions", data.PromptActionSimple)
	protected.GET("/prompts/:id/history", data.PromptHistorySimple)
	protected.GET("/redemption-codes", data.ListRedemptions)
	protected.POST("/redemption-batches", data.CreateRedemptionBatch)
	protected.POST("/uploads/style-images", data.UploadStyleImage)
	protected.PUT("/resources/styles/reorder", data.ReorderStyles)
	resources := protected.Group("/resources/:resource")
	resources.Use(allowAdminResource("styles", "channels", "models"))
	resources.GET("", data.ListResource)
	resources.POST("", data.CreateResource)
	resources.PUT("/:id", data.UpdateResource)
	resources.PATCH("/:id/enabled", data.ToggleResource)
	resources.DELETE("/:id", data.DeleteResource)
}

func allowAdminResource(resources ...string) gin.HandlerFunc {
	allowed := make(map[string]struct{}, len(resources))
	for _, resource := range resources {
		allowed[resource] = struct{}{}
	}
	return func(c *gin.Context) {
		if _, ok := allowed[c.Param("resource")]; !ok {
			c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"code": "route_not_found", "message": "接口不存在"})
			return
		}
		c.Next()
	}
}

func traceID() gin.HandlerFunc {
	return func(c *gin.Context) {
		id := strings.TrimSpace(c.GetHeader("X-Request-ID"))
		if id == "" {
			id = uuid.NewString()
		}
		c.Set("trace_id", id)
		c.Header("X-Request-ID", id)
		c.Next()
	}
}

func registerStatic(router *gin.Engine, root string) {
	adminRoot := filepath.Join(root, "admin")
	webRoot := filepath.Join(root, "web")
	router.Static("/admin/assets", filepath.Join(adminRoot, "assets"))
	router.Static("/assets", filepath.Join(webRoot, "assets"))
	router.GET("/admin", func(c *gin.Context) { c.File(filepath.Join(adminRoot, "index.html")) })
	router.GET("/", func(c *gin.Context) { c.File(filepath.Join(webRoot, "index.html")) })

	router.NoRoute(func(c *gin.Context) {
		path := c.Request.URL.Path
		if strings.HasPrefix(path, "/api/") {
			c.JSON(http.StatusNotFound, gin.H{"code": "route_not_found", "message": "接口不存在"})
			return
		}
		if strings.HasPrefix(path, "/admin") {
			c.File(filepath.Join(adminRoot, "index.html"))
			return
		}

		relativePath := filepath.Clean(strings.TrimPrefix(path, "/"))
		if filepath.IsLocal(relativePath) {
			candidate := filepath.Join(webRoot, relativePath)
			if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
				c.File(candidate)
				return
			}
		}
		c.File(filepath.Join(webRoot, "index.html"))
	})
}

func cors(allowedOrigins []string) gin.HandlerFunc {
	allowed := make(map[string]struct{}, len(allowedOrigins))
	for _, origin := range allowedOrigins {
		allowed[origin] = struct{}{}
	}
	return func(c *gin.Context) {
		origin := c.GetHeader("Origin")
		if _, ok := allowed[origin]; ok {
			c.Header("Access-Control-Allow-Origin", origin)
			c.Header("Access-Control-Allow-Credentials", "true")
			c.Header("Vary", "Origin")
		}
		c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
		c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
		if c.Request.Method == http.MethodOptions {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}
		c.Next()
	}
}
