// Package scheduler runs configured cron tasks and delegates business work to handlers.
package scheduler

import (
	"context"
	"log/slog"
	"sort"
	"strings"
	"sync"
	"time"

	"hyapp/pkg/idgen"
	"hyapp/pkg/logx"
	"hyapp/services/cron-service/internal/config"
	mysqlstorage "hyapp/services/cron-service/internal/storage/mysql"
)

const (
	runStatusRunning   = "running"
	runStatusSucceeded = "succeeded"
	runStatusFailed    = "failed"
)

// Store is the cron metadata boundary used by scheduler loops.
type Store interface {
	TryAcquireTaskLease(ctx context.Context, taskName string, appCode string, ownerNodeID string, nowMs int64, leaseTTL time.Duration) (bool, error)
	StartTaskRun(ctx context.Context, run mysqlstorage.TaskRun) error
	FinishTaskRun(ctx context.Context, run mysqlstorage.TaskRun) error
}

// BatchRequest is the stable input passed to task handlers.
type BatchRequest struct {
	TaskName                string
	AppCode                 string
	RunID                   string
	WorkerID                string
	BatchSize               int
	LockTTL                 time.Duration
	PendingPublishMaxAge    time.Duration
	PublishingSessionMaxAge time.Duration
}

// BatchResult summarizes one owner-service batch response.
type BatchResult struct {
	ClaimedCount   int
	ProcessedCount int
	SuccessCount   int
	FailureCount   int
	HasMore        bool
}

// Handler executes one batch through an owner service client.
type Handler func(ctx context.Context, request BatchRequest) (BatchResult, error)

// Scheduler owns task loops. It never implements domain logic directly.
type Scheduler struct {
	nodeID   string
	tasks    map[string]config.TaskConfig
	store    Store
	handlers map[string]Handler
}

// New creates a scheduler with explicit handlers for tasks that are already migrated.
func New(nodeID string, tasks map[string]config.TaskConfig, store Store, handlers map[string]Handler) *Scheduler {
	return &Scheduler{
		nodeID:   strings.TrimSpace(nodeID),
		tasks:    tasks,
		store:    store,
		handlers: handlers,
	}
}

// Run starts one loop per enabled task and app_code until ctx is cancelled.
func (s *Scheduler) Run(ctx context.Context) {
	if s == nil {
		return
	}
	taskNames := make([]string, 0, len(s.tasks))
	for name := range s.tasks {
		taskNames = append(taskNames, name)
	}
	sort.Strings(taskNames)

	var wg sync.WaitGroup
	for _, taskName := range taskNames {
		task := s.tasks[taskName]
		if !task.Enabled {
			continue
		}
		handler := s.handlers[taskName]
		if handler == nil {
			// 配置可以先落地，但没有 owner-service RPC handler 时不能启动空跑循环。
			logx.Warn(ctx, "cron_task_handler_missing", slog.String("task_name", taskName))
			continue
		}
		for _, appCode := range task.AppCodes {
			taskName := taskName
			appCode := appCode
			task := task
			handler := handler
			wg.Go(func() {
				s.runTaskLoop(ctx, taskName, appCode, task, handler)
			})
		}
	}
	wg.Wait()
}

func (s *Scheduler) runTaskLoop(ctx context.Context, taskName string, appCode string, task config.TaskConfig, handler Handler) {
	interval := parseDuration(task.Interval, 5*time.Second)
	timer := time.NewTimer(0)
	defer timer.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-timer.C:
			hasMore := s.runOnce(ctx, taskName, appCode, task, handler)
			if hasMore {
				timer.Reset(0)
				continue
			}
			timer.Reset(interval)
		}
	}
}

func (s *Scheduler) runOnce(ctx context.Context, taskName string, appCode string, task config.TaskConfig, handler Handler) bool {
	if s.store == nil {
		logx.Error(ctx, "cron_store_missing", nil, slog.String("task_name", taskName), slog.String("app_code", appCode))
		return false
	}
	now := time.Now().UTC()
	lockTTL := parseDuration(task.LockTTL, 30*time.Second)
	acquired, err := s.store.TryAcquireTaskLease(ctx, taskName, appCode, s.nodeID, now.UnixMilli(), lockTTL)
	if err != nil {
		logx.Error(ctx, "cron_task_lease_failed", err, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("worker_id", s.nodeID))
		return false
	}
	if !acquired {
		return false
	}

	runID := idgen.New("cronrun")
	run := mysqlstorage.TaskRun{
		RunID:       runID,
		TaskName:    taskName,
		AppCode:     appCode,
		OwnerNodeID: s.nodeID,
		Status:      runStatusRunning,
		StartedAtMs: now.UnixMilli(),
	}
	if err := s.store.StartTaskRun(ctx, run); err != nil {
		logx.Error(ctx, "cron_task_run_start_failed", err, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID))
		return false
	}

	timeout := parseDuration(task.Timeout, 5*time.Second)
	taskCtx, cancel := context.WithTimeout(ctx, timeout)
	result, handlerErr := handler(taskCtx, BatchRequest{
		TaskName:                taskName,
		AppCode:                 appCode,
		RunID:                   runID,
		WorkerID:                s.nodeID,
		BatchSize:               task.BatchSize,
		LockTTL:                 lockTTL,
		PendingPublishMaxAge:    parseDuration(task.PendingPublishMaxAge, 0),
		PublishingSessionMaxAge: parseDuration(task.PublishingSessionMaxAge, 0),
	})
	cancel()

	finished := time.Now().UTC()
	run.FinishedAtMs = finished.UnixMilli()
	run.DurationMs = finished.Sub(now).Milliseconds()
	run.ClaimedCount = result.ClaimedCount
	run.ProcessedCount = result.ProcessedCount
	run.SuccessCount = result.SuccessCount
	run.FailureCount = result.FailureCount
	run.Status = runStatusSucceeded
	if handlerErr != nil {
		run.Status = runStatusFailed
		run.ErrorMessage = handlerErr.Error()
	}
	if err := s.store.FinishTaskRun(context.Background(), run); err != nil {
		logx.Error(ctx, "cron_task_run_finish_failed", err, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID))
		return false
	}
	if handlerErr != nil {
		logx.Error(ctx, "cron_task_run_failed", handlerErr, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID), slog.Int("processed_count", run.ProcessedCount), slog.Int64("duration_ms", run.DurationMs))
		return false
	}
	logx.Info(ctx, "cron_task_run_succeeded", slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID), slog.Int("processed_count", run.ProcessedCount), slog.Int("claimed_count", run.ClaimedCount), slog.Bool("has_more", result.HasMore), slog.Int64("duration_ms", run.DurationMs))
	return result.HasMore
}

func parseDuration(raw string, fallback time.Duration) time.Duration {
	duration, err := time.ParseDuration(strings.TrimSpace(raw))
	if err != nil || duration <= 0 {
		return fallback
	}
	return duration
}
