package automation

import (
	"context"
	"fmt"
	"strings"
	"time"
)

const (
	// Scan a bounded window so a busy workspace does not monopolize small
	// recovery batches without making pollers aggregate the full queue.
	automationQueueFairScanMultiplier = 16
	automationQueueFairScanMax        = 1000
)

// ListReadyWorkspaces returns a bounded set of workspaces with ready queue work.
func (s *Store) ListReadyWorkspaces(ctx context.Context, queueName string, now time.Time, limit int, workspaceScope ...[]string) ([]string, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	queueName = strings.TrimSpace(queueName)
	if queueName == "" {
		return nil, fmt.Errorf("automation ready workspace queue name is required")
	}
	if limit <= 0 {
		limit = 20
	}
	if limit > 100 {
		limit = 100
	}
	type readyWorkspace struct {
		WorkspaceID string
	}
	var rows []readyWorkspace
	scanLimit := automationQueueCandidateScanLimit(limit)
	query := s.db.WithContext(ctx).
		Model(&AutomationJob{}).
		Select("workspace_id").
		Where("status = ? AND queue_name = ? AND scheduled_at <= ?", AutomationJobStatusReady, queueName, now)
	query = applyWorkspaceScope(query, "workspace_id", workspaceScope)
	err := query.
		Order("scheduled_at ASC").
		Order("workspace_id ASC").
		Limit(scanLimit).
		Scan(&rows).Error
	if err != nil {
		return nil, fmt.Errorf("list automation ready workspaces: %w", err)
	}
	workspaces := make([]string, 0, len(rows))
	seen := make(map[string]struct{}, limit)
	for _, row := range rows {
		workspaceID := strings.TrimSpace(row.WorkspaceID)
		if workspaceID == "" {
			continue
		}
		if _, ok := seen[workspaceID]; ok {
			continue
		}
		seen[workspaceID] = struct{}{}
		workspaces = append(workspaces, workspaceID)
		if len(workspaces) >= limit {
			break
		}
	}
	return workspaces, nil
}

func (s *Store) expiredJobRecoveryCandidates(ctx context.Context, now time.Time, limit int, workspaceScope ...[]string) ([]AutomationJob, error) {
	var rows []AutomationJob
	query := s.db.WithContext(ctx).
		Where("status = ? AND lease_until IS NOT NULL AND lease_until < ?", AutomationJobStatusLeased, now).
		Select("id, workspace_id")
	query = applyWorkspaceScope(query, "workspace_id", workspaceScope)
	if err := query.
		Order("lease_until ASC").
		Order("workspace_id ASC").
		Order("id ASC").
		Limit(automationQueueCandidateScanLimit(limit)).
		Find(&rows).Error; err != nil {
		return nil, fmt.Errorf("list expired automation jobs: %w", err)
	}
	candidateLimit := min(limit, len(rows))
	candidates := make([]AutomationJob, 0, candidateLimit)
	deferred := make([]AutomationJob, 0)
	seenWorkspaces := make(map[string]struct{}, candidateLimit)
	for _, row := range rows {
		workspaceID := strings.TrimSpace(row.WorkspaceID)
		if workspaceID == "" {
			deferred = append(deferred, row)
			continue
		}
		if _, ok := seenWorkspaces[workspaceID]; ok {
			deferred = append(deferred, row)
			continue
		}
		seenWorkspaces[workspaceID] = struct{}{}
		candidates = append(candidates, row)
		if len(candidates) >= limit {
			return candidates, nil
		}
	}
	for _, row := range deferred {
		candidates = append(candidates, row)
		if len(candidates) >= limit {
			break
		}
	}
	return candidates, nil
}

func automationQueueCandidateScanLimit(limit int) int {
	scanLimit := limit * automationQueueFairScanMultiplier
	if scanLimit < limit {
		return limit
	}
	if scanLimit > automationQueueFairScanMax {
		return automationQueueFairScanMax
	}
	return scanLimit
}
