package automation

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"gorm.io/gorm"
)

// AutomationWaitRun pairs a wait with its owning run for authorization and
// scheduler delivery paths that need both records.
type AutomationWaitRun struct {
	Wait AutomationWait
	Run  AutomationRun
}

// AutomationWaitSignalCursor is a stable cursor for signal-matched wait scans.
type AutomationWaitSignalCursor struct {
	UpdatedAt time.Time
	ID        string
}

type AutomationWaitSignal struct {
	WaitKind     string
	EventType    string
	ResourceType string
	ResourceID   string
}

// AutomationWaitSignalKey returns the compact storage key for one wait signal
// identity. It is deterministic and scoped to the workspace plus signal fields.
func AutomationWaitSignalKey(workspaceID string, signal AutomationWaitSignal) string {
	parts := []string{
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(signal.WaitKind),
		strings.TrimSpace(signal.EventType),
		strings.TrimSpace(signal.ResourceType),
		strings.TrimSpace(signal.ResourceID),
	}
	for _, part := range parts {
		if part == "" {
			return ""
		}
	}
	sum := sha256.Sum256([]byte(strings.Join(parts, "\x1f")))
	return hex.EncodeToString(sum[:])
}

func (s *Store) CreateWait(ctx context.Context, wait AutomationWait, jobRun AutomationJobRun, event AutomationEvent) (AutomationWait, error) {
	if err := s.ensureDB(); err != nil {
		return AutomationWait{}, err
	}
	normalizeAutomationWaitSignalKey(&wait)
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if wait.ActiveKey != nil && strings.TrimSpace(*wait.ActiveKey) != "" {
			var existing AutomationWait
			err := tx.Where("active_key = ?", strings.TrimSpace(*wait.ActiveKey)).First(&existing).Error
			if err == nil {
				wait = existing
				return nil
			}
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}
		}
		if err := tx.Save(&jobRun).Error; err != nil {
			return fmt.Errorf("mark automation job run waiting: %w", err)
		}
		if err := tx.Create(&wait).Error; err != nil {
			return fmt.Errorf("create automation wait: %w", err)
		}
		if err := tx.Create(&event).Error; err != nil {
			return fmt.Errorf("create automation wait event: %w", err)
		}
		if err := tx.Model(&AutomationRun{}).Where("workspace_id = ? AND id = ? AND status NOT IN ?", wait.WorkspaceID, wait.RunID, []string{AutomationRunStatusSucceeded, AutomationRunStatusFailed, AutomationRunStatusCanceled}).Updates(map[string]any{
			"status":     AutomationRunStatusWaiting,
			"updated_at": wait.UpdatedAt,
		}).Error; err != nil {
			return fmt.Errorf("mark automation run waiting: %w", err)
		}
		return nil
	})
	if err != nil {
		return AutomationWait{}, err
	}
	return wait, nil
}

func normalizeAutomationWaitSignalKey(wait *AutomationWait) {
	if wait == nil {
		return
	}
	wait.SignalKey = AutomationWaitSignalKey(wait.WorkspaceID, AutomationWaitSignal{
		WaitKind:     wait.WaitKind,
		EventType:    wait.ResumeEventType,
		ResourceType: wait.ResourceType,
		ResourceID:   wait.ResourceID,
	})
}

func (s *Store) GetWait(ctx context.Context, workspaceID string, waitID string) (AutomationWait, error) {
	if err := s.ensureDB(); err != nil {
		return AutomationWait{}, err
	}
	var wait AutomationWait
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(waitID)).First(&wait).Error
	if err != nil {
		return AutomationWait{}, err
	}
	return wait, nil
}

func normalizeAutomationWaitSignalQuery(workspaceID string, signal AutomationWaitSignal) (string, AutomationWaitSignal, string) {
	workspaceID = strings.TrimSpace(workspaceID)
	signal.WaitKind = strings.TrimSpace(signal.WaitKind)
	signal.EventType = strings.TrimSpace(signal.EventType)
	signal.ResourceType = strings.TrimSpace(signal.ResourceType)
	signal.ResourceID = strings.TrimSpace(signal.ResourceID)
	return workspaceID, signal, AutomationWaitSignalKey(workspaceID, signal)
}

func (s *Store) ListActiveWaitsBySignal(ctx context.Context, workspaceID string, signal AutomationWaitSignal) ([]AutomationWait, error) {
	return s.listWaitsBySignalPage(ctx, workspaceID, signal, []string{AutomationWaitStatusActive}, AutomationWaitSignalCursor{}, 0)
}

// ListActiveWaitRunsBySignal returns active signal-matched waits with their
// owning runs hydrated in one batch.
func (s *Store) ListActiveWaitRunsBySignal(ctx context.Context, workspaceID string, signal AutomationWaitSignal, cursor AutomationWaitSignalCursor, limit int) ([]AutomationWaitRun, error) {
	waits, err := s.listWaitsBySignalPage(ctx, workspaceID, signal, []string{AutomationWaitStatusActive}, cursor, limit)
	if err != nil {
		return nil, err
	}
	return s.automationWaitRuns(ctx, strings.TrimSpace(workspaceID), waits)
}

// ListResolvedWaitsBySignalNeedingSchedulerReplay returns resolved waits for an
// exact signal only when their run still has scheduler-recoverable job runs or
// queued job runs that are missing their queue row.
func (s *Store) ListResolvedWaitsBySignalNeedingSchedulerReplay(ctx context.Context, workspaceID string, signal AutomationWaitSignal) ([]AutomationWait, error) {
	return s.listResolvedWaitsBySignalNeedingSchedulerReplayPage(ctx, workspaceID, signal, AutomationWaitSignalCursor{}, 0)
}

// ListResolvedWaitRunsBySignalNeedingSchedulerReplay returns scheduler-replay
// signal waits with their owning runs hydrated in one batch.
func (s *Store) ListResolvedWaitRunsBySignalNeedingSchedulerReplay(ctx context.Context, workspaceID string, signal AutomationWaitSignal, cursor AutomationWaitSignalCursor, limit int) ([]AutomationWaitRun, error) {
	waits, err := s.listResolvedWaitsBySignalNeedingSchedulerReplayPage(ctx, workspaceID, signal, cursor, limit)
	if err != nil {
		return nil, err
	}
	return s.automationWaitRuns(ctx, strings.TrimSpace(workspaceID), waits)
}

func (s *Store) listResolvedWaitsBySignalNeedingSchedulerReplayPage(ctx context.Context, workspaceID string, signal AutomationWaitSignal, cursor AutomationWaitSignalCursor, limit int) ([]AutomationWait, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	workspaceID, signal, signalKey := normalizeAutomationWaitSignalQuery(workspaceID, signal)
	if signalKey == "" {
		return nil, nil
	}
	var waits []AutomationWait
	q := s.db.WithContext(ctx).
		Where("signal_key = ? AND workspace_id = ? AND wait_kind = ? AND resume_event_type = ? AND resource_type = ? AND resource_id = ? AND status = ?", signalKey, workspaceID, signal.WaitKind, signal.EventType, signal.ResourceType, signal.ResourceID, AutomationWaitStatusResolved).
		Where(`(EXISTS (
			SELECT 1 FROM automation_job_runs
			WHERE automation_job_runs.workspace_id = automation_waits.workspace_id
				AND automation_job_runs.run_id = automation_waits.run_id
				AND automation_job_runs.status IN ?
		) OR EXISTS (
			SELECT 1 FROM automation_job_runs
			WHERE automation_job_runs.workspace_id = automation_waits.workspace_id
				AND automation_job_runs.run_id = automation_waits.run_id
				AND automation_job_runs.status = ?
				AND NOT EXISTS (
					SELECT 1 FROM automation_jobs
					WHERE automation_jobs.workspace_id = automation_job_runs.workspace_id
						AND automation_jobs.run_id = automation_job_runs.run_id
						AND automation_jobs.job_run_id = automation_job_runs.id
				)
		))`, []string{AutomationJobRunStatusPending, AutomationJobRunStatusReady}, AutomationJobRunStatusQueued).
		Order("updated_at ASC").
		Order("id ASC")
	q = applyAutomationWaitSignalCursor(q, cursor)
	if limit > 0 {
		q = q.Limit(limit)
	}
	err := q.Find(&waits).Error
	if err != nil {
		return nil, err
	}
	return waits, nil
}

func (s *Store) listWaitsBySignalPage(ctx context.Context, workspaceID string, signal AutomationWaitSignal, statuses []string, cursor AutomationWaitSignalCursor, limit int) ([]AutomationWait, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	workspaceID, signal, signalKey := normalizeAutomationWaitSignalQuery(workspaceID, signal)
	if signalKey == "" || len(statuses) == 0 {
		return nil, nil
	}
	var waits []AutomationWait
	q := s.db.WithContext(ctx).
		Where("signal_key = ? AND workspace_id = ? AND wait_kind = ? AND resume_event_type = ? AND resource_type = ? AND resource_id = ? AND status IN ?", signalKey, workspaceID, signal.WaitKind, signal.EventType, signal.ResourceType, signal.ResourceID, statuses).
		Order("CASE WHEN status = '" + AutomationWaitStatusActive + "' THEN 0 ELSE 1 END").
		Order("updated_at ASC").
		Order("id ASC")
	q = applyAutomationWaitSignalCursor(q, cursor)
	if limit > 0 {
		q = q.Limit(limit)
	}
	err := q.Find(&waits).Error
	if err != nil {
		return nil, err
	}
	return waits, nil
}

func applyAutomationWaitSignalCursor(q *gorm.DB, cursor AutomationWaitSignalCursor) *gorm.DB {
	if strings.TrimSpace(cursor.ID) == "" {
		return q
	}
	return q.Where("(updated_at > ? OR (updated_at = ? AND id > ?))", cursor.UpdatedAt, cursor.UpdatedAt, strings.TrimSpace(cursor.ID))
}

func (s *Store) automationWaitRuns(ctx context.Context, workspaceID string, waits []AutomationWait) ([]AutomationWaitRun, error) {
	if len(waits) == 0 {
		return nil, nil
	}
	runIDs := make([]string, 0, len(waits))
	seenRunIDs := make(map[string]struct{}, len(waits))
	for _, wait := range waits {
		runID := strings.TrimSpace(wait.RunID)
		if runID == "" {
			continue
		}
		if _, ok := seenRunIDs[runID]; ok {
			continue
		}
		seenRunIDs[runID] = struct{}{}
		runIDs = append(runIDs, runID)
	}
	runsByID := make(map[string]AutomationRun, len(runIDs))
	if len(runIDs) > 0 {
		var runs []AutomationRun
		if err := s.db.WithContext(ctx).Where("workspace_id = ? AND id IN ?", strings.TrimSpace(workspaceID), runIDs).Find(&runs).Error; err != nil {
			return nil, fmt.Errorf("get automation wait runs: %w", err)
		}
		for _, run := range runs {
			runsByID[run.ID] = run
		}
	}
	waitRuns := make([]AutomationWaitRun, 0, len(waits))
	for _, wait := range waits {
		run, ok := runsByID[strings.TrimSpace(wait.RunID)]
		if !ok {
			return nil, fmt.Errorf("get automation wait run: %w", gorm.ErrRecordNotFound)
		}
		waitRuns = append(waitRuns, AutomationWaitRun{Wait: wait, Run: run})
	}
	return waitRuns, nil
}

func (s *Store) GetWaitBySignal(ctx context.Context, workspaceID string, signal AutomationWaitSignal) (AutomationWait, error) {
	if err := s.ensureDB(); err != nil {
		return AutomationWait{}, err
	}
	workspaceID, signal, signalKey := normalizeAutomationWaitSignalQuery(workspaceID, signal)
	if signalKey == "" {
		return AutomationWait{}, gorm.ErrRecordNotFound
	}
	var wait AutomationWait
	err := s.db.WithContext(ctx).
		Where("signal_key = ? AND workspace_id = ? AND wait_kind = ? AND resume_event_type = ? AND resource_type = ? AND resource_id = ? AND status IN ?", signalKey, workspaceID, signal.WaitKind, signal.EventType, signal.ResourceType, signal.ResourceID, []string{AutomationWaitStatusActive, AutomationWaitStatusResolved}).
		Order("CASE WHEN status = '" + AutomationWaitStatusActive + "' THEN 0 ELSE 1 END").
		Order("updated_at DESC").
		Order("id DESC").
		First(&wait).Error
	if err != nil {
		return AutomationWait{}, err
	}
	return wait, nil
}

func (s *Store) ListWaits(ctx context.Context, workspaceID string, runID string, status string) ([]AutomationWait, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	q := s.db.WithContext(ctx).Where("workspace_id = ? AND run_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(runID))
	if strings.TrimSpace(status) != "" {
		q = q.Where("status = ?", strings.TrimSpace(status))
	}
	var waits []AutomationWait
	if err := q.Order("created_at ASC").Order("id ASC").Find(&waits).Error; err != nil {
		return nil, fmt.Errorf("list automation waits: %w", err)
	}
	return waits, nil
}

func (s *Store) ResolveWait(ctx context.Context, workspaceID string, waitID string, actorPrincipalID string, resultJSON string, now time.Time) (AutomationWait, AutomationJobRun, error) {
	if err := s.ensureDB(); err != nil {
		return AutomationWait{}, AutomationJobRun{}, err
	}
	var wait AutomationWait
	var jobRun AutomationJobRun
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(waitID)).First(&wait).Error; err != nil {
			return err
		}
		if err := tx.Where("workspace_id = ? AND id = ?", wait.WorkspaceID, wait.JobRunID).First(&jobRun).Error; err != nil {
			return fmt.Errorf("get automation wait job run: %w", err)
		}
		if wait.Status == AutomationWaitStatusResolved && jobRun.Status == AutomationJobRunStatusSucceeded {
			return nil
		}
		if wait.Status != AutomationWaitStatusActive || jobRun.Status != AutomationJobRunStatusWaiting {
			return gorm.ErrRecordNotFound
		}
		wait.Status = AutomationWaitStatusResolved
		wait.ActiveKey = nil
		wait.ResultJSON = normalizeJSONForStore(resultJSON)
		wait.ResolvedBy = strings.TrimSpace(actorPrincipalID)
		wait.ResolvedAt = &now
		wait.UpdatedAt = now
		if err := tx.Save(&wait).Error; err != nil {
			return fmt.Errorf("resolve automation wait: %w", err)
		}
		jobRun.Status = AutomationJobRunStatusSucceeded
		jobRun.OutputJSON = wait.ResultJSON
		jobRun.CompletedAt = &now
		jobRun.UpdatedAt = now
		if err := tx.Save(&jobRun).Error; err != nil {
			return fmt.Errorf("complete automation wait job run: %w", err)
		}
		event := automationEvent("wait_resolved", jobRun, "Automation wait resolved", wait.ResultJSON, now)
		event.ActorPrincipalID = strings.TrimSpace(actorPrincipalID)
		event.ResourceType = wait.ResourceType
		event.ResourceID = wait.ResourceID
		if err := tx.Create(&event).Error; err != nil {
			return fmt.Errorf("create automation wait resolved event: %w", err)
		}
		if err := tx.Model(&AutomationRun{}).
			Where("workspace_id = ? AND id = ? AND status = ?", wait.WorkspaceID, wait.RunID, AutomationRunStatusWaiting).
			Where("NOT EXISTS (SELECT 1 FROM automation_waits WHERE automation_waits.workspace_id = automation_runs.workspace_id AND automation_waits.run_id = automation_runs.id AND automation_waits.status = ?)", AutomationWaitStatusActive).
			Updates(map[string]any{
				"status":     AutomationRunStatusRunning,
				"updated_at": now,
			}).Error; err != nil {
			return fmt.Errorf("mark automation run running after wait: %w", err)
		}
		return nil
	})
	if err != nil {
		return AutomationWait{}, AutomationJobRun{}, err
	}
	return wait, jobRun, nil
}
