// scheduler.go 实现任务分解的并行调度器（autonomy roadmap §10）。Tick 拓扑式地
// 选取"就绪"（未被阻塞、open、叶子、无活跃 session）的子任务，并把相互独立的若干个
// 扇出到独立 worktree 上的并行 ACP session。调度器是服务端组件，以特权 owner 身份
// 调用 acp.SessionService.Create，因而不受 checkNotSystemToken 限制。
//
// 调度器无状态、幂等：ListReadyLeafSubtasks 已排除有活跃 session 的 issue，故重复 tick
// 不会重复派单。并发硬上限由 acp 层 MaxActiveGlobal/MaxActivePerUser 兜底（命中时
// CodeAcpSessionQuotaExceeded 当作软失败，下一 tick 重试）；软上限由 MaxConcurrentPerProject
// 在调度器内预检。把 Scheduler.Tick 注册为 jobs RunnerFn（单实例、单 tick），即可跨重启存活。
package orchestrator

import (
	"context"
	"errors"
	"log/slog"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/project"
)

// ReadyTaskSource 是调度器读取就绪子任务的窄接口（app 层用 project.Repository 适配）。
type ReadyTaskSource interface {
	// ListSchedulableProjects 返回当前至少有一个就绪子任务的 project id（预筛）。
	ListSchedulableProjects(ctx context.Context) ([]uuid.UUID, error)
	// ListReadyLeafSubtasks 返回 project 下就绪叶子子任务（priority DESC, number ASC），最多 limit 条。
	ListReadyLeafSubtasks(ctx context.Context, projectID uuid.UUID, limit int) ([]*project.Issue, error)
	// CountActiveSessionsForProject 返回 project 内活跃 acp_session 数（软并发预检用）。
	CountActiveSessionsForProject(ctx context.Context, projectID uuid.UUID) (int, error)
}

// SessionStarter 为某 issue 启动一个 ACP session（包装 acp.SessionService.Create）。
// 实现以特权身份（项目 owner / 系统账户）调用，使 worktree Acquire 鉴权通过。
type SessionStarter interface {
	// StartForIssue 为 issue 创建一个绑定 issue/<slug>-N worktree 的 session，返回 session id。
	StartForIssue(ctx context.Context, iss *project.Issue) (uuid.UUID, error)
}

// SchedulerNotifier 是调度器投递 ready/dispatch/quota 通知的窄接口（app 用 notify.Dispatcher 适配）。
type SchedulerNotifier interface {
	NotifyDispatched(ctx context.Context, iss *project.Issue, sessionID uuid.UUID)
	NotifyQuotaBlocked(ctx context.Context, iss *project.Issue)
}

// SchedulerDeps 是调度器依赖集合。
type SchedulerDeps struct {
	Projects ReadyTaskSource
	Sessions SessionStarter
	Notify   SchedulerNotifier
	Log      *slog.Logger
	// MaxFanoutPerTick 是每个 project 每 tick 最多派发的 session 数（<=0 时默认 8）。
	MaxFanoutPerTick int
	// MaxConcurrentPerProject 是 project 内活跃 session 软上限（<=0 时不预检，仅靠 acp 层硬上限）。
	MaxConcurrentPerProject int
}

// Scheduler 是任务分解并行调度器。单一 Tick 方法便于解耦其宿主（jobs RunnerFn 或
// 未来的 pipeline host）。
type Scheduler interface {
	// Tick 执行一轮调度，返回本轮派发的 session 数。单个 issue 派发失败不致整轮失败。
	Tick(ctx context.Context) (dispatched int, err error)
	// Run 是 Tick 的 jobs.RunnerFunc 适配（func(ctx)）：记录日志、吞掉错误。
	Run(ctx context.Context)
}

type scheduler struct {
	projects                ReadyTaskSource
	sessions                SessionStarter
	notify                  SchedulerNotifier
	log                     *slog.Logger
	maxFanoutPerTick        int
	maxConcurrentPerProject int
}

// NewScheduler 构造调度器。
func NewScheduler(d SchedulerDeps) Scheduler {
	log := d.Log
	if log == nil {
		log = slog.Default()
	}
	fanout := d.MaxFanoutPerTick
	if fanout <= 0 {
		fanout = 8
	}
	return &scheduler{
		projects:                d.Projects,
		sessions:                d.Sessions,
		notify:                  d.Notify,
		log:                     log,
		maxFanoutPerTick:        fanout,
		maxConcurrentPerProject: d.MaxConcurrentPerProject,
	}
}

var _ Scheduler = (*scheduler)(nil)

// Run 把 Tick 适配为 jobs.RunnerFunc 签名（func(ctx)）：记录日志、吞掉错误。
func (s *scheduler) Run(ctx context.Context) {
	n, err := s.Tick(ctx)
	if err != nil {
		s.log.Warn("orchestrator.scheduler.tick_failed", "err", err.Error())
		return
	}
	if n > 0 {
		s.log.Info("orchestrator.scheduler.tick", "dispatched", n)
	}
}

func (s *scheduler) Tick(ctx context.Context) (int, error) {
	projects, err := s.projects.ListSchedulableProjects(ctx)
	if err != nil {
		return 0, err
	}
	total := 0
	for _, projID := range projects {
		n, perr := s.tickProject(ctx, projID)
		total += n
		if perr != nil {
			// 单个 project 失败不致整轮失败：记录并继续下一个 project。
			s.log.Warn("orchestrator.scheduler.project_failed", "project_id", projID.String(), "err", perr.Error())
			continue
		}
	}
	return total, nil
}

// tickProject 为单个 project 派发就绪子任务。受 MaxFanoutPerTick 与（可选）
// MaxConcurrentPerProject 软上限约束。命中 acp 配额（CodeAcpSessionQuotaExceeded）时
// 停止本 project（软失败，下一 tick 重试），不返回 error。
func (s *scheduler) tickProject(ctx context.Context, projectID uuid.UUID) (int, error) {
	limit := s.maxFanoutPerTick
	// 软并发预检：若已达 project 内活跃上限，本 tick 不派发。
	if s.maxConcurrentPerProject > 0 {
		active, err := s.projects.CountActiveSessionsForProject(ctx, projectID)
		if err != nil {
			return 0, err
		}
		budget := s.maxConcurrentPerProject - active
		if budget <= 0 {
			return 0, nil
		}
		if budget < limit {
			limit = budget
		}
	}

	ready, err := s.projects.ListReadyLeafSubtasks(ctx, projectID, limit)
	if err != nil {
		return 0, err
	}

	dispatched := 0
	for _, iss := range ready {
		sessionID, serr := s.sessions.StartForIssue(ctx, iss)
		if serr != nil {
			if isQuotaExceeded(serr) {
				// 软配额：停止本 project 的派发，下一 tick 重试。通知关注者。
				if s.notify != nil {
					s.notify.NotifyQuotaBlocked(ctx, iss)
				}
				return dispatched, nil
			}
			// 其它失败：记录并跳过该 issue，继续其余就绪 issue。
			s.log.Warn("orchestrator.scheduler.start_failed",
				"issue_id", iss.ID.String(), "number", iss.Number, "err", serr.Error())
			continue
		}
		dispatched++
		if s.notify != nil {
			s.notify.NotifyDispatched(ctx, iss, sessionID)
		}
	}
	return dispatched, nil
}

// isQuotaExceeded 报告 err 是否为 acp 会话配额超限（软失败语义）。
func isQuotaExceeded(err error) bool {
	var ae *errs.AppError
	if errors.As(err, &ae) {
		return ae.Code == errs.CodeAcpSessionQuotaExceeded
	}
	return false
}
