package api

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"scheduler-backend/internal/executor"
	"scheduler-backend/internal/jobdef"
	"scheduler-backend/internal/store/model"
)

type handlerItem struct {
	Key         string `json:"key"`
	Name        string `json:"name"`
	Description string `json:"description"`
}

type handlerListResponse struct {
	List []handlerItem `json:"list"`
}

type jobItem struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	HandlerKey    string `json:"handlerKey"`
	Enabled       bool   `json:"enabled"`
	ScheduleType  string `json:"scheduleType"`
	ScheduleValue string `json:"scheduleValue"`
	DefaultParams string `json:"defaultParams"`
	LastStatus    string `json:"lastStatus"`
	LastRunAt     string `json:"lastRunAt,omitempty"`
	NextRunAt     string `json:"nextRunAt,omitempty"`
}

type jobListResponse struct {
	List  []jobItem `json:"list"`
	Total int       `json:"total"`
}

type executionItem struct {
	ID            string `json:"id"`
	JobName       string `json:"jobName"`
	TriggerType   string `json:"triggerType"`
	ScheduleType  string `json:"scheduleType,omitempty"`
	Status        string `json:"status"`
	StartedAt     string `json:"startedAt,omitempty"`
	FinishedAt    string `json:"finishedAt,omitempty"`
	ResultSummary string `json:"resultSummary"`
}

type executionDetailItem struct {
	ID             string `json:"id"`
	JobConfigID    string `json:"jobConfigId"`
	TriggerType    string `json:"triggerType"`
	ScheduleType   string `json:"scheduleType,omitempty"`
	Status         string `json:"status"`
	ParamsSnapshot string `json:"paramsSnapshot"`
	StartedAt      string `json:"startedAt,omitempty"`
	FinishedAt     string `json:"finishedAt,omitempty"`
	DurationMs     int64  `json:"durationMs"`
	ResultSummary  string `json:"resultSummary"`
	ErrorMessage   string `json:"errorMessage"`
	LogText        string `json:"logText"`
	LogFile        string `json:"logFile,omitempty"`
	CreatedAt      string `json:"createdAt"`
}

type executionListResponse struct {
	List  []executionItem `json:"list"`
	Total int             `json:"total"`
}

type profileItem struct {
	ID        string `json:"id"`
	Account   string `json:"account"`
	Nickname  string `json:"nickname"`
	Avatar    string `json:"avatar"`
	Remark    string `json:"remark"`
	CreatedAt string `json:"createdAt"`
	UpdatedAt string `json:"updatedAt"`
}

type adminUserItem struct {
	ID        string `json:"id"`
	Account   string `json:"account"`
	Nickname  string `json:"nickname"`
	Status    string `json:"status"`
	Role      string `json:"role"`
	Remark    string `json:"remark"`
	CreatedAt string `json:"createdAt"`
	UpdatedAt string `json:"updatedAt"`
}

type adminUserListResponse struct {
	List  []adminUserItem `json:"list"`
	Total int             `json:"total"`
}

type systemConfigItem struct {
	ID          string `json:"id"`
	Key         string `json:"key"`
	Title       string `json:"title"`
	Value       string `json:"value"`
	ValueType   string `json:"valueType"`
	Description string `json:"description"`
	Enabled     bool   `json:"enabled"`
	CreatedAt   string `json:"createdAt"`
	UpdatedAt   string `json:"updatedAt"`
}

type systemConfigListResponse struct {
	List  []systemConfigItem `json:"list"`
	Total int                `json:"total"`
}

type dashboardOverviewResponse struct {
	JobTotal          int             `json:"jobTotal"`
	EnabledJobTotal   int             `json:"enabledJobTotal"`
	ExecutionToday    int             `json:"executionToday"`
	FailedToday       int             `json:"failedToday"`
	LastExecutionAt   string          `json:"lastExecutionAt,omitempty"`
	FailedJobTotal    int             `json:"failedJobTotal"`
	AdminUserTotal    int             `json:"adminUserTotal"`
	SystemConfigTotal int             `json:"systemConfigTotal"`
	RecentExecutions  []executionItem `json:"recentExecutions"`
}

type globalConfigItem struct {
	Config    string `json:"config"`
	UpdatedBy string `json:"updatedBy"`
	UpdatedAt string `json:"updatedAt,omitempty"`
}

type JobConfigLister interface {
	List(ctx context.Context) ([]model.JobConfig, error)
	GetByID(ctx context.Context, id primitive.ObjectID) (*model.JobConfig, error)
	Create(ctx context.Context, item *model.JobConfig) error
	Update(ctx context.Context, item *model.JobConfig) error
	ToggleEnabled(ctx context.Context, id primitive.ObjectID, enabled bool) error
	UpdateRunState(ctx context.Context, id primitive.ObjectID, status string, lastRunAt *time.Time) error
}

type JobExecutionLister interface {
	List(ctx context.Context) ([]model.JobExecution, error)
	GetByID(ctx context.Context, id primitive.ObjectID) (*model.JobExecution, error)
	Create(ctx context.Context, item *model.JobExecution) error
	UpdateResult(ctx context.Context, item *model.JobExecution) error
}

type AdminProfileStore interface {
	Get(ctx context.Context) (*model.AdminProfile, error)
	Upsert(ctx context.Context, profile *model.AdminProfile) error
	ChangePassword(ctx context.Context, oldPassword, newPassword string) error
	Authenticate(ctx context.Context, account, password string) (*model.AdminProfile, error)
}

type AdminUserStore interface {
	List(ctx context.Context) ([]model.AdminUser, error)
	GetByID(ctx context.Context, id primitive.ObjectID) (*model.AdminUser, error)
	Create(ctx context.Context, item *model.AdminUser) error
	Update(ctx context.Context, item *model.AdminUser) error
	ChangePassword(ctx context.Context, id primitive.ObjectID, newPassword string) error
	Authenticate(ctx context.Context, account, password string) (*model.AdminUser, error)
}

type SystemConfigStore interface {
	List(ctx context.Context) ([]model.SystemConfig, error)
	GetByID(ctx context.Context, id primitive.ObjectID) (*model.SystemConfig, error)
	Create(ctx context.Context, item *model.SystemConfig) error
	Update(ctx context.Context, item *model.SystemConfig) error
	ToggleEnabled(ctx context.Context, id primitive.ObjectID, enabled bool) error
}

type GlobalConfigStore interface {
	Get(ctx context.Context, key string) (*model.GlobalConfig, error)
	Upsert(ctx context.Context, item *model.GlobalConfig) error
}

type RouterDeps struct {
	Registry          *jobdef.Registry
	JobConfigStore    JobConfigLister
	ExecutionStore    JobExecutionLister
	ProfileStore      AdminProfileStore
	AdminUserStore    AdminUserStore
	ConfigStore       SystemConfigStore
	GlobalConfigStore GlobalConfigStore
	Executor          *executor.Service
}

type upsertJobRequest struct {
	Name          string `json:"name" binding:"required"`
	HandlerKey    string `json:"handlerKey" binding:"required"`
	Enabled       bool   `json:"enabled"`
	ScheduleType  string `json:"scheduleType" binding:"required"`
	ScheduleValue string `json:"scheduleValue"`
	DefaultParams string `json:"defaultParams"`
}

type toggleJobRequest struct {
	Enabled bool `json:"enabled"`
}

type runJobRequest struct {
	Params json.RawMessage `json:"params"`
}

type updateProfileRequest struct {
	Account  string `json:"account" binding:"required"`
	Nickname string `json:"nickname" binding:"required"`
	Avatar   string `json:"avatar"`
	Remark   string `json:"remark"`
}

type changeProfilePasswordRequest struct {
	OldPassword string `json:"oldPassword" binding:"required"`
	NewPassword string `json:"newPassword" binding:"required"`
}

type loginRequest struct {
	Account  string `json:"account" binding:"required"`
	Password string `json:"password" binding:"required"`
}

type upsertAdminUserRequest struct {
	Account  string `json:"account" binding:"required"`
	Nickname string `json:"nickname" binding:"required"`
	Status   string `json:"status"`
	Role     string `json:"role"`
	Remark   string `json:"remark"`
	Password string `json:"password"`
}

type changeAdminPasswordRequest struct {
	NewPassword string `json:"newPassword" binding:"required"`
}

type upsertSystemConfigRequest struct {
	Key         string `json:"key" binding:"required"`
	Title       string `json:"title" binding:"required"`
	Value       string `json:"value"`
	ValueType   string `json:"valueType" binding:"required"`
	Description string `json:"description"`
	Enabled     bool   `json:"enabled"`
}

type toggleSystemConfigRequest struct {
	Enabled bool `json:"enabled"`
}

type updateGlobalConfigRequest struct {
	Config json.RawMessage `json:"config" binding:"required"`
}

const schedulerGlobalConfigKey = "scheduler"

func NewRouter(deps RouterDeps) *gin.Engine {
	router := gin.Default()
	router.Use(func(c *gin.Context) {
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
		c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
		c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type")
		if c.Request.Method == http.MethodOptions {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}
		c.Next()
	})
	router.POST("/admin/account/login", func(c *gin.Context) {
		var req loginRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		profile, err := deps.ProfileStore.Authenticate(c.Request.Context(), req.Account, req.Password)
		if err == nil {
			c.JSON(http.StatusOK, toProfileItem(*profile))
			return
		}

		adminUser, userErr := deps.AdminUserStore.Authenticate(c.Request.Context(), req.Account, req.Password)
		if userErr != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid account or password"})
			return
		}

		c.JSON(http.StatusOK, adminUserItem{
			ID:        objectIDToString(adminUser.ID),
			Account:   adminUser.Account,
			Nickname:  adminUser.Nickname,
			Status:    adminUser.Status,
			Role:      adminUser.Role,
			Remark:    adminUser.Remark,
			CreatedAt: adminUser.CreatedAt.Format(time.RFC3339),
			UpdatedAt: adminUser.UpdatedAt.Format(time.RFC3339),
		})
	})
	router.GET("/admin/dashboard/overview", func(c *gin.Context) {
		jobs, err := deps.JobConfigStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		executions, err := deps.ExecutionStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		adminUsers, err := deps.AdminUserStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		systemConfigs, err := deps.ConfigStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		enabledCount := 0
		failedJobCount := 0
		jobNameMap := make(map[string]string, len(jobs))
		for _, item := range jobs {
			jobNameMap[item.ID.Hex()] = item.Name
			if item.Enabled {
				enabledCount++
			}
			if item.LastStatus == "failed" {
				failedJobCount++
			}
		}

		now := time.Now()
		todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
		executionToday := 0
		failedToday := 0
		lastExecutionAt := ""
		recent := make([]executionItem, 0, min(len(executions), 5))
		for idx, item := range executions {
			if idx < 5 {
				recent = append(recent, executionItem{
					ID:            objectIDToString(item.ID),
					JobName:       jobDisplayName(jobNameMap, item.JobConfigID),
					TriggerType:   item.TriggerType,
					Status:        item.Status,
					StartedAt:     formatTime(item.StartedAt),
					FinishedAt:    formatTime(item.FinishedAt),
					ResultSummary: item.ResultSummary,
				})
			}
			if lastExecutionAt == "" && item.StartedAt != nil {
				lastExecutionAt = item.StartedAt.Format(time.RFC3339)
			}
			if item.CreatedAt.After(todayStart) {
				executionToday++
				if item.Status == "failed" {
					failedToday++
				}
			}
		}

		c.JSON(http.StatusOK, dashboardOverviewResponse{
			JobTotal:          len(jobs),
			EnabledJobTotal:   enabledCount,
			ExecutionToday:    executionToday,
			FailedToday:       failedToday,
			LastExecutionAt:   lastExecutionAt,
			FailedJobTotal:    failedJobCount,
			AdminUserTotal:    len(adminUsers),
			SystemConfigTotal: len(systemConfigs),
			RecentExecutions:  recent,
		})
	})
	router.GET("/admin/scheduler/handlers", func(c *gin.Context) {
		items := make([]handlerItem, 0, len(deps.Registry.List()))
		for _, handler := range deps.Registry.List() {
			items = append(items, handlerItem{
				Key:         handler.Key(),
				Name:        handler.Name(),
				Description: handler.Description(),
			})
		}

		c.JSON(http.StatusOK, handlerListResponse{List: items})
	})
	router.GET("/admin/scheduler/jobs", func(c *gin.Context) {
		items, err := deps.JobConfigStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		responseItems := make([]jobItem, 0, len(items))
		for _, item := range items {
			responseItems = append(responseItems, toJobItem(item))
		}

		c.JSON(http.StatusOK, jobListResponse{
			List:  responseItems,
			Total: len(responseItems),
		})
	})
	router.GET("/admin/scheduler/jobs/:id", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}

		item, err := deps.JobConfigStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if item == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "job not found"})
			return
		}

		c.JSON(http.StatusOK, toJobItem(*item))
	})
	router.POST("/admin/scheduler/jobs", func(c *gin.Context) {
		var req upsertJobRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		item := model.NewJobConfig(req.Name, req.HandlerKey)
		item.Enabled = req.Enabled
		item.ScheduleType = req.ScheduleType
		item.ScheduleValue = req.ScheduleValue
		item.DefaultParams = req.DefaultParams

		if err := deps.JobConfigStore.Create(c.Request.Context(), &item); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusCreated, toJobItem(item))
	})
	router.PUT("/admin/scheduler/jobs/:id", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}

		current, err := deps.JobConfigStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if current == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "job not found"})
			return
		}

		var req upsertJobRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		current.Name = req.Name
		current.HandlerKey = req.HandlerKey
		current.Enabled = req.Enabled
		current.ScheduleType = req.ScheduleType
		current.ScheduleValue = req.ScheduleValue
		current.DefaultParams = req.DefaultParams

		if err := deps.JobConfigStore.Update(c.Request.Context(), current); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, toJobItem(*current))
	})
	router.POST("/admin/scheduler/jobs/:id/toggle", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}

		var req toggleJobRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		if err := deps.JobConfigStore.ToggleEnabled(c.Request.Context(), id, req.Enabled); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{"success": true})
	})
	router.POST("/admin/scheduler/jobs/:id/run", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}

		job, err := deps.JobConfigStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if job == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "job not found"})
			return
		}

		var req runJobRequest
		if err := c.ShouldBindJSON(&req); err != nil && err != io.EOF {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		params := req.Params
		params, err = mergeExecutionParams(job.DefaultParams, req.Params)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		now := time.Now()
		execRecord := model.JobExecution{
			JobConfigID:    job.ID,
			TriggerType:    "manual",
			Status:         "running",
			ParamsSnapshot: string(params),
			StartedAt:      &now,
			CreatedAt:      now,
		}
		if err := deps.ExecutionStore.Create(c.Request.Context(), &execRecord); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		execReq := jobdef.ExecuteRequest{
			ExecutionID:  execRecord.ID.Hex(),
			JobID:        job.ID.Hex(),
			TriggerType:  "manual",
			Params:       params,
			LogCollector: jobdef.NewLogCollector(),
		}
		runErr := deps.Executor.Execute(c.Request.Context(), job.HandlerKey, execReq)

		finishedAt := time.Now()
		execRecord.FinishedAt = &finishedAt
		execRecord.DurationMs = finishedAt.Sub(now).Milliseconds()
		flushed := execReq.LogCollector.Flush(execRecord.ID.Hex())
		if runErr != nil {
			execRecord.Status = "failed"
			execRecord.ErrorMessage = runErr.Error()
			execRecord.ResultSummary = "manual execution failed"
			if flushed.LogText != "" {
				execRecord.LogText = flushed.LogText
			} else {
				execRecord.LogText = runErr.Error()
			}
			execRecord.LogFile = flushed.LogFile
			_ = deps.JobConfigStore.UpdateRunState(c.Request.Context(), job.ID, "failed", &finishedAt)
		} else {
			execRecord.Status = "success"
			execRecord.ResultSummary = "manual execution succeeded"
			if flushed.LogText != "" {
				execRecord.LogText = flushed.LogText
			} else {
				execRecord.LogText = "handler executed successfully"
			}
			execRecord.LogFile = flushed.LogFile
			_ = deps.JobConfigStore.UpdateRunState(c.Request.Context(), job.ID, "success", &finishedAt)
		}

		if err := deps.ExecutionStore.UpdateResult(c.Request.Context(), &execRecord); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"executionID":   execRecord.ID.Hex(),
			"status":        execRecord.Status,
			"resultSummary": execRecord.ResultSummary,
			"errorMessage":  execRecord.ErrorMessage,
		})
	})
	router.GET("/admin/scheduler/global-config", func(c *gin.Context) {
		item, err := deps.GlobalConfigStore.Get(c.Request.Context(), schedulerGlobalConfigKey)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, toGlobalConfigItem(*item))
	})
	router.PUT("/admin/scheduler/global-config", func(c *gin.Context) {
		var req updateGlobalConfigRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if !json.Valid(req.Config) {
			c.JSON(http.StatusBadRequest, gin.H{"error": errInvalidJSON("config").Error()})
			return
		}
		item := &model.GlobalConfig{
			Key:       schedulerGlobalConfigKey,
			Config:    string(req.Config),
			UpdatedBy: "admin",
		}
		if err := deps.GlobalConfigStore.Upsert(c.Request.Context(), item); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, toGlobalConfigItem(*item))
	})
	router.GET("/admin/scheduler/executions", func(c *gin.Context) {
		jobs, err := deps.JobConfigStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		items, err := deps.ExecutionStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		jobNameMap := make(map[string]string, len(jobs))
		jobScheduleTypeMap := make(map[string]string, len(jobs))
		for _, item := range jobs {
			jobNameMap[item.ID.Hex()] = item.Name
			jobScheduleTypeMap[item.ID.Hex()] = item.ScheduleType
		}

		responseItems := make([]executionItem, 0, len(items))
		for _, item := range items {
			jobID := item.JobConfigID.Hex()
			responseItems = append(responseItems, executionItem{
				ID:            objectIDToString(item.ID),
				JobName:       jobDisplayName(jobNameMap, item.JobConfigID),
				TriggerType:   item.TriggerType,
				ScheduleType:  jobScheduleTypeMap[jobID],
				Status:        item.Status,
				StartedAt:     formatTime(item.StartedAt),
				FinishedAt:    formatTime(item.FinishedAt),
				ResultSummary: item.ResultSummary,
			})
		}

		c.JSON(http.StatusOK, executionListResponse{
			List:  responseItems,
			Total: len(responseItems),
		})
	})
	router.GET("/admin/scheduler/executions/:id", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}

		item, err := deps.ExecutionStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if item == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "execution not found"})
			return
		}

		detail := toExecutionDetailItem(*item)
		job, err := deps.JobConfigStore.GetByID(c.Request.Context(), item.JobConfigID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if job != nil {
			detail.ScheduleType = job.ScheduleType
		}

		c.JSON(http.StatusOK, detail)
	})
	router.GET("/admin/scheduler/executions/:id/logfile", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}
		item, err := deps.ExecutionStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if item == nil || item.LogFile == "" {
			c.JSON(http.StatusNotFound, gin.H{"error": "log file not found"})
			return
		}
		filePath := filepath.Join("logs", "joblog", item.LogFile)
		if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) {
			c.JSON(http.StatusNotFound, gin.H{"error": "log file not found on disk"})
			return
		}
		c.FileAttachment(filePath, item.LogFile)
	})
	router.GET("/admin/account/profile", func(c *gin.Context) {
		profile, err := deps.ProfileStore.Get(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, toProfileItem(*profile))
	})
	router.PUT("/admin/account/profile", func(c *gin.Context) {
		current, err := deps.ProfileStore.Get(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		var req updateProfileRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		current.Account = req.Account
		current.Nickname = req.Nickname
		current.Avatar = req.Avatar
		current.Remark = req.Remark

		if err := deps.ProfileStore.Upsert(c.Request.Context(), current); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, toProfileItem(*current))
	})
	router.POST("/admin/account/change-password", func(c *gin.Context) {
		var req changeProfilePasswordRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if err := deps.ProfileStore.ChangePassword(c.Request.Context(), req.OldPassword, req.NewPassword); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"success": true})
	})
	router.GET("/admin/admin-users", func(c *gin.Context) {
		items, err := deps.AdminUserStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		result := make([]adminUserItem, 0, len(items))
		for _, item := range items {
			result = append(result, toAdminUserItem(item))
		}
		c.JSON(http.StatusOK, adminUserListResponse{List: result, Total: len(result)})
	})
	router.POST("/admin/admin-users", func(c *gin.Context) {
		var req upsertAdminUserRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		item := model.AdminUser{
			Account:      req.Account,
			Nickname:     req.Nickname,
			Status:       req.Status,
			Role:         req.Role,
			Remark:       req.Remark,
			PasswordHash: modelPasswordHash(req.Password),
		}
		if item.PasswordHash == "" {
			item.PasswordHash = modelPasswordHash("admin123")
		}
		if err := deps.AdminUserStore.Create(c.Request.Context(), &item); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusCreated, toAdminUserItem(item))
	})
	router.PUT("/admin/admin-users/:id", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}
		current, err := deps.AdminUserStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if current == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "admin user not found"})
			return
		}
		var req upsertAdminUserRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		current.Account = req.Account
		current.Nickname = req.Nickname
		current.Status = req.Status
		current.Role = req.Role
		current.Remark = req.Remark
		if err := deps.AdminUserStore.Update(c.Request.Context(), current); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, toAdminUserItem(*current))
	})
	router.POST("/admin/admin-users/:id/change-password", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}
		var req changeAdminPasswordRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if err := deps.AdminUserStore.ChangePassword(c.Request.Context(), id, req.NewPassword); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"success": true})
	})
	router.GET("/admin/system-configs", func(c *gin.Context) {
		items, err := deps.ConfigStore.List(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		result := make([]systemConfigItem, 0, len(items))
		for _, item := range items {
			result = append(result, toSystemConfigItem(item))
		}
		c.JSON(http.StatusOK, systemConfigListResponse{List: result, Total: len(result)})
	})
	router.POST("/admin/system-configs", func(c *gin.Context) {
		var req upsertSystemConfigRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		item := model.SystemConfig{
			Key:         req.Key,
			Title:       req.Title,
			Value:       req.Value,
			ValueType:   req.ValueType,
			Description: req.Description,
			Enabled:     req.Enabled,
		}
		if err := deps.ConfigStore.Create(c.Request.Context(), &item); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusCreated, toSystemConfigItem(item))
	})
	router.PUT("/admin/system-configs/:id", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}
		current, err := deps.ConfigStore.GetByID(c.Request.Context(), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if current == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "system config not found"})
			return
		}
		var req upsertSystemConfigRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		current.Key = req.Key
		current.Title = req.Title
		current.Value = req.Value
		current.ValueType = req.ValueType
		current.Description = req.Description
		current.Enabled = req.Enabled
		if err := deps.ConfigStore.Update(c.Request.Context(), current); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, toSystemConfigItem(*current))
	})
	router.POST("/admin/system-configs/:id/toggle", func(c *gin.Context) {
		id, err := primitive.ObjectIDFromHex(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
			return
		}
		var req toggleSystemConfigRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if err := deps.ConfigStore.ToggleEnabled(c.Request.Context(), id, req.Enabled); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"success": true})
	})

	return router
}

func toJobItem(item model.JobConfig) jobItem {
	return jobItem{
		ID:            objectIDToString(item.ID),
		Name:          item.Name,
		HandlerKey:    item.HandlerKey,
		Enabled:       item.Enabled,
		ScheduleType:  item.ScheduleType,
		ScheduleValue: item.ScheduleValue,
		DefaultParams: item.DefaultParams,
		LastStatus:    item.LastStatus,
		LastRunAt:     formatTime(item.LastRunAt),
		NextRunAt:     formatTime(item.NextRunAt),
	}
}

func toExecutionDetailItem(item model.JobExecution) executionDetailItem {
	return executionDetailItem{
		ID:             objectIDToString(item.ID),
		JobConfigID:    objectIDToString(item.JobConfigID),
		TriggerType:    item.TriggerType,
		Status:         item.Status,
		ParamsSnapshot: item.ParamsSnapshot,
		StartedAt:      formatTime(item.StartedAt),
		FinishedAt:     formatTime(item.FinishedAt),
		DurationMs:     item.DurationMs,
		ResultSummary:  item.ResultSummary,
		ErrorMessage:   item.ErrorMessage,
		LogText:        item.LogText,
		LogFile:        item.LogFile,
		CreatedAt:      item.CreatedAt.Format(time.RFC3339),
	}
}

func toGlobalConfigItem(item model.GlobalConfig) globalConfigItem {
	return globalConfigItem{
		Config:    item.Config,
		UpdatedBy: item.UpdatedBy,
		UpdatedAt: formatTime(&item.UpdatedAt),
	}
}

func toProfileItem(item model.AdminProfile) profileItem {
	return profileItem{
		ID:        objectIDToString(item.ID),
		Account:   item.Account,
		Nickname:  item.Nickname,
		Avatar:    item.Avatar,
		Remark:    item.Remark,
		CreatedAt: item.CreatedAt.Format(time.RFC3339),
		UpdatedAt: item.UpdatedAt.Format(time.RFC3339),
	}
}

func toAdminUserItem(item model.AdminUser) adminUserItem {
	return adminUserItem{
		ID:        objectIDToString(item.ID),
		Account:   item.Account,
		Nickname:  item.Nickname,
		Status:    item.Status,
		Role:      item.Role,
		Remark:    item.Remark,
		CreatedAt: item.CreatedAt.Format(time.RFC3339),
		UpdatedAt: item.UpdatedAt.Format(time.RFC3339),
	}
}

func toSystemConfigItem(item model.SystemConfig) systemConfigItem {
	return systemConfigItem{
		ID:          objectIDToString(item.ID),
		Key:         item.Key,
		Title:       item.Title,
		Value:       item.Value,
		ValueType:   item.ValueType,
		Description: item.Description,
		Enabled:     item.Enabled,
		CreatedAt:   item.CreatedAt.Format(time.RFC3339),
		UpdatedAt:   item.UpdatedAt.Format(time.RFC3339),
	}
}

func mergeExecutionParams(defaultParams string, customParams json.RawMessage) (json.RawMessage, error) {
	if len(customParams) == 0 {
		if defaultParams == "" {
			return nil, nil
		}
		if !json.Valid([]byte(defaultParams)) {
			return nil, errInvalidJSON("defaultParams")
		}
		return json.RawMessage(defaultParams), nil
	}

	if !json.Valid(customParams) {
		return nil, errInvalidJSON("params")
	}

	if defaultParams == "" {
		return customParams, nil
	}

	if !json.Valid([]byte(defaultParams)) {
		return nil, errInvalidJSON("defaultParams")
	}

	var base map[string]any
	var override map[string]any

	if err := json.Unmarshal([]byte(defaultParams), &base); err != nil {
		return nil, errInvalidJSON("defaultParams")
	}
	if err := json.Unmarshal(customParams, &override); err != nil {
		return nil, errInvalidJSON("params")
	}

	if base == nil || override == nil {
		return customParams, nil
	}

	for key, value := range override {
		base[key] = value
	}

	merged, err := json.Marshal(base)
	if err != nil {
		return nil, err
	}

	return merged, nil
}

func errInvalidJSON(field string) error {
	return fmt.Errorf("%s must be valid JSON", field)
}

func formatTime(value *time.Time) string {
	if value == nil {
		return ""
	}

	return value.Format(time.RFC3339)
}

func objectIDToString(value primitive.ObjectID) string {
	if value.IsZero() {
		return ""
	}

	return value.Hex()
}

func jobDisplayName(jobNameMap map[string]string, id primitive.ObjectID) string {
	if name := jobNameMap[id.Hex()]; name != "" {
		return name
	}
	return id.Hex()
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func modelPasswordHash(password string) string {
	if password == "" {
		return ""
	}
	sum := sha256.Sum256([]byte(password))
	return hex.EncodeToString(sum[:])
}
