package controlplane

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"strings"

	bootstate "github.com/nexuscrm/db/pkg/bootstate"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// migrationIdNormalizeProcessActionLiteralFieldValue is the stable
// `_System_Migration.Id` for this migration. Once stamped, subsequent
// RunRepair invocations no-op the entire function.
//
// Format: yyyymmdd_<short_action> — keep stable; bumping the constant retires
// the stamp and forces a re-run on every existing instance, which is almost
// never what you want. To force a re-run, bump the Version arg to
// RecordMigrationApplied instead and add an explicit re-run gate.
const migrationIdNormalizeProcessActionLiteralFieldValue = "20260507_normalize_process_action_literal_fieldvalue"

// NormalizeProcessActionLiteralFieldValue strips the Salesforce-shaped
// "Literal: " prefix from existing _System_ProcessAction.FieldValue rows.
//
// Why this is a one-time migration, not runtime logic:
//
//   - The L0 runtime's approval FieldUpdate execution now writes
//     ProcessAction.FieldValue verbatim. The "Literal: <value>"
//     authoring grammar is a Salesforce L1-pack concern; baking it
//     into the platform binary violates the L0/L1 boundary.
//   - New writes are already clean: the CRM lifecycle materializer's
//     normalizeApprovalFieldValue strips the prefix at the XML→generic-record
//     adapter boundary.
//   - Rows authored by older builds carry the raw "Literal: Approved"
//     form because the previous runtime trimmed at execution time and
//     never persisted the normalized value. Without this migration,
//     warm-upgraded orgs would write "Literal: Approved" verbatim into
//     the linked record on the next approval transition (data
//     corruption — IANTHEREAL P1, PR #271 Round 3).
//
// One-shot guard: this function is gated by a `_System_Migration` stamp
// (`migrationIdNormalizeProcessActionLiteralFieldValue`). After the first
// successful run, every subsequent call returns 0 without touching
// ProcessAction. The stamp is essential because under the new opaque-
// FieldValue contract a post-upgrade admin/L1 author may legitimately store
// a value that itself starts with the four bytes "Lite" and the rest spelling
// out the prefix — re-running this UPDATE on every repair would silently
// corrupt that authored value (Codex BLOCK, PR #271 Round 8).
//
// Within a single run the WHERE-clause prefix anchor (LEFT(LTRIM(FieldValue),
// 9) = BINARY 'Literal: ') still gives row-level idempotency, so a partial
// failure followed by a re-run before the stamp is recorded converges to the
// same final state.
//
// LTRIM tolerates leading whitespace to mirror the CRM lifecycle materializer's
// normalizeApprovalFieldValue at the XML adapter — both layers treat
// "  Literal: Approved" identically.
// BINARY makes the prefix match case-sensitive across collations, matching
// strings.HasPrefix semantics in the Go normalizer.
func NormalizeProcessActionLiteralFieldValue(ctx context.Context, db *sql.DB) (int, error) {
	applied, err := bootstate.MigrationApplied(ctx, db, migrationIdNormalizeProcessActionLiteralFieldValue)
	if err != nil {
		return 0, err
	}
	if applied {
		log.Printf("   ⏭  NormalizeProcessActionLiteralFieldValue: already applied; skipping (post-upgrade rows may legitimately start with \"Literal: \")")
		return 0, nil
	}

	const updateSQL = `
UPDATE ProcessAction
SET FieldValue = SUBSTRING(LTRIM(FieldValue), 10),
    LastModifiedDate = NOW()
WHERE IsDeleted = 0
  AND FieldValue IS NOT NULL
  AND CHAR_LENGTH(LTRIM(FieldValue)) >= 9
  AND LEFT(LTRIM(FieldValue), 9) = BINARY 'Literal: '`

	res, err := db.ExecContext(ctx, updateSQL)
	if err != nil {
		return 0, fmt.Errorf("NormalizeProcessActionLiteralFieldValue: %w", err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return 0, fmt.Errorf("NormalizeProcessActionLiteralFieldValue rows affected: %w", err)
	}

	// Stamp the migration after the UPDATE succeeds. A failure here must
	// abort RunRepair: the work is half-done (rows stripped, no stamp),
	// and a subsequent run would re-strip post-upgrade authored values
	// because the stamp guard would not yet be in place. RunRepair is
	// wrapped in trackStepFatal at the call site, so returning the error
	// stops ApplyAll before runtime starts.
	if err := bootstate.RecordMigrationApplied(
		ctx, db,
		migrationIdNormalizeProcessActionLiteralFieldValue,
		1,
		"Strip Salesforce 'Literal: ' prefix from warm-upgrade ProcessAction.FieldValue rows (PR #271).",
	); err != nil {
		return 0, err
	}

	if n > 0 {
		log.Printf("   🧼 NormalizeProcessActionLiteralFieldValue: stripped \"Literal: \" prefix from %d ProcessAction row(s)", n)
	}
	return int(n), nil
}

// migrationIdBackfillWorkflowAlertTemplateIds is the stable
// `_System_Migration.Id` for this migration. The stamp is recorded only
// after the function proves closure for active legacy dependencies. After
// Flow-native cutover, dead _System_WorkflowAlert rows may remain as
// historical inventory; unresolved Template values on those rows must not
// keep aborting setup apply once no ProcessAction.Alert or Flow emailAlert can
// read them.
const migrationIdBackfillWorkflowAlertTemplateIds = "20260507_backfill_workflow_alert_template_ids"

// remainingTemplateSampleSize caps the GROUP_CONCAT sample length passed
// back to the operator when closure proof fails. Five distinct values is
// enough to identify the failing pack/L1 component; full sets can be
// queried out-of-band if needed.
const remainingTemplateSampleSize = 5

// BackfillWorkflowAlertTemplateIds resolves _System_WorkflowAlert.Template
// references that store a Salesforce-shaped folder path
// ("unfiled$public/Quote_Approved") or a bare DeveloperName, replacing
// them with the matching _System_EmailTemplate.Id.
//
// Why this is a one-time migration, not runtime logic:
//
//   - The L0 runtime now treats WorkflowAlert.Template as an opaque
//     identifier and only loads _System_EmailTemplate by primary-key Id.
//     Parsing folder-paths or DeveloperName references at runtime baked
//     L1-pack authoring grammar into the platform binary.
//   - L1 packs are now expected to author Template as a resolved Id at
//     pack-apply time.
//   - Rows authored by older builds still carry the path/DeveloperName
//     form. Without this backfill, warm-upgraded orgs silently stop
//     sending workflow-alert email (IANTHEREAL P1, PR #271 Round 3).
//
// Disambiguation: trailing path segment is matched against
// _System_EmailTemplate.DeveloperName, so "unfiled$public/Foo" and
// bare "Foo" resolve identically (SUBSTRING_INDEX(Template, '/', -1)
// returns the entire value when no '/' is present).
//
// Safety:
//
//   - NOT EXISTS guard skips rows whose Template already references a
//     valid EmailTemplate.Id (idempotent — re-running is a no-op).
//   - COUNT(DISTINCT et.Id) = 1 guard skips ambiguous DeveloperName
//     matches so two EmailTemplate rows with the same DeveloperName
//     never lead to non-deterministic Id selection. Ambiguous rows
//     stay unchanged and the runtime fail-graceful path surfaces the
//     misconfiguration. DISTINCT is required: the inner SELECT joins
//     _System_WorkflowAlert × _System_EmailTemplate, so a non-DISTINCT
//     COUNT measures post-JOIN cross-product cardinality (number of
//     alerts × matching templates) rather than the true ambiguity
//     surface (number of distinct EmailTemplate rows that share a
//     DeveloperName). Two alerts referencing the same unambiguous
//     DeveloperName would otherwise produce match_count = 2 and be
//     incorrectly skipped, leaving warm-upgraded orgs silently unable
//     to send email (Codex HIGH, PR #271 Round 5/6).
//   - _System_WorkflowAlert has no IsDeleted column (verify against
//     bootstrap/system_tables.json:9834), so the migration scans every
//     non-empty Template row.
//
// Round-10 closure proof (IANTHEREAL P1, 2026-05-07T04:27:55Z): the
// migration only resolves rows where `resolved.match_count = 1` —
// ambiguous, missing, or otherwise unmatched legacy references stay
// unchanged. Under the new opaque-Template runtime contract,
// approval_service.resolveEmailTemplateID returns those values verbatim
// and ActionService.LoadAndRenderTemplate then NotFoundErrors on the
// Id-only persistence lookup. The runtime fail-graceful path swallows
// the NotFoundError and returns early, so warm-upgraded orgs silently
// stop sending those workflow-alert emails — exactly the failure mode
// this migration was added to prevent.
//
// Flow-native cutover adjustment (issue #1829): the fatal closure check now
// applies only when one of the remaining unresolved rows is still reachable
// from an active legacy dependency: ProcessAction.Alert or Flow emailAlert.
// Unreferenced WorkflowAlert rows are historical inventory, and setup apply
// must not fail on those dead rows after runtime dispatch is disabled.
func BackfillWorkflowAlertTemplateIds(ctx context.Context, db *sql.DB) (int, error) {
	applied, err := bootstate.MigrationApplied(ctx, db, migrationIdBackfillWorkflowAlertTemplateIds)
	if err != nil {
		return 0, err
	}
	if applied {
		log.Printf("   ⏭  BackfillWorkflowAlertTemplateIds: already applied; skipping")
		return 0, nil
	}

	const updateSQL = `
UPDATE _System_WorkflowAlert wa
JOIN (
    SELECT SUBSTRING_INDEX(wa2.Template, '/', -1) AS dev_name,
           MIN(et.Id) AS resolved_id,
           COUNT(DISTINCT et.Id) AS match_count
    FROM _System_WorkflowAlert wa2
    JOIN _System_EmailTemplate et
      ON et.IsDeleted = 0
     AND et.DeveloperName = SUBSTRING_INDEX(wa2.Template, '/', -1)
    WHERE wa2.Template IS NOT NULL
      AND wa2.Template <> ''
      AND NOT EXISTS (
          SELECT 1 FROM _System_EmailTemplate et_id
          WHERE et_id.Id = wa2.Template
            AND et_id.IsDeleted = 0
      )
    GROUP BY SUBSTRING_INDEX(wa2.Template, '/', -1)
) resolved
  ON resolved.dev_name = SUBSTRING_INDEX(wa.Template, '/', -1)
SET wa.Template = resolved.resolved_id,
    wa.LastModifiedDate = NOW()
WHERE wa.Template IS NOT NULL
  AND wa.Template <> ''
  AND resolved.match_count = 1
  AND NOT EXISTS (
      SELECT 1 FROM _System_EmailTemplate et_id2
      WHERE et_id2.Id = wa.Template
        AND et_id2.IsDeleted = 0
  )`

	res, err := db.ExecContext(ctx, updateSQL)
	if err != nil {
		return 0, fmt.Errorf("BackfillWorkflowAlertTemplateIds: %w", err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return 0, fmt.Errorf("BackfillWorkflowAlertTemplateIds rows affected: %w", err)
	}
	if n > 0 {
		log.Printf("   🧼 BackfillWorkflowAlertTemplateIds: resolved %d WorkflowAlert.Template ref(s) to _System_EmailTemplate.Id", n)
	}

	// Closure proof: count remaining non-empty Template values that do not
	// resolve to an active EmailTemplate.Id. After Flow-native cutover, this
	// set is fatal only when an active legacy dependency can still read one of
	// those rows; unreferenced rows are historical inventory.
	const closureSQL = `
SELECT COUNT(*),
       GROUP_CONCAT(DISTINCT Template ORDER BY Template SEPARATOR ', ')
FROM _System_WorkflowAlert
WHERE Template IS NOT NULL
  AND Template <> ''
  AND NOT EXISTS (
      SELECT 1 FROM _System_EmailTemplate et
      WHERE et.Id = _System_WorkflowAlert.Template
        AND et.IsDeleted = 0
  )`

	var remaining int
	var sample sql.NullString
	if err := db.QueryRowContext(ctx, closureSQL).Scan(&remaining, &sample); err != nil {
		return int(n), fmt.Errorf("BackfillWorkflowAlertTemplateIds closure check: %w", err)
	}
	if remaining > 0 {
		activeDependencyCount, activeDependencySample, activeErr := activeWorkflowAlertTemplateDependencies(ctx, db)
		if activeErr != nil {
			return int(n), activeErr
		}
		if activeDependencyCount > 0 {
			if activeDependencySample == "" {
				activeDependencySample = sample.String
			}
			return int(n), fmt.Errorf(
				"BackfillWorkflowAlertTemplateIds: %d active legacy WorkflowAlert dependency row(s) still reference _System_WorkflowAlert.Template value(s) that do not resolve to an active _System_EmailTemplate.Id (sample: %s); migrate the dependency to Flow sendEmail, remove it, or repair the active dependency before setup apply.",
				activeDependencyCount, activeDependencySample,
			)
		}
		log.Printf("   ⏭  BackfillWorkflowAlertTemplateIds: %d unresolved WorkflowAlert.Template row(s) remain, but no active ProcessAction.Alert or Flow emailAlert can read them; treating rows as historical inventory", remaining)
	}

	// Closure proven — stamp the migration so subsequent RunRepair
	// invocations short-circuit at the SELECT and never re-run the UPDATE.
	if err := bootstate.RecordMigrationApplied(
		ctx, db,
		migrationIdBackfillWorkflowAlertTemplateIds,
		1,
		"Resolve _System_WorkflowAlert.Template legacy folder-path / DeveloperName references to EmailTemplate.Id (PR #271).",
	); err != nil {
		return int(n), err
	}

	return int(n), nil
}

type workflowAlertTemplateDependencyRow struct {
	id       string
	object   string
	name     string
	template string
}

func activeWorkflowAlertTemplateDependencies(ctx context.Context, db *sql.DB) (int, string, error) {
	alerts, err := unresolvedWorkflowAlertTemplateRows(ctx, db)
	if err != nil {
		return 0, "", err
	}
	if len(alerts) == 0 {
		return 0, "", nil
	}

	alertsByName := make(map[string][]workflowAlertTemplateDependencyRow, len(alerts))
	alertsByKey := make(map[string]workflowAlertTemplateDependencyRow, len(alerts))
	for _, alert := range alerts {
		alertsByName[strings.ToLower(strings.TrimSpace(alert.name))] = append(alertsByName[strings.ToLower(strings.TrimSpace(alert.name))], alert)
		alertsByKey[workflowAlertTemplateDependencyKey(alert.object, alert.name)] = alert
	}

	count := 0
	var samples []string
	addDependency := func(source string, alert workflowAlertTemplateDependencyRow) {
		count++
		if len(samples) >= remainingTemplateSampleSize {
			return
		}
		samples = append(samples, fmt.Sprintf("%s -> %s.%s Template=%s", source, alert.object, alert.name, alert.template))
	}

	if err := collectActiveProcessActionAlertTemplateDependencies(ctx, db, alertsByName, addDependency); err != nil {
		return 0, "", err
	}
	if err := collectActiveFlowEmailAlertTemplateDependencies(ctx, db, alertsByKey, addDependency); err != nil {
		return 0, "", err
	}

	sample := strings.Join(samples, ", ")
	if count > len(samples) {
		if sample != "" {
			sample += ", "
		}
		sample += fmt.Sprintf("… (%d total)", count)
	}
	return count, sample, nil
}

func unresolvedWorkflowAlertTemplateRows(ctx context.Context, db *sql.DB) ([]workflowAlertTemplateDependencyRow, error) {
	const query = `
SELECT Id, ObjectApiName, Name, Template
FROM _System_WorkflowAlert
WHERE Template IS NOT NULL
  AND Template <> ''
  AND NOT EXISTS (
      SELECT 1 FROM _System_EmailTemplate et
      WHERE et.Id = _System_WorkflowAlert.Template
        AND et.IsDeleted = 0
  )
ORDER BY ObjectApiName, Name, Id`

	rows, err := db.QueryContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("BackfillWorkflowAlertTemplateIds active dependency scan unresolved WorkflowAlert rows: %w", err)
	}
	defer rows.Close()

	var out []workflowAlertTemplateDependencyRow
	for rows.Next() {
		var row workflowAlertTemplateDependencyRow
		if err := rows.Scan(&row.id, &row.object, &row.name, &row.template); err != nil {
			return nil, fmt.Errorf("BackfillWorkflowAlertTemplateIds scan unresolved WorkflowAlert row: %w", err)
		}
		out = append(out, row)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("BackfillWorkflowAlertTemplateIds iterate unresolved WorkflowAlert rows: %w", err)
	}
	return out, nil
}

func collectActiveProcessActionAlertTemplateDependencies(
	ctx context.Context,
	db *sql.DB,
	alertsByName map[string][]workflowAlertTemplateDependencyRow,
	addDependency func(source string, alert workflowAlertTemplateDependencyRow),
) error {
	// Match approval runtime reachability: State='Active' drives current submit
	// resolution, legacy rows may only have IsActive, and nonterminal instances
	// can still fetch actions by their stored ProcessDefinitionId.
	const query = `
	SELECT COALESCE(a.ApiName, ''), COALESCE(pd.DeveloperName, ''), COALESCE(a.Name, ''),
	       CASE WHEN COALESCE(pd.State, '') = 'Active' OR COALESCE(pd.IsActive, 0) = 1 THEN 1 ELSE 0 END AS RuntimeReachable,
	       COUNT(DISTINCT pi.Id)
	FROM ProcessAction a
	JOIN ProcessDefinition pd ON pd.Id = a.ProcessDefinitionId
	LEFT JOIN ProcessInstance pi ON pi.ProcessDefinitionId = a.ProcessDefinitionId
	  AND COALESCE(pi.Status, '') NOT IN (?, ?, ?)
	WHERE a.ActionType = ?
	  AND COALESCE(a.IsDeleted, 0) = 0
	  AND (COALESCE(pd.State, '') = 'Active' OR COALESCE(pd.IsActive, 0) = 1 OR pi.Id IS NOT NULL)
	GROUP BY a.Id, a.ApiName, pd.DeveloperName, a.Name, pd.State, pd.IsActive
	ORDER BY pd.DeveloperName, a.Name, a.Id`

	rows, err := db.QueryContext(ctx, query,
		constants.ApprovalStatusApproved,
		constants.ApprovalStatusRejected,
		constants.ApprovalStatusRemoved,
		constants.ProcessActionTypeAlert,
	)
	if err != nil {
		return fmt.Errorf("BackfillWorkflowAlertTemplateIds active dependency scan ProcessAction.Alert rows: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var apiName, processName, actionName string
		var definitionRuntimeReachable, runnableInstanceCount int
		if err := rows.Scan(&apiName, &processName, &actionName, &definitionRuntimeReachable, &runnableInstanceCount); err != nil {
			return fmt.Errorf("BackfillWorkflowAlertTemplateIds scan ProcessAction.Alert row: %w", err)
		}
		apiName = strings.TrimSpace(apiName)
		if apiName == "" {
			continue
		}
		source := fmt.Sprintf("ProcessAction.Alert:%s/%s", processName, actionName)
		if definitionRuntimeReachable == 0 && runnableInstanceCount > 0 {
			source = fmt.Sprintf("%s[pendingInstances=%d]", source, runnableInstanceCount)
		}
		for _, alert := range alertsByName[strings.ToLower(apiName)] {
			addDependency(source, alert)
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("BackfillWorkflowAlertTemplateIds iterate ProcessAction.Alert rows: %w", err)
	}
	return nil
}

func collectActiveFlowEmailAlertTemplateDependencies(
	ctx context.Context,
	db *sql.DB,
	alertsByKey map[string]workflowAlertTemplateDependencyRow,
	addDependency func(source string, alert workflowAlertTemplateDependencyRow),
) error {
	const query = `
	SELECT fd.DeveloperName, COALESCE(f.Metadata, ''), f.Id, COALESCE(f.VersionNumber, 0),
	       CASE WHEN COALESCE(fd.IsDeleted, 0) = 0 AND f.Id = fd.ActiveVersionId THEN 1 ELSE 0 END,
	       COUNT(DISTINCT CASE WHEN COALESCE(fi.InterviewStatus, '') = 'Paused' THEN fi.Id END),
	       COUNT(DISTINCT CASE WHEN COALESCE(fi.InterviewStatus, '') = 'Running' THEN fi.Id END)
	FROM Flow f
	JOIN FlowDefinition fd ON fd.Id = f.DefinitionId
	LEFT JOIN FlowInterview fi ON fi.FlowVersionId = f.Id
	  AND (
	    COALESCE(fi.InterviewStatus, '') = ?
	    OR (
	      COALESCE(fi.InterviewStatus, '') = ?
	      AND fi.ClaimedAt IS NOT NULL
	      AND fi.ClaimedAt <= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 10 MINUTE)
	    )
	  )
	WHERE COALESCE(f.IsDeleted, 0) = 0
	  AND (
	    (COALESCE(fd.IsDeleted, 0) = 0 AND f.Id = fd.ActiveVersionId)
	    OR fi.Id IS NOT NULL
	  )
	GROUP BY fd.DeveloperName, f.Metadata, f.Id, f.VersionNumber, fd.IsDeleted, fd.ActiveVersionId
	ORDER BY fd.DeveloperName, f.VersionNumber, f.Id`

	rows, err := db.QueryContext(ctx, query, models.FlowInterviewStatusPaused, models.FlowInterviewStatusRunning)
	if err != nil {
		return fmt.Errorf("BackfillWorkflowAlertTemplateIds active dependency scan Flow emailAlert rows: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var developerName, metadataJSON, versionID string
		var versionNumber, activeVersion, pausedInterviewCount, recoverableRunningInterviewCount int
		if err := rows.Scan(&developerName, &metadataJSON, &versionID, &versionNumber, &activeVersion, &pausedInterviewCount, &recoverableRunningInterviewCount); err != nil {
			return fmt.Errorf("BackfillWorkflowAlertTemplateIds scan Flow row: %w", err)
		}
		runtimeState := workflowAlertFlowRuntimeState(activeVersion != 0, pausedInterviewCount, recoverableRunningInterviewCount)
		var metadata models.FlowMetadata
		if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
			if strings.Contains(strings.ToLower(metadataJSON), "emailalert") {
				return fmt.Errorf("BackfillWorkflowAlertTemplateIds cannot classify %s Flow %q metadata containing emailAlert: %w", runtimeState, developerName, err)
			}
			continue
		}
		startObject := ""
		if metadata.Start != nil {
			startObject = strings.TrimSpace(metadata.Start.Object)
		}
		for _, action := range metadata.ActionCalls {
			if !strings.EqualFold(action.ActionType, "emailAlert") {
				continue
			}
			objectAPIName, alertName := splitWorkflowAlertEmailAlertActionName(action.ActionName, startObject)
			if alert, ok := alertsByKey[workflowAlertTemplateDependencyKey(objectAPIName, alertName)]; ok {
				source := fmt.Sprintf("Flow.emailAlert:%s/%s", developerName, action.Name)
				if activeVersion == 0 && (pausedInterviewCount > 0 || recoverableRunningInterviewCount > 0) {
					versionRef := fmt.Sprintf("v%d", versionNumber)
					if versionNumber == 0 {
						versionRef = versionID
					}
					var details []string
					if pausedInterviewCount > 0 {
						details = append(details, fmt.Sprintf("pausedInterviews=%d", pausedInterviewCount))
					}
					if recoverableRunningInterviewCount > 0 {
						details = append(details, fmt.Sprintf("recoverableRunningInterviews=%d", recoverableRunningInterviewCount))
					}
					source = fmt.Sprintf("Flow.emailAlert:%s/%s/%s[%s]", developerName, versionRef, action.Name, strings.Join(details, ","))
				}
				addDependency(source, alert)
			}
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("BackfillWorkflowAlertTemplateIds iterate Flow rows: %w", err)
	}
	return nil
}

func splitWorkflowAlertEmailAlertActionName(actionName, startObject string) (string, string) {
	actionName = strings.TrimSpace(actionName)
	if actionName == "" {
		return "", ""
	}
	if idx := strings.Index(actionName, "."); idx > 0 && idx < len(actionName)-1 {
		return strings.TrimSpace(actionName[:idx]), strings.TrimSpace(actionName[idx+1:])
	}
	return strings.TrimSpace(startObject), actionName
}

func workflowAlertTemplateDependencyKey(objectAPIName, alertName string) string {
	return strings.ToLower(strings.TrimSpace(objectAPIName)) + "\x00" + strings.ToLower(strings.TrimSpace(alertName))
}

func workflowAlertFlowRuntimeState(activeVersion bool, pausedInterviewCount, recoverableRunningInterviewCount int) string {
	var states []string
	if activeVersion {
		states = append(states, "active_version")
	}
	if pausedInterviewCount > 0 {
		states = append(states, "paused_interview")
	}
	if recoverableRunningInterviewCount > 0 {
		states = append(states, "recoverable_running_interview")
	}
	if len(states) == 0 {
		return "runtime_reachable"
	}
	return strings.Join(states, "+")
}
