package automation

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"gorm.io/gorm"
)

func (s *Store) RecoverExpiredJobs(ctx context.Context, now time.Time, limit int, workspaceScope ...[]string) (int64, error) {
	if err := s.ensureDB(); err != nil {
		return 0, err
	}
	if limit <= 0 {
		limit = 100
	}
	candidates, err := s.expiredJobRecoveryCandidates(ctx, now, limit, workspaceScope...)
	if err != nil {
		return 0, err
	}
	var changed int64
	for _, candidate := range candidates {
		recovered, err := s.recoverExpiredJob(ctx, candidate, now)
		if err != nil {
			return changed, err
		}
		if recovered {
			changed++
		}
	}
	return changed, nil
}

func (s *Store) ReconcileTerminalRuns(ctx context.Context, now time.Time, limit int, workspaceScope ...[]string) (int64, error) {
	if err := s.ensureDB(); err != nil {
		return 0, err
	}
	if limit <= 0 {
		limit = 100
	}
	var candidates []AutomationRun
	query := s.db.WithContext(ctx).
		Where("status IN ?", activeAutomationRunStatuses()).
		Where("EXISTS (SELECT 1 FROM automation_job_runs WHERE automation_job_runs.workspace_id = automation_runs.workspace_id AND automation_job_runs.run_id = automation_runs.id)").
		Where("NOT EXISTS (SELECT 1 FROM automation_job_runs WHERE automation_job_runs.workspace_id = automation_runs.workspace_id AND automation_job_runs.run_id = automation_runs.id AND automation_job_runs.status NOT IN ?)", []string{AutomationJobRunStatusBlocked, AutomationJobRunStatusFailed, AutomationJobRunStatusSucceeded, AutomationJobRunStatusSkipped, AutomationJobRunStatusCanceled})
	query = applyWorkspaceScope(query, "automation_runs.workspace_id", workspaceScope)
	if err := query.
		Order("updated_at ASC").
		Limit(limit).
		Find(&candidates).Error; err != nil {
		return 0, fmt.Errorf("list active automation runs for terminal reconciliation: %w", err)
	}
	var changed int64
	for _, candidate := range candidates {
		reconciled, err := s.reconcileTerminalRun(ctx, candidate, now)
		if err != nil {
			return changed, err
		}
		if reconciled {
			changed++
		}
	}
	return changed, nil
}

func (s *Store) ReconcileBlockedDependencies(ctx context.Context, now time.Time, limit int, workspaceScope ...[]string) (int64, error) {
	if err := s.ensureDB(); err != nil {
		return 0, err
	}
	if limit <= 0 {
		limit = 100
	}
	var candidates []automationRunKey
	query := s.db.WithContext(ctx).Table("automation_job_runs").
		Select("automation_job_runs.workspace_id AS workspace_id, automation_job_runs.run_id AS run_id").
		Joins("JOIN automation_runs ON automation_runs.workspace_id = automation_job_runs.workspace_id AND automation_runs.id = automation_job_runs.run_id").
		Where("automation_job_runs.status IN ?", nonTerminalAutomationJobRunStatuses()).
		Where("automation_runs.status IN ?", append(activeAutomationRunStatuses(), AutomationRunStatusFailed)).
		Where("EXISTS (SELECT 1 FROM automation_job_runs failed WHERE failed.workspace_id = automation_job_runs.workspace_id AND failed.run_id = automation_job_runs.run_id AND failed.status IN ?)", []string{AutomationJobRunStatusFailed, AutomationJobRunStatusBlocked})
	query = applyWorkspaceScope(query, "automation_job_runs.workspace_id", workspaceScope)
	if err := query.
		Order("automation_job_runs.updated_at ASC").
		Limit(limit).
		Scan(&candidates).Error; err != nil {
		return 0, fmt.Errorf("list automation runs with failed dependencies: %w", err)
	}
	seen := make(map[automationRunKey]struct{}, len(candidates))
	var changed int64
	for _, candidate := range candidates {
		if _, ok := seen[candidate]; ok {
			continue
		}
		seen[candidate] = struct{}{}
		reconciled, err := s.failRunAfterJobFailure(ctx, candidate.WorkspaceID, candidate.RunID, "", automationFailureMetadataJSON("automation dependency failed", nil), now)
		if err != nil {
			return changed, err
		}
		if reconciled {
			changed++
		}
	}
	return changed, nil
}

func (s *Store) recoverExpiredJob(ctx context.Context, candidate AutomationJob, now time.Time) (bool, error) {
	recovered := false
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var job AutomationJob
		if err := tx.Where("id = ? AND status = ? AND lease_until IS NOT NULL AND lease_until < ?", candidate.ID, AutomationJobStatusLeased, now).First(&job).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil
			}
			return fmt.Errorf("lock expired automation job: %w", err)
		}
		var jobRun AutomationJobRun
		if err := tx.Where("workspace_id = ? AND id = ?", job.WorkspaceID, job.JobRunID).First(&jobRun).Error; err != nil {
			return fmt.Errorf("get expired automation job run: %w", err)
		}

		if job.Attempt >= job.MaxAttempts {
			errorJSON := automationJSON(map[string]any{"message": "automation job lease expired", "lease_owner": job.LeaseOwner, "attempt": job.Attempt})
			result := tx.Model(&AutomationJob{}).Where("id = ? AND status = ?", job.ID, AutomationJobStatusLeased).Updates(map[string]any{
				"status":      AutomationJobStatusFailed,
				"lease_owner": "",
				"lease_until": nil,
				"updated_at":  now,
			})
			if result.Error != nil {
				return fmt.Errorf("fail expired automation job: %w", result.Error)
			}
			if result.RowsAffected == 0 {
				return nil
			}
			jobRun.Status = AutomationJobRunStatusFailed
			jobRun.Attempt = job.Attempt
			jobRun.LeaseOwner = ""
			jobRun.LeaseUntil = nil
			jobRun.ErrorJSON = errorJSON
			jobRun.CompletedAt = &now
			jobRun.UpdatedAt = now
			if err := tx.Save(&jobRun).Error; err != nil {
				return fmt.Errorf("fail expired automation job run: %w", err)
			}
			event := automationEvent("job_failed", jobRun, "Automation job lease expired", errorJSON, now)
			if err := tx.Create(&event).Error; err != nil {
				return fmt.Errorf("create expired job failure event: %w", err)
			}
			if _, err := s.failRunAfterJobFailureTx(tx, job.WorkspaceID, job.RunID, jobRun.ID, errorJSON, now); err != nil {
				return err
			}
			recovered = true
			return nil
		}

		scheduledAt := now.Add(RetryBackoff(job.Attempt))
		result := tx.Model(&AutomationJob{}).Where("id = ? AND status = ?", job.ID, AutomationJobStatusLeased).Updates(map[string]any{
			"status":       AutomationJobStatusReady,
			"lease_owner":  "",
			"lease_until":  nil,
			"scheduled_at": scheduledAt,
			"updated_at":   now,
		})
		if result.Error != nil {
			return fmt.Errorf("requeue expired automation job: %w", result.Error)
		}
		if result.RowsAffected == 0 {
			return nil
		}
		jobRun.Status = AutomationJobRunStatusQueued
		jobRun.Attempt = job.Attempt
		jobRun.LeaseOwner = ""
		jobRun.LeaseUntil = nil
		jobRun.ScheduledAt = scheduledAt
		jobRun.UpdatedAt = now
		if err := tx.Save(&jobRun).Error; err != nil {
			return fmt.Errorf("requeue expired automation job run: %w", err)
		}
		event := automationEvent("job_retry_scheduled", jobRun, "Automation job lease expired; retry scheduled", automationJSON(map[string]any{"attempt": job.Attempt, "scheduled_at": scheduledAt}), now)
		if err := tx.Create(&event).Error; err != nil {
			return fmt.Errorf("create expired job retry event: %w", err)
		}
		recovered = true
		return nil
	})
	return recovered, err
}

func (s *Store) countTerminalInconsistentRuns(ctx context.Context, workspaceID string) (int64, error) {
	query := s.db.WithContext(ctx).Model(&AutomationRun{}).
		Where("status IN ?", activeAutomationRunStatuses()).
		Where("EXISTS (SELECT 1 FROM automation_job_runs WHERE automation_job_runs.workspace_id = automation_runs.workspace_id AND automation_job_runs.run_id = automation_runs.id)").
		Where("NOT EXISTS (SELECT 1 FROM automation_job_runs WHERE automation_job_runs.workspace_id = automation_runs.workspace_id AND automation_job_runs.run_id = automation_runs.id AND automation_job_runs.status NOT IN ?)", terminalAutomationJobRunStatuses())
	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 active automation runs with terminal jobs for dashboard metrics: %w", err)
	}
	return count, nil
}

func (s *Store) failRunAfterJobFailure(ctx context.Context, workspaceID string, runID string, failedJobRunID string, metadataJSON string, now time.Time) (bool, error) {
	changed := false
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		reconciled, err := s.failRunAfterJobFailureTx(tx, workspaceID, runID, failedJobRunID, metadataJSON, now)
		if err != nil {
			return err
		}
		changed = reconciled
		return nil
	})
	return changed, err
}

func (s *Store) failRunAfterJobFailureTx(tx *gorm.DB, workspaceID string, runID string, failedJobRunID string, metadataJSON string, now time.Time) (bool, error) {
	var run AutomationRun
	if err := tx.Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(runID)).First(&run).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil
		}
		return false, fmt.Errorf("get automation run after job failure: %w", err)
	}
	if run.Status == AutomationRunStatusSucceeded || run.Status == AutomationRunStatusCanceled {
		return false, nil
	}

	var jobRuns []AutomationJobRun
	if err := tx.Where("workspace_id = ? AND run_id = ?", run.WorkspaceID, run.ID).Find(&jobRuns).Error; err != nil {
		return false, fmt.Errorf("list automation job runs after failure: %w", err)
	}
	blockedIDs := dependentBlockedJobRunIDs(jobRuns, failedJobRunID)
	blockedSet := stringSet(blockedIDs)

	var affectedIDs []string
	var blockedEvents []AutomationEvent
	changed := false
	for _, jobRun := range jobRuns {
		if isTerminalJobRunStatus(jobRun.Status) {
			continue
		}
		updates := map[string]any{
			"error_json":   metadataJSON,
			"lease_owner":  "",
			"lease_until":  nil,
			"completed_at": now,
			"updated_at":   now,
		}
		if _, blocked := blockedSet[jobRun.ID]; blocked {
			updates["status"] = AutomationJobRunStatusBlocked
		} else {
			updates["status"] = AutomationJobRunStatusCanceled
		}
		result := tx.Model(&AutomationJobRun{}).Where("workspace_id = ? AND id = ? AND status NOT IN ?", jobRun.WorkspaceID, jobRun.ID, terminalAutomationJobRunStatuses()).Updates(updates)
		if result.Error != nil {
			return false, fmt.Errorf("settle automation job run after failure: %w", result.Error)
		}
		if result.RowsAffected > 0 {
			changed = true
			affectedIDs = append(affectedIDs, jobRun.ID)
			if _, blocked := blockedSet[jobRun.ID]; blocked {
				blockedEvents = append(blockedEvents, automationEvent("job_blocked", AutomationJobRun{ID: jobRun.ID, WorkspaceID: jobRun.WorkspaceID, RunID: jobRun.RunID}, "Automation job blocked by failed dependency", metadataJSON, now))
			}
		}
	}
	if len(affectedIDs) > 0 {
		if err := tx.Model(&AutomationJob{}).Where("workspace_id = ? AND run_id = ? AND job_run_id IN ? AND status IN ?", run.WorkspaceID, run.ID, affectedIDs, []string{AutomationJobStatusReady, AutomationJobStatusLeased}).Updates(map[string]any{
			"status":     AutomationJobStatusCanceled,
			"updated_at": now,
		}).Error; err != nil {
			return false, fmt.Errorf("cancel automation job assignments after failure: %w", err)
		}
		if err := tx.Model(&AutomationWait{}).Where("workspace_id = ? AND run_id = ? AND job_run_id IN ? AND status = ?", run.WorkspaceID, run.ID, affectedIDs, AutomationWaitStatusActive).Updates(map[string]any{
			"status":      AutomationWaitStatusCanceled,
			"active_key":  nil,
			"result_json": metadataJSON,
			"updated_at":  now,
			"canceled_at": now,
		}).Error; err != nil {
			return false, fmt.Errorf("cancel automation waits after failure: %w", err)
		}
	}
	if len(blockedEvents) > 0 {
		if err := tx.Create(&blockedEvents).Error; err != nil {
			return false, fmt.Errorf("create automation blocked job events: %w", err)
		}
	}
	if !isTerminalRunStatus(run.Status) {
		result := tx.Model(&AutomationRun{}).Where("workspace_id = ? AND id = ? AND status IN ?", run.WorkspaceID, run.ID, activeAutomationRunStatuses()).Updates(map[string]any{
			"status":       AutomationRunStatusFailed,
			"error_json":   metadataJSON,
			"completed_at": now,
			"updated_at":   now,
		})
		if result.Error != nil {
			return false, fmt.Errorf("fail automation run after job failure: %w", result.Error)
		}
		if result.RowsAffected == 0 {
			return changed, nil
		}
		if err := releaseActiveRunSlotTx(tx, run.WorkspaceID, run.ID); err != nil {
			return false, err
		}
		run.Status = AutomationRunStatusFailed
		run.ErrorJSON = metadataJSON
		run.CompletedAt = &now
		run.UpdatedAt = now
		if err := enqueueAutomationRepoStatusTerminalTx(tx, run, now); err != nil {
			return false, err
		}
		event := AutomationEvent{ID: automationEventID("run_failed", run.ID, now), WorkspaceID: run.WorkspaceID, RunID: run.ID, EventType: "run_failed", Summary: "Automation run failed", MetadataJSON: metadataJSON, CreatedAt: now}
		if err := tx.Create(&event).Error; err != nil {
			return false, fmt.Errorf("create automation run failure event: %w", err)
		}
		changed = true
	}
	return changed, nil
}

func dependentBlockedJobRunIDs(jobRuns []AutomationJobRun, failedJobRunID string) []string {
	blockers := map[string]struct{}{}
	for _, jobRun := range jobRuns {
		if jobRun.Status == AutomationJobRunStatusFailed || jobRun.Status == AutomationJobRunStatusBlocked || jobRun.ID == strings.TrimSpace(failedJobRunID) {
			blockers[jobRun.ID] = struct{}{}
		}
	}
	blocked := map[string]struct{}{}
	for {
		changed := false
		for _, jobRun := range jobRuns {
			if isTerminalJobRunStatus(jobRun.Status) {
				continue
			}
			if _, ok := blockers[jobRun.ID]; ok {
				continue
			}
			for _, depID := range decodeJobRunDependencies(jobRun.DependsOnJSON) {
				if _, blockedDep := blockers[depID]; blockedDep {
					blockers[jobRun.ID] = struct{}{}
					blocked[jobRun.ID] = struct{}{}
					changed = true
					break
				}
			}
		}
		if !changed {
			break
		}
	}
	ids := make([]string, 0, len(blocked))
	for id := range blocked {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	return ids
}

func (s *Store) reconcileTerminalRun(ctx context.Context, candidate AutomationRun, now time.Time) (bool, error) {
	reconciled := false
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var run AutomationRun
		if err := tx.Where("workspace_id = ? AND id = ? AND status IN ?", candidate.WorkspaceID, candidate.ID, activeAutomationRunStatuses()).First(&run).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil
			}
			return fmt.Errorf("lock active automation run for terminal reconciliation: %w", err)
		}
		changed, err := s.reconcileTerminalRunTx(tx, run, now)
		if err != nil {
			return err
		}
		reconciled = changed
		return nil
	})
	return reconciled, err
}

func (s *Store) reconcileTerminalRunTx(tx *gorm.DB, run AutomationRun, now time.Time) (bool, error) {
	var jobRuns []AutomationJobRun
	if err := tx.Where("workspace_id = ? AND run_id = ?", run.WorkspaceID, run.ID).Find(&jobRuns).Error; err != nil {
		return false, fmt.Errorf("list automation job runs for terminal reconciliation: %w", err)
	}
	summary := summarizeJobRunStatuses(jobRuns)
	if summary.Total == 0 || summary.Terminal != summary.Total {
		return false, nil
	}
	status := AutomationRunStatusSucceeded
	message := "Automation run reconciled after all jobs reached terminal state"
	if summary.Failed > 0 || summary.Blocked > 0 {
		status = AutomationRunStatusFailed
		message = "Automation run failed after terminal job reconciliation"
	} else if summary.Canceled > 0 {
		status = AutomationRunStatusCanceled
		message = "Automation run canceled after terminal job reconciliation"
	}
	metadataJSON := automationJSON(map[string]any{
		"message":         message,
		"total_jobs":      summary.Total,
		"succeeded_jobs":  summary.Succeeded,
		"failed_jobs":     summary.Failed,
		"blocked_jobs":    summary.Blocked,
		"skipped_jobs":    summary.Skipped,
		"canceled_jobs":   summary.Canceled,
		"previous_status": run.Status,
		"reconciled_at":   now,
	})
	updates := map[string]any{
		"status":       status,
		"completed_at": now,
		"updated_at":   now,
	}
	if status == AutomationRunStatusFailed || status == AutomationRunStatusCanceled {
		updates["error_json"] = metadataJSON
	}
	result := tx.Model(&AutomationRun{}).Where("workspace_id = ? AND id = ? AND status IN ?", run.WorkspaceID, run.ID, activeAutomationRunStatuses()).Updates(updates)
	if result.Error != nil {
		return false, fmt.Errorf("reconcile terminal automation run: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		return false, nil
	}
	if err := releaseActiveRunSlotTx(tx, run.WorkspaceID, run.ID); err != nil {
		return false, err
	}
	run.Status = status
	run.CompletedAt = &now
	run.UpdatedAt = now
	if errorJSON, ok := updates["error_json"].(string); ok {
		run.ErrorJSON = errorJSON
	}
	if err := enqueueAutomationRepoStatusTerminalTx(tx, run, now); err != nil {
		return false, err
	}
	eventType := "run_reconciled"
	if status == AutomationRunStatusFailed {
		eventType = "run_failed"
	} else if status == AutomationRunStatusCanceled {
		eventType = "run_canceled"
	}
	event := AutomationEvent{
		ID:           automationEventID(eventType, run.ID, now),
		WorkspaceID:  run.WorkspaceID,
		RunID:        run.ID,
		EventType:    eventType,
		Summary:      message,
		MetadataJSON: metadataJSON,
		CreatedAt:    now,
	}
	if err := tx.Create(&event).Error; err != nil {
		return false, fmt.Errorf("create terminal run reconciliation event: %w", err)
	}
	return true, nil
}
