package automation

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

	"gorm.io/gorm"
)

// RepairLeakedActiveRunSlots releases capacity slots whose run is missing or
// already terminal. It deliberately does not synthesize slots for active runs.
func (s *Store) RepairLeakedActiveRunSlots(ctx context.Context, limit int, workspaceScope ...[]string) (int64, error) {
	if err := s.ensureDB(); err != nil {
		return 0, err
	}
	if limit <= 0 {
		limit = 100
	}
	type leakedSlot struct {
		WorkspaceID string
		SlotIndex   int
		RunID       string
	}
	var candidates []leakedSlot
	query := s.db.WithContext(ctx).
		Table("automation_run_slots AS slots").
		Select("slots.workspace_id AS workspace_id, slots.slot_index AS slot_index, slots.run_id AS run_id").
		Joins("LEFT JOIN automation_runs AS runs ON runs.workspace_id = slots.workspace_id AND runs.id = slots.run_id").
		Where("runs.id IS NULL OR runs.status IN ?", []string{AutomationRunStatusSucceeded, AutomationRunStatusFailed, AutomationRunStatusCanceled})
	query = applyWorkspaceScope(query, "slots.workspace_id", workspaceScope)
	if err := query.
		Order("slots.updated_at ASC").
		Order("slots.workspace_id ASC").
		Order("slots.slot_index ASC").
		Limit(limit).
		Scan(&candidates).Error; err != nil {
		return 0, fmt.Errorf("list leaked automation active run slots: %w", err)
	}
	if len(candidates) == 0 {
		return 0, nil
	}
	keys := make([][]any, 0, len(candidates))
	for _, candidate := range candidates {
		keys = append(keys, []any{candidate.WorkspaceID, candidate.SlotIndex, candidate.RunID})
	}
	result := s.db.WithContext(ctx).
		Where("(workspace_id, slot_index, run_id) IN ?", keys).
		Delete(&AutomationRunSlot{})
	if result.Error != nil {
		return 0, fmt.Errorf("repair leaked automation active run slot: %w", result.Error)
	}
	return result.RowsAffected, nil
}

func (s *Store) countActiveRunSlots(ctx context.Context, workspaceID string) (int64, error) {
	query := s.db.WithContext(ctx).Model(&AutomationRunSlot{})
	if workspaceID = strings.TrimSpace(workspaceID); workspaceID != "" {
		query = query.Where("workspace_id = ?", workspaceID)
	}
	var count int64
	if err := query.Count(&count).Error; err != nil {
		return 0, fmt.Errorf("count automation active run slots: %w", err)
	}
	return count, nil
}

func (s *Store) countLeakedActiveRunSlots(ctx context.Context, workspaceID string) (int64, error) {
	query := s.db.WithContext(ctx).
		Table("automation_run_slots AS slots").
		Joins("LEFT JOIN automation_runs AS runs ON runs.workspace_id = slots.workspace_id AND runs.id = slots.run_id").
		Where("runs.id IS NULL OR runs.status IN ?", []string{AutomationRunStatusSucceeded, AutomationRunStatusFailed, AutomationRunStatusCanceled})
	if workspaceID = strings.TrimSpace(workspaceID); workspaceID != "" {
		query = query.Where("slots.workspace_id = ?", workspaceID)
	}
	var count int64
	if err := query.Count(&count).Error; err != nil {
		return 0, fmt.Errorf("count leaked automation active run slots: %w", err)
	}
	return count, nil
}

func reserveActiveRunSlotTx(tx *gorm.DB, workspaceID string, runID string, limit int, now time.Time) error {
	if limit <= 0 {
		return nil
	}
	workspaceID = strings.TrimSpace(workspaceID)
	runID = strings.TrimSpace(runID)
	if workspaceID == "" || runID == "" {
		return fmt.Errorf("automation active run slot requires workspace and run id")
	}
	if now.IsZero() {
		now = time.Now().UTC()
	}
	var activeSlots int64
	if err := tx.Model(&AutomationRunSlot{}).Where("workspace_id = ?", workspaceID).Count(&activeSlots).Error; err != nil {
		return fmt.Errorf("count automation active run slots: %w", err)
	}
	if activeSlots >= int64(limit) {
		return ErrActiveRunCapacity
	}
	remaining := limit - int(activeSlots)
	var occupiedSlotIndexes []int
	if err := tx.Model(&AutomationRunSlot{}).
		Where("workspace_id = ? AND slot_index >= ? AND slot_index < ?", workspaceID, 0, limit).
		Pluck("slot_index", &occupiedSlotIndexes).Error; err != nil {
		return fmt.Errorf("load automation active run slots: %w", err)
	}
	occupiedSlots := make(map[int]struct{}, len(occupiedSlotIndexes))
	for _, slot := range occupiedSlotIndexes {
		occupiedSlots[slot] = struct{}{}
	}
	// Out-of-range slots still consume capacity, so only try the number of
	// in-range candidates that the count says remain available.
	attempts := 0
	for slot := 0; slot < limit; slot++ {
		if _, ok := occupiedSlots[slot]; ok {
			continue
		}
		attempts++
		err := tx.Create(&AutomationRunSlot{WorkspaceID: workspaceID, SlotIndex: slot, RunID: runID, CreatedAt: now, UpdatedAt: now}).Error
		if err == nil {
			return nil
		}
		if !isUniqueConstraintError(err) {
			return fmt.Errorf("reserve automation active run slot: %w", err)
		}
		if attempts >= remaining {
			break
		}
	}
	return ErrActiveRunCapacity
}

func releaseActiveRunSlotTx(tx *gorm.DB, workspaceID string, runID string) error {
	if err := tx.Where("workspace_id = ? AND run_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(runID)).Delete(&AutomationRunSlot{}).Error; err != nil {
		return fmt.Errorf("release automation active run slot: %w", err)
	}
	return nil
}

func isUniqueConstraintError(err error) bool {
	if err == nil {
		return false
	}
	message := strings.ToLower(err.Error())
	return strings.Contains(message, "duplicate") ||
		strings.Contains(message, "duplicated") ||
		strings.Contains(message, "unique constraint") ||
		strings.Contains(message, "constraint failed")
}
