package main

import (
	"fmt"
	"time"
)

// parsedPMV is the minimal bundle of values we pull out of the root
// processMetadataValues array. Strings default to empty when absent; the
// caller decides how to handle missing required values.
type parsedPMV struct {
	ObjectType        string
	ObjectVariable    string
	OldObjectVariable string
	TriggerType       string // "onCreateOnly" / "onCreateOrTriggeringUpdate" / "onAllChanges"
}

// parseProcessMetadataValues extracts the legacy Workflow trigger metadata
// from the flow's root processMetadataValues array. The array entries look
// like:
//
//	{ "name": "ObjectType", "value": { "stringValue": "Order", ...nulls } }
//	{ "name": "ObjectVariable", "value": { "elementReference": "myVariable_current", ... } }
//
// We tolerate missing entries — classify() decides whether any of them are
// actually required for a given flow's migration profile.
func parseProcessMetadataValues(metadata map[string]any) parsedPMV {
	var out parsedPMV
	pmv, ok := metadata["processMetadataValues"].([]any)
	if !ok {
		return out
	}
	for _, entry := range pmv {
		m, ok := entry.(map[string]any)
		if !ok {
			continue
		}
		name, _ := m["name"].(string)
		val, _ := m["value"].(map[string]any)
		switch name {
		case "ObjectType":
			out.ObjectType = stringFromTypedValue(val)
		case "ObjectVariable":
			out.ObjectVariable = stringFromTypedValue(val)
		case "OldObjectVariable":
			out.OldObjectVariable = stringFromTypedValue(val)
		case "TriggerType":
			out.TriggerType = stringFromTypedValue(val)
		}
	}
	return out
}

// stringFromTypedValue pulls the first non-nil string from an SF typed
// value. Only two of the ~16 fields are populated at a time (stringValue
// or elementReference for our targets), so a short ladder is clearer than
// a generic switch.
func stringFromTypedValue(v map[string]any) string {
	if v == nil {
		return ""
	}
	if s, ok := v["stringValue"].(string); ok && s != "" {
		return s
	}
	if s, ok := v["elementReference"].(string); ok && s != "" {
		return s
	}
	return ""
}

// mapTriggerType maps the legacy SF Workflow TriggerType string to the
// native NexusCRM (object, triggerType, recordTriggerType) triple. The
// `ok` return is false when the input token is unknown — callers must
// fail the migration rather than silently emit an empty trigger
// definition (which would produce an inert flow with a corrupted
// FlowDefinition row).
//
//	onCreateOnly               -> RecordAfterSave / Create
//	onCreateOrTriggeringUpdate -> RecordAfterSave / CreateAndUpdate
//	onAllChanges               -> RecordAfterSave / CreateAndUpdate
//
// We intentionally default to RecordAfterSave (post-commit) because every
// legacy Workflow flow in the KEEP list performs record updates on other
// objects or actionCalls — those are unsafe in RecordBeforeSave.
// CreateAndUpdate is the safe default for onAllChanges to preserve
// behavior.
func mapTriggerType(sfTrigger string) (triggerType, recordTriggerType string, ok bool) {
	switch sfTrigger {
	case "onCreateOnly":
		return "RecordAfterSave", "Create", true
	case "onCreateOrTriggeringUpdate":
		return "RecordAfterSave", "CreateAndUpdate", true
	case "onAllChanges":
		return "RecordAfterSave", "CreateAndUpdate", true
	default:
		return "", "", false
	}
}

// buildStart constructs the native FlowStart element for the migrated
// flow, plus the FlowDefinition.TriggerType and TriggerObjectOrEvent
// strings. For InvocableProcess callees returns (nil, "", "", nil).
// Returns a non-nil error when the parsed processMetadataValues cannot
// be translated into a valid native trigger (missing ObjectType,
// unknown TriggerType, ...).
func buildStart(metadata map[string]any, profile migrationProfile, pmv parsedPMV) (map[string]any, string, string, error) {
	if profile.IsCallableOnly {
		// No FlowStart — callable-only flow.
		return nil, "", "", nil
	}

	if profile.IsScheduled {
		// Order_Expiration_Notice_90 special-case. Per design doc Wave 3:
		// ProcessType=AutoLaunchedFlow, start.triggerType="Scheduled".
		// The scheduled driver needs a target object; fail loudly if
		// processMetadataValues does not supply one.
		if pmv.ObjectType == "" {
			return nil, "", "", fmt.Errorf("%s: scheduled flow requires processMetadataValues.ObjectType", profile.DeveloperName)
		}
		start := newStartScaffold()
		start["triggerType"] = "Scheduled"
		start["object"] = pmv.ObjectType
		// Synthesize the schedule block so the scheduler picks up this flow.
		// Design doc Artifact 1 Class B: Daily at midnight UTC.
		start["schedule"] = map[string]any{
			"frequency":      "Daily",
			"startDate":      time.Now().Format("2006-01-02"),
			"startTime":      "00:00:00",
			"timezoneSidKey": "UTC",
		}
		// Connector: chain to whatever startElementReference the legacy flow
		// already had, so the Wait logic runs unchanged under the scheduled
		// trigger.
		if ref, ok := metadata["startElementReference"].(string); ok && ref != "" {
			start["connector"] = map[string]any{"targetReference": ref}
		} else if existing, ok := metadata["start"].(map[string]any); ok {
			if conn, ok := existing["connector"]; ok {
				start["connector"] = conn
			}
		}
		return start, "Scheduled", pmv.ObjectType, nil
	}

	// Default: native record-triggered FlowStart.
	if pmv.ObjectType == "" {
		return nil, "", "", fmt.Errorf("%s: record-triggered flow requires processMetadataValues.ObjectType", profile.DeveloperName)
	}
	triggerType, recordTriggerType, ok := mapTriggerType(pmv.TriggerType)
	if !ok {
		return nil, "", "", fmt.Errorf("%s: unknown processMetadataValues.TriggerType %q (expected onCreateOnly / onCreateOrTriggeringUpdate / onAllChanges)",
			profile.DeveloperName, pmv.TriggerType)
	}
	start := newStartScaffold()
	start["object"] = pmv.ObjectType
	start["triggerType"] = triggerType
	start["recordTriggerType"] = recordTriggerType
	if ref, ok := metadata["startElementReference"].(string); ok && ref != "" {
		start["connector"] = map[string]any{"targetReference": ref}
	} else if existing, ok := metadata["start"].(map[string]any); ok {
		if conn, ok := existing["connector"]; ok {
			start["connector"] = conn
		}
	}
	// If the legacy flow already has a `start` block with filters, preserve
	// them. SF Workflow rules historically encoded entry criteria as a
	// top-level `decisions` chain, not as a start filter, but we carry
	// whatever the upstream author configured across verbatim.
	if existing, ok := metadata["start"].(map[string]any); ok {
		if filters, ok := existing["filters"].([]any); ok && len(filters) > 0 {
			start["filters"] = filters
		}
		if fl, ok := existing["filterLogic"].(string); ok && fl != "" {
			start["filterLogic"] = fl
		}
	}
	return start, triggerType, pmv.ObjectType, nil
}

// newStartScaffold returns the baseline FlowStart with location keys and a
// stable null-shape so downstream tooling does not trip on optional keys
// flipping between absent and present across flows.
//
// Numeric literals are float64 so the DeepEqual roundtrip check in
// Migrate passes — json.Unmarshal decodes untyped numbers as float64.
func newStartScaffold() map[string]any {
	return map[string]any{
		"locationX": float64(50),
		"locationY": float64(0),
	}
}

// diffSummary returns a short human-readable diff headline for dry-run
// output. We avoid pulling a full-blown diff library — a rough delta is
// enough for a log line.
func diffSummary(r *MigrationResult) string {
	if r.AlreadyMigrated {
		return "already migrated (no-op)"
	}
	return "migrated"
}
