package main

import (
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

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

const (
	applyPackName        = "workflow-alert-migration-generated"
	applyPackDisplayName = "WorkflowAlert Migration Generated"
	applyPackPath        = "generated-pack"
	applyPackHashSuffix  = 8
)

type ApplyPackPlan struct {
	PackName                string                           `json:"packName"`
	PackPath                string                           `json:"packPath"`
	GeneratedEmailTemplates []GeneratedEmailTemplateArtifact `json:"generatedEmailTemplates,omitempty"`
	GeneratedFlows          []GeneratedFlowArtifact          `json:"generatedFlows,omitempty"`
	SkippedActions          []SkippedApplyPackAction         `json:"skippedActions,omitempty"`
}

type GeneratedEmailTemplateArtifact struct {
	SourceTemplateID        string `json:"sourceTemplateId"`
	SourceTemplateDeveloper string `json:"sourceTemplateDeveloperName,omitempty"`
	DeveloperName           string `json:"developerName"`
	GeneratedTemplateID     string `json:"generatedTemplateId"`
	FilePath                string `json:"filePath"`
	RequiresRecordContext   bool   `json:"requiresRecordContext,omitempty"`

	payload []byte
}

type GeneratedFlowArtifact struct {
	SourceType               string                           `json:"sourceType"`
	SourceKey                string                           `json:"sourceKey"`
	FlowDeveloperName        string                           `json:"flowDeveloperName"`
	SendEmailActionName      string                           `json:"sendEmailActionName"`
	TemplateID               string                           `json:"templateId"`
	TemplateDeveloper        string                           `json:"templateDeveloperName"`
	To                       []string                         `json:"to,omitempty"`
	Cc                       []string                         `json:"cc,omitempty"`
	SenderPolicy             *SendEmailSenderPolicy           `json:"senderPolicy,omitempty"`
	ResolverInputFields      []string                         `json:"resolverInputFields,omitempty"`
	RequiresRecordContext    bool                             `json:"requiresRecordContext,omitempty"`
	RecordContextObject      string                           `json:"recordContextObject,omitempty"`
	RecordFieldDependencies  []GeneratedRecordFieldDependency `json:"recordFieldDependencies,omitempty"`
	RequiredPackDependencies []string                         `json:"requiredPackDependencies,omitempty"`
	FilePath                 string                           `json:"filePath"`

	payload []byte
}

type GeneratedRecordFieldDependency struct {
	ObjectAPIName string `json:"objectApiName"`
	FieldAPIName  string `json:"fieldApiName"`
	OwnerPack     string `json:"ownerPack"`
}

type SkippedApplyPackAction struct {
	SourceType string   `json:"sourceType"`
	SourceKey  string   `json:"sourceKey"`
	ActionName string   `json:"actionName"`
	Reasons    []string `json:"reasons"`
}

type generatedPackManifest struct {
	ManifestFormatVersion string                     `json:"manifestFormatVersion"`
	Name                  string                     `json:"name"`
	DisplayName           string                     `json:"displayName"`
	Category              string                     `json:"category"`
	Version               string                     `json:"version"`
	PlatformCompatibility generatedPackCompatibility `json:"platformCompatibility"`
	Dependencies          []generatedPackDependency  `json:"dependencies"`
	Components            []generatedPackComponent   `json:"components"`
	Extensions            generatedPackExtensions    `json:"extensions"`
}

type generatedPackCompatibility struct {
	Min          string `json:"min"`
	MaxExclusive string `json:"maxExclusive"`
}

type generatedPackDependency struct {
	Name              string `json:"name"`
	VersionConstraint string `json:"versionConstraint,omitempty"`
}

type generatedPackComponent struct {
	Kind          string                  `json:"kind"`
	BusinessKey   string                  `json:"businessKey"`
	Manageability string                  `json:"manageability"`
	Path          string                  `json:"path"`
	DependsOn     []generatedComponentRef `json:"dependsOn,omitempty"`
}

type generatedComponentRef struct {
	Kind        string `json:"kind"`
	BusinessKey string `json:"businessKey"`
	Required    bool   `json:"required"`
}

type generatedPackExtensions struct {
	FlowNativeEmailMigration generatedPackProvenance `json:"flowNativeEmailMigration"`
}

type generatedPackProvenance struct {
	DesignAuthority string                         `json:"designAuthority"`
	SourceMode      string                         `json:"sourceMode"`
	Sources         []generatedComponentProvenance `json:"sources"`
}

type generatedComponentProvenance struct {
	Kind        string `json:"kind"`
	BusinessKey string `json:"businessKey"`
	SourceType  string `json:"sourceType"`
	SourceKey   string `json:"sourceKey"`
}

type generatedEmailTemplatePayload struct {
	Name          string  `json:"name,omitempty"`
	DeveloperName string  `json:"developerName"`
	Subject       string  `json:"subject"`
	HTMLValue     string  `json:"htmlValue,omitempty"`
	Body          string  `json:"body,omitempty"`
	Description   *string `json:"description,omitempty"`
	TemplateType  string  `json:"templateType,omitempty"`
	IsActive      *bool   `json:"isActive,omitempty"`
}

type generatedFlowPayload struct {
	Label                 string                  `json:"label"`
	Description           string                  `json:"description,omitempty"`
	ProcessType           string                  `json:"processType"`
	StartElementReference string                  `json:"startElementReference"`
	Variables             []models.FlowVariable   `json:"variables,omitempty"`
	ActionCalls           []models.FlowActionCall `json:"actionCalls"`
}

func (r *Report) buildApplyPackPlan() {
	plan := &ApplyPackPlan{
		PackName: applyPackName,
		PackPath: applyPackPath,
	}
	templateArtifacts := map[string]GeneratedEmailTemplateArtifact{}
	templateIDs := map[string]string{}
	templateDevelopers := map[string]string{}

	if r.DryRun == nil {
		plan.SkippedActions = append(plan.SkippedActions, SkippedApplyPackAction{
			Reasons: []string{"dry-run plan is required before apply-pack generation"},
		})
		r.addUnsupported(UnsupportedItem{
			Severity:       "blocker",
			SourceType:     "migration",
			SourceKey:      "apply-pack",
			Code:           "apply_pack_generation_unsupported",
			Message:        "apply-pack generation requires a dry-run plan.",
			Recommendation: "Run apply-pack through the migration command so inventory and dry-run planning happen first.",
		})
		r.ApplyPack = plan
		return
	}

	for _, action := range r.DryRun.ProposedSendEmailActions {
		flow, template, reasons := r.generateApplyPackFlow(action, templateArtifacts, templateIDs, templateDevelopers)
		if len(reasons) > 0 {
			plan.SkippedActions = append(plan.SkippedActions, SkippedApplyPackAction{
				SourceType: action.SourceType,
				SourceKey:  action.SourceKey,
				ActionName: action.ActionName,
				Reasons:    uniqueSortedStrings(reasons),
			})
			if len(action.BlockedBy) == 0 {
				r.addUnsupported(UnsupportedItem{
					Severity:       "blocker",
					SourceType:     action.SourceType,
					SourceKey:      action.SourceKey,
					Code:           "apply_pack_generation_unsupported",
					Message:        fmt.Sprintf("apply-pack generator cannot safely emit metadata for %s/%s: %s", action.SourceType, action.SourceKey, strings.Join(uniqueSortedStrings(reasons), "; ")),
					Recommendation: "Keep this source in the migration report until the target Flow shape or binding surface is implemented explicitly.",
				})
			}
			continue
		}
		if template.SourceTemplateID != "" {
			templateArtifacts[template.SourceTemplateID] = template
			templateIDs[template.SourceTemplateID] = template.GeneratedTemplateID
			templateDevelopers[template.SourceTemplateID] = template.DeveloperName
		}
		plan.GeneratedFlows = append(plan.GeneratedFlows, flow)
	}

	templateIDsSorted := make([]string, 0, len(templateArtifacts))
	for id := range templateArtifacts {
		templateIDsSorted = append(templateIDsSorted, id)
	}
	sort.Strings(templateIDsSorted)
	for _, id := range templateIDsSorted {
		plan.GeneratedEmailTemplates = append(plan.GeneratedEmailTemplates, templateArtifacts[id])
	}
	sort.Slice(plan.GeneratedFlows, func(i, j int) bool {
		return plan.GeneratedFlows[i].FlowDeveloperName < plan.GeneratedFlows[j].FlowDeveloperName
	})
	sort.Slice(plan.SkippedActions, func(i, j int) bool {
		if plan.SkippedActions[i].SourceType == plan.SkippedActions[j].SourceType {
			return plan.SkippedActions[i].SourceKey < plan.SkippedActions[j].SourceKey
		}
		return plan.SkippedActions[i].SourceType < plan.SkippedActions[j].SourceType
	})
	r.ApplyPack = plan
}

func (r *Report) generateApplyPackFlow(
	action ProposedSendEmailAction,
	templateArtifacts map[string]GeneratedEmailTemplateArtifact,
	templateIDs map[string]string,
	templateDevelopers map[string]string,
) (GeneratedFlowArtifact, GeneratedEmailTemplateArtifact, []string) {
	var reasons []string
	if len(action.BlockedBy) > 0 {
		reasons = append(reasons, action.BlockedBy...)
	}
	if action.ActionType != targetSendEmailAction {
		reasons = append(reasons, "only canonical sendEmail dry-run actions can be generated")
	}
	if action.SourceType != constants.TableWorkflowAlert {
		reasons = append(reasons, "this generator only emits new Flow-native notification Flows for WorkflowAlert rows; ProcessAction and existing Flow rewrites require their source-specific binding surfaces")
	}
	if len(action.VariablesHint) > 0 {
		reasons = append(reasons, "template variables are not generated unless the dry-run provides an exact variables contract")
	}

	var template GeneratedEmailTemplateArtifact
	if action.TemplateID == "" {
		reasons = append(reasons, "templateId is required")
	} else if existing, ok := templateArtifacts[action.TemplateID]; ok {
		template = existing
	} else {
		var templateReasons []string
		template, templateReasons = r.generateEmailTemplateArtifact(action.TemplateID)
		reasons = append(reasons, templateReasons...)
	}

	resolverFields, resolverReasons := applyPackResolverFields(action.Prerequisites)
	reasons = append(reasons, resolverReasons...)
	if len(action.To) == 0 && len(resolverFields) == 0 {
		reasons = append(reasons, "sendEmail.to would be empty")
	}
	if len(action.To) == 0 && len(resolverFields) > 0 {
		reasons = append(reasons, "resolver-only WorkflowAlert recipients require an explicit no-recipient Flow branch before apply-pack generation")
	}
	var senderPolicy *SendEmailSenderPolicy
	if action.SenderPolicy != nil {
		switch normalized := normalizeWorkflowAlertSenderPolicyType(action.SenderPolicy.Type); normalized {
		case sendEmailSenderPolicyDefault:
			senderPolicy = &SendEmailSenderPolicy{Type: normalized}
		default:
			reasons = append(reasons, fmt.Sprintf("sender policy %q cannot be emitted as canonical sendEmail metadata", action.SenderPolicy.Type))
		}
	}
	requiresRecordContext := len(resolverFields) > 0 || template.RequiresRecordContext
	recordContextObject := ""
	var recordFieldDependencies []GeneratedRecordFieldDependency
	var requiredPackDependencies []string
	if requiresRecordContext {
		recordContextObject = applyPackWorkflowAlertObject(action)
		if recordContextObject == "" {
			reasons = append(reasons, "record context object is required for generated $Record references")
		} else {
			var dependencyReasons []string
			recordFieldDependencies, dependencyReasons = r.applyPackRecordFieldDependencies(action.TemplateID, recordContextObject, resolverFields)
			reasons = append(reasons, dependencyReasons...)
			requiredPackDependencies = requiredPackDependenciesFromRecordFields(recordFieldDependencies)
		}
	}
	if len(reasons) > 0 {
		return GeneratedFlowArtifact{}, template, uniqueSortedStrings(reasons)
	}

	if generatedID := strings.TrimSpace(templateIDs[action.TemplateID]); generatedID != "" {
		template.GeneratedTemplateID = generatedID
	}
	if developerName := strings.TrimSpace(templateDevelopers[action.TemplateID]); developerName != "" {
		template.DeveloperName = developerName
	}
	if template.GeneratedTemplateID == "" {
		template.GeneratedTemplateID = deterministicSourceBackedEmailTemplateID(applyPackName, template.DeveloperName)
	}

	flowDeveloperName := generatedFlowDeveloperName(action)
	sendActionName := generatedActionName(action.ActionName, "Send_Email")
	resolverName := generatedActionName("Resolve_"+action.ActionName, "Resolve_Email_Recipients")
	resolvedToVar := generatedActionName(flowDeveloperName+"_Resolved_To", "Resolved_To")
	sentCountVar := generatedActionName(flowDeveloperName+"_Sent_Count", "Sent_Count")
	recipientResultsVar := generatedActionName(flowDeveloperName+"_Recipient_Results", "Recipient_Results")
	messageIDVar := generatedActionName(flowDeveloperName+"_Message_Id", "Message_Id")

	var actions []models.FlowActionCall
	var variables []models.FlowVariable
	if requiresRecordContext {
		variables = append(variables, models.FlowVariable{
			Name:       "$Record",
			DataType:   "SObject",
			IsInput:    true,
			ObjectType: recordContextObject,
		})
	}
	toValue := applyPackSendEmailToValue(action.To, "")
	if len(resolverFields) > 0 {
		resolverInputs := make([]interface{}, 0, len(resolverFields))
		for _, field := range resolverFields {
			resolverInputs = append(resolverInputs, map[string]interface{}{"elementReference": "$Record." + field})
		}
		actions = append(actions, models.FlowActionCall{
			Name:       resolverName,
			ActionName: resolverName,
			ActionType: "resolveEmailRecipients",
			InputParameters: []models.FlowActionParameter{{
				Name:  "emails",
				Value: applyPackSingleOrSlice(resolverInputs),
			}},
			OutputParameters: []models.FlowActionParameter{{
				Name:  "emails",
				Value: resolvedToVar,
			}},
			Connector: &models.FlowConnector{TargetReference: sendActionName},
		})
		variables = append(variables, models.FlowVariable{
			Name:         resolvedToVar,
			DataType:     "String",
			IsCollection: true,
		})
		toValue = applyPackSendEmailToValue(action.To, resolvedToVar)
	}

	inputs := []models.FlowActionParameter{
		{Name: "templateId", Value: template.GeneratedTemplateID},
		{Name: constants.ConfigTo, Value: toValue},
	}
	if senderPolicy != nil {
		inputs = append(inputs, models.FlowActionParameter{Name: "from", Value: map[string]interface{}{"type": senderPolicy.Type}})
	}
	if len(action.Cc) > 0 {
		ccValues := make([]interface{}, 0, len(action.Cc))
		for _, cc := range action.Cc {
			ccValues = append(ccValues, cc)
		}
		inputs = append(inputs, models.FlowActionParameter{Name: constants.ConfigCc, Value: applyPackSingleOrSlice(ccValues)})
	}
	actions = append(actions, models.FlowActionCall{
		Name:            sendActionName,
		ActionName:      sendActionName,
		ActionType:      targetSendEmailAction,
		InputParameters: inputs,
		OutputParameters: []models.FlowActionParameter{
			{Name: "sentCount", Value: sentCountVar},
			{Name: "recipientResults", Value: recipientResultsVar},
			{Name: "messageId", Value: messageIDVar},
		},
	})
	variables = append(variables,
		models.FlowVariable{Name: sentCountVar, DataType: "Number"},
		models.FlowVariable{Name: recipientResultsVar, DataType: "String", IsCollection: true},
		models.FlowVariable{Name: messageIDVar, DataType: "String"},
	)

	flowPayload := generatedFlowPayload{
		Label:                 generatedLabel(flowDeveloperName),
		Description:           fmt.Sprintf("Generated Flow-native email notification from migration source %s.", action.SourceKey),
		ProcessType:           "AutoLaunchedFlow",
		StartElementReference: actions[0].Name,
		Variables:             variables,
		ActionCalls:           actions,
	}
	payload, err := marshalApplyPackJSON(flowPayload)
	if err != nil {
		return GeneratedFlowArtifact{}, template, []string{fmt.Sprintf("marshal generated flow payload: %v", err)}
	}

	return GeneratedFlowArtifact{
		SourceType:               action.SourceType,
		SourceKey:                action.SourceKey,
		FlowDeveloperName:        flowDeveloperName,
		SendEmailActionName:      sendActionName,
		TemplateID:               template.GeneratedTemplateID,
		TemplateDeveloper:        template.DeveloperName,
		To:                       append([]string(nil), action.To...),
		Cc:                       append([]string(nil), action.Cc...),
		SenderPolicy:             cloneSendEmailSenderPolicy(senderPolicy),
		ResolverInputFields:      resolverFields,
		RequiresRecordContext:    requiresRecordContext,
		RecordContextObject:      recordContextObject,
		RecordFieldDependencies:  recordFieldDependencies,
		RequiredPackDependencies: requiredPackDependencies,
		FilePath:                 filepath.ToSlash(filepath.Join("flows", flowDeveloperName+".flow.json")),
		payload:                  payload,
	}, template, nil
}

func (r *Report) generateEmailTemplateArtifact(templateID string) (GeneratedEmailTemplateArtifact, []string) {
	template, ok := r.applyPackTemplateByID(templateID)
	if !ok {
		return GeneratedEmailTemplateArtifact{}, []string{fmt.Sprintf("referenced template %q was not inventoried", templateID)}
	}
	reasons := r.emailTemplateDependencyBlockers(template.ID)
	for _, finding := range template.TokenFindings {
		if finding.Classification != "candidate" {
			reasons = append(reasons, fmt.Sprintf("template %s has unsupported token %s: %s", template.ID, finding.Token, finding.Message))
		}
	}
	if strings.TrimSpace(template.Subject) == "" {
		reasons = append(reasons, fmt.Sprintf("template %s subject is empty", template.ID))
	}
	if strings.TrimSpace(template.HTMLValue) == "" && strings.TrimSpace(template.Body) == "" {
		reasons = append(reasons, fmt.Sprintf("template %s has neither HTML nor text body", template.ID))
	}
	if len(reasons) > 0 {
		return GeneratedEmailTemplateArtifact{}, reasons
	}

	developerName := generatedEmailTemplateDeveloperName(template)
	description := fmt.Sprintf("Generated from migration source template %s.", template.ID)
	active := true
	payloadDef := generatedEmailTemplatePayload{
		Name:          generatedLabel(developerName),
		DeveloperName: developerName,
		Subject:       applyPackReplaceTemplateTokens(template.Subject, constants.FieldSysEmailTemplate_Subject, template.TokenFindings),
		HTMLValue:     applyPackReplaceTemplateTokens(template.HTMLValue, constants.FieldSysEmailTemplate_HTMLValue, template.TokenFindings),
		Body:          applyPackReplaceTemplateTokens(template.Body, constants.FieldSysEmailTemplate_Body, template.TokenFindings),
		Description:   &description,
		TemplateType:  "custom",
		IsActive:      &active,
	}
	for _, field := range []struct {
		name  string
		value string
	}{
		{name: constants.FieldSysEmailTemplate_Subject, value: payloadDef.Subject},
		{name: constants.FieldSysEmailTemplate_HTMLValue, value: payloadDef.HTMLValue},
		{name: constants.FieldSysEmailTemplate_Body, value: payloadDef.Body},
	} {
		if strings.Contains(field.value, "{!") {
			reasons = append(reasons, fmt.Sprintf("template %s %s still contains Salesforce merge syntax after conversion", template.ID, field.name))
		}
	}
	if len(reasons) > 0 {
		return GeneratedEmailTemplateArtifact{}, reasons
	}
	payload, err := marshalApplyPackJSON(payloadDef)
	if err != nil {
		return GeneratedEmailTemplateArtifact{}, []string{fmt.Sprintf("marshal generated email_template payload: %v", err)}
	}

	return GeneratedEmailTemplateArtifact{
		SourceTemplateID:        template.ID,
		SourceTemplateDeveloper: template.DeveloperName,
		DeveloperName:           developerName,
		GeneratedTemplateID:     deterministicSourceBackedEmailTemplateID(applyPackName, developerName),
		FilePath:                filepath.ToSlash(filepath.Join("email_templates", developerName+".json")),
		RequiresRecordContext:   templateHasRecordScopedTokens(template),
		payload:                 payload,
	}, nil
}

func (r *Report) applyPackTemplateByID(templateID string) (EmailTemplateInventory, bool) {
	templateID = strings.TrimSpace(templateID)
	if templateID == "" {
		return EmailTemplateInventory{}, false
	}
	if r.templatesByID != nil {
		if template, ok := r.templatesByID[templateID]; ok {
			return template, true
		}
	}
	for _, template := range r.ReferencedTemplates {
		if template.ID == templateID {
			return template, true
		}
	}
	return EmailTemplateInventory{}, false
}

func applyPackResolverFields(prerequisites []string) ([]string, []string) {
	var fields []string
	var reasons []string
	const prefix = "resolveEmailRecipients emails:Record."
	for _, prerequisite := range prerequisites {
		prerequisite = strings.TrimSpace(prerequisite)
		if prerequisite == "" {
			continue
		}
		if !strings.HasPrefix(prerequisite, prefix) {
			reasons = append(reasons, fmt.Sprintf("unsupported apply-pack prerequisite %q", prerequisite))
			continue
		}
		field := strings.TrimSpace(strings.TrimPrefix(prerequisite, prefix))
		if field == "" || strings.Contains(field, ".") {
			reasons = append(reasons, fmt.Sprintf("unsupported resolver field path %q", field))
			continue
		}
		fields = append(fields, field)
	}
	return uniqueSortedStrings(fields), uniqueSortedStrings(reasons)
}

func applyPackWorkflowAlertObject(action ProposedSendEmailAction) string {
	if action.SourceType != constants.TableWorkflowAlert {
		return ""
	}
	sourceKey := strings.TrimSpace(action.SourceKey)
	if sourceKey == "" {
		return ""
	}
	objectAPIName, _, ok := strings.Cut(sourceKey, ".")
	if !ok {
		return ""
	}
	return strings.TrimSpace(objectAPIName)
}

func cloneSendEmailSenderPolicy(policy *SendEmailSenderPolicy) *SendEmailSenderPolicy {
	if policy == nil {
		return nil
	}
	return &SendEmailSenderPolicy{Type: policy.Type}
}

func (r *Report) applyPackRecordFieldDependencies(templateID, recordContextObject string, resolverFields []string) ([]GeneratedRecordFieldDependency, []string) {
	recordContextObject = strings.TrimSpace(recordContextObject)
	fields := append([]string(nil), resolverFields...)
	if template, ok := r.applyPackTemplateByID(templateID); ok {
		fields = append(fields, applyPackRecordFieldsFromTemplate(template)...)
	}
	fields = uniqueSortedStrings(fields)
	var dependencies []GeneratedRecordFieldDependency
	var reasons []string
	for _, fieldAPIName := range fields {
		field, ok := r.lookupObjectFieldByAPIName(recordContextObject, fieldAPIName)
		if !ok || field.IsDeleted {
			reasons = append(reasons, fmt.Sprintf("record field dependency %s.%s is not present in inventory", recordContextObject, fieldAPIName))
			continue
		}
		ownerPack := strings.TrimSpace(field.OwnerPack)
		if ownerPack == "" {
			reasons = append(reasons, fmt.Sprintf("record field dependency %s.%s has no OwnerPack, so apply-pack cannot declare a pack dependency", recordContextObject, field.FieldAPIName))
			continue
		}
		dependencies = append(dependencies, GeneratedRecordFieldDependency{
			ObjectAPIName: recordContextObject,
			FieldAPIName:  field.FieldAPIName,
			OwnerPack:     ownerPack,
		})
	}
	sort.Slice(dependencies, func(i, j int) bool {
		if dependencies[i].ObjectAPIName != dependencies[j].ObjectAPIName {
			return dependencies[i].ObjectAPIName < dependencies[j].ObjectAPIName
		}
		if dependencies[i].FieldAPIName != dependencies[j].FieldAPIName {
			return dependencies[i].FieldAPIName < dependencies[j].FieldAPIName
		}
		return dependencies[i].OwnerPack < dependencies[j].OwnerPack
	})
	return dependencies, uniqueSortedStrings(reasons)
}

func applyPackRecordFieldsFromTemplate(template EmailTemplateInventory) []string {
	text := []string{
		applyPackReplaceTemplateTokens(template.Subject, constants.FieldSysEmailTemplate_Subject, template.TokenFindings),
		applyPackReplaceTemplateTokens(template.HTMLValue, constants.FieldSysEmailTemplate_HTMLValue, template.TokenFindings),
		applyPackReplaceTemplateTokens(template.Body, constants.FieldSysEmailTemplate_Body, template.TokenFindings),
	}
	var fields []string
	for _, value := range text {
		fields = append(fields, applyPackRecordFieldsFromNativeTemplateText(value)...)
	}
	return uniqueSortedStrings(fields)
}

func applyPackRecordFieldsFromNativeTemplateText(text string) []string {
	var fields []string
	for _, match := range nativeMergeTokenRE.FindAllStringSubmatch(text, -1) {
		if len(match) < 3 || strings.TrimSpace(match[1]) != "Record" {
			continue
		}
		suffix := strings.TrimSpace(match[2])
		if !strings.HasPrefix(suffix, ".") {
			continue
		}
		fieldAPIName := strings.TrimSpace(strings.TrimPrefix(suffix, "."))
		if fieldAPIName == "" || strings.Contains(fieldAPIName, ".") {
			continue
		}
		fields = append(fields, fieldAPIName)
	}
	return uniqueSortedStrings(fields)
}

func requiredPackDependenciesFromRecordFields(fields []GeneratedRecordFieldDependency) []string {
	seen := map[string]struct{}{}
	for _, field := range fields {
		ownerPack := strings.TrimSpace(field.OwnerPack)
		if ownerPack == "" || ownerPack == applyPackName {
			continue
		}
		seen[ownerPack] = struct{}{}
	}
	out := make([]string, 0, len(seen))
	for ownerPack := range seen {
		out = append(out, ownerPack)
	}
	sort.Strings(out)
	return out
}

func applyPackSendEmailToValue(literals []string, resolvedVar string) interface{} {
	values := make([]interface{}, 0, len(literals)+1)
	for _, literal := range literals {
		if strings.TrimSpace(literal) != "" {
			values = append(values, literal)
		}
	}
	if strings.TrimSpace(resolvedVar) != "" {
		values = append(values, map[string]interface{}{"elementReference": resolvedVar})
	}
	return applyPackSingleOrSlice(values)
}

func applyPackSingleOrSlice(values []interface{}) interface{} {
	if len(values) == 1 {
		return values[0]
	}
	return values
}

func applyPackReplaceTemplateTokens(text, fieldName string, findings []TemplateTokenFinding) string {
	if strings.TrimSpace(text) == "" {
		return text
	}
	replacements := map[string]string{}
	for _, finding := range findings {
		if finding.Field != fieldName || finding.Classification != "candidate" || finding.Replacement == "" {
			continue
		}
		replacements[finding.Token] = finding.Replacement
	}
	return salesforceMergeTokenRE.ReplaceAllStringFunc(text, func(raw string) string {
		matches := salesforceMergeTokenRE.FindStringSubmatch(raw)
		if len(matches) < 2 {
			return raw
		}
		token := "{!" + strings.TrimSpace(matches[1]) + "}"
		if replacement, ok := replacements[token]; ok {
			return replacement
		}
		return raw
	})
}

func generatedEmailTemplateDeveloperName(template EmailTemplateInventory) string {
	base := strings.TrimSpace(template.DeveloperName)
	if base == "" {
		base = strings.TrimSpace(template.Name)
	}
	if base == "" {
		base = strings.TrimSpace(template.ID)
	}
	return generatedDeveloperName("Migrated_", base, "Email_Template", template.ID)
}

func generatedFlowDeveloperName(action ProposedSendEmailAction) string {
	key := strings.TrimSpace(action.SourceKey)
	if key == "" {
		key = strings.TrimSpace(action.ActionName)
	}
	return generatedDeveloperName("Migrated_Email_", key, "Notification", action.SourceType+"\x00"+action.SourceKey)
}

func generatedActionName(name, fallback string) string {
	return safeIdentifier(name, fallback)
}

func generatedLabel(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return "Migrated Email Notification"
	}
	value = strings.ReplaceAll(value, "_", " ")
	words := strings.Fields(value)
	for i, word := range words {
		if len(word) > 1 {
			words[i] = strings.ToUpper(word[:1]) + word[1:]
		}
	}
	if len(words) == 0 {
		return "Migrated Email Notification"
	}
	return strings.Join(words, " ")
}

func generatedDeveloperName(prefix, value, fallback, hashSeed string) string {
	hash := shortStableHash(hashSeed)
	if len(hash) > applyPackHashSuffix {
		hash = hash[:applyPackHashSuffix]
	}
	suffix := "_" + hash
	limit := developerNameMaxLength - len(prefix) - len(suffix)
	if limit < 1 {
		limit = 1
	}
	return prefix + safeIdentifierWithMax(value, fallback, limit) + suffix
}

func safeIdentifier(value, fallback string) string {
	return safeIdentifierWithMax(value, fallback, developerNameMaxLength)
}

func safeIdentifierWithMax(value, fallback string, maxLen int) string {
	value = strings.TrimSpace(value)
	if value == "" {
		value = fallback
	}
	var b strings.Builder
	lastUnderscore := false
	for _, r := range value {
		ok := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')
		if ok {
			b.WriteRune(r)
			lastUnderscore = false
			continue
		}
		if !lastUnderscore {
			b.WriteByte('_')
			lastUnderscore = true
		}
	}
	out := strings.Trim(b.String(), "_")
	if out == "" {
		out = fallback
	}
	first := out[0]
	if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')) {
		out = "X_" + out
	}
	out = sanitizeDeveloperName(out)
	if out == "" {
		out = sanitizeDeveloperName(fallback)
	}
	if out == "" {
		out = defaultDeveloperName
	}
	if maxLen > 0 && len(out) > maxLen {
		out = strings.Trim(out[:maxLen], "_")
		if out == "" {
			out = defaultDeveloperName
			if len(out) > maxLen {
				out = out[:maxLen]
			}
		}
	}
	return out
}

func shortStableHash(value string) string {
	sum := sha1.Sum([]byte(value))
	return hex.EncodeToString(sum[:])[:8]
}

func deterministicSourceBackedEmailTemplateID(ownerPack, developerName string) string {
	sum := sha1.Sum([]byte(strings.ToLower(strings.TrimSpace(ownerPack)) + "\x00" + strings.ToLower(strings.TrimSpace(developerName))))
	return "email_template_" + hex.EncodeToString(sum[:])
}

func writeApplyPackFiles(plan *ApplyPackPlan, outputDir string) error {
	if plan == nil {
		return nil
	}
	root := filepath.Join(outputDir, plan.PackPath)
	tempRoot, err := os.MkdirTemp(outputDir, "."+filepath.Base(plan.PackPath)+"-tmp-*")
	if err != nil {
		return fmt.Errorf("create temporary generated apply-pack directory: %w", err)
	}
	tempReport, err := os.CreateTemp(outputDir, ".apply_pack_report-*.json")
	if err != nil {
		_ = os.RemoveAll(tempRoot)
		return fmt.Errorf("create temporary apply_pack_report.json: %w", err)
	}
	tempReportPath := tempReport.Name()
	if err := tempReport.Close(); err != nil {
		_ = os.RemoveAll(tempRoot)
		_ = os.Remove(tempReportPath)
		return fmt.Errorf("close temporary apply_pack_report.json: %w", err)
	}
	committed := false
	defer func() {
		if !committed {
			_ = os.RemoveAll(tempRoot)
			_ = os.Remove(tempReportPath)
		}
	}()
	for _, dir := range []string{
		tempRoot,
		filepath.Join(tempRoot, "flows"),
		filepath.Join(tempRoot, "email_templates"),
	} {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("create apply-pack directory %s: %w", dir, err)
		}
	}
	manifest, err := buildGeneratedPackManifest(plan)
	if err != nil {
		return err
	}
	if err := os.WriteFile(filepath.Join(tempRoot, "pack.json"), manifest, 0o644); err != nil {
		return fmt.Errorf("write generated pack manifest: %w", err)
	}
	for _, template := range plan.GeneratedEmailTemplates {
		if err := os.WriteFile(filepath.Join(tempRoot, filepath.FromSlash(template.FilePath)), template.payload, 0o644); err != nil {
			return fmt.Errorf("write generated email_template %s: %w", template.DeveloperName, err)
		}
	}
	for _, flow := range plan.GeneratedFlows {
		if err := os.WriteFile(filepath.Join(tempRoot, filepath.FromSlash(flow.FilePath)), flow.payload, 0o644); err != nil {
			return fmt.Errorf("write generated flow %s: %w", flow.FlowDeveloperName, err)
		}
	}
	reportBytes, err := json.MarshalIndent(plan, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal apply_pack_report.json: %w", err)
	}
	reportBytes = append(reportBytes, '\n')
	if err := os.WriteFile(tempReportPath, reportBytes, 0o644); err != nil {
		return fmt.Errorf("write temporary apply_pack_report.json: %w", err)
	}
	if err := os.RemoveAll(root); err != nil {
		return fmt.Errorf("clear generated apply-pack directory %s: %w", root, err)
	}
	if err := os.Rename(tempRoot, root); err != nil {
		return fmt.Errorf("publish generated apply-pack directory: %w", err)
	}
	reportPath := filepath.Join(outputDir, "apply_pack_report.json")
	if err := os.Rename(tempReportPath, reportPath); err != nil {
		_ = os.RemoveAll(root)
		return fmt.Errorf("publish apply_pack_report.json: %w", err)
	}
	committed = true
	return nil
}

func clearApplyPackFiles(plan *ApplyPackPlan, outputDir string) error {
	if plan == nil {
		return nil
	}
	root := filepath.Join(outputDir, plan.PackPath)
	if err := os.RemoveAll(root); err != nil {
		return fmt.Errorf("clear generated apply-pack directory %s: %w", root, err)
	}
	reportPath := filepath.Join(outputDir, "apply_pack_report.json")
	if err := os.Remove(reportPath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("clear apply_pack_report.json: %w", err)
	}
	return nil
}

func buildGeneratedPackManifest(plan *ApplyPackPlan) ([]byte, error) {
	manifest := generatedPackManifest{
		ManifestFormatVersion: "1",
		Name:                  applyPackName,
		DisplayName:           applyPackDisplayName,
		Category:              "app-pack",
		Version:               "0.1.0",
		PlatformCompatibility: generatedPackCompatibility{Min: "0.2.0", MaxExclusive: "0.3.0"},
		Dependencies:          buildGeneratedPackDependencies(plan),
		Extensions: generatedPackExtensions{FlowNativeEmailMigration: generatedPackProvenance{
			DesignAuthority: "docs/design/issue-2267-flow-native-email-notifications.md",
			SourceMode:      string(ModeApplyPack),
		}},
	}
	for _, template := range plan.GeneratedEmailTemplates {
		manifest.Components = append(manifest.Components, generatedPackComponent{
			Kind:          "email_template",
			BusinessKey:   template.DeveloperName,
			Manageability: "locked",
			Path:          template.FilePath,
		})
		manifest.Extensions.FlowNativeEmailMigration.Sources = append(manifest.Extensions.FlowNativeEmailMigration.Sources, generatedComponentProvenance{
			Kind:        "email_template",
			BusinessKey: template.DeveloperName,
			SourceType:  constants.TableEmailTemplate,
			SourceKey:   template.SourceTemplateID,
		})
	}
	for _, flow := range plan.GeneratedFlows {
		manifest.Components = append(manifest.Components, generatedPackComponent{
			Kind:          "standard_flow",
			BusinessKey:   flow.FlowDeveloperName,
			Manageability: "defaults_only",
			Path:          flow.FilePath,
			DependsOn: []generatedComponentRef{{
				Kind:        "email_template",
				BusinessKey: flow.TemplateDeveloper,
				Required:    true,
			}},
		})
		manifest.Extensions.FlowNativeEmailMigration.Sources = append(manifest.Extensions.FlowNativeEmailMigration.Sources, generatedComponentProvenance{
			Kind:        "standard_flow",
			BusinessKey: flow.FlowDeveloperName,
			SourceType:  flow.SourceType,
			SourceKey:   flow.SourceKey,
		})
	}
	sort.Slice(manifest.Extensions.FlowNativeEmailMigration.Sources, func(i, j int) bool {
		if manifest.Extensions.FlowNativeEmailMigration.Sources[i].Kind == manifest.Extensions.FlowNativeEmailMigration.Sources[j].Kind {
			return manifest.Extensions.FlowNativeEmailMigration.Sources[i].BusinessKey < manifest.Extensions.FlowNativeEmailMigration.Sources[j].BusinessKey
		}
		return manifest.Extensions.FlowNativeEmailMigration.Sources[i].Kind < manifest.Extensions.FlowNativeEmailMigration.Sources[j].Kind
	})
	return marshalApplyPackJSON(manifest)
}

func buildGeneratedPackDependencies(plan *ApplyPackPlan) []generatedPackDependency {
	if plan == nil {
		return nil
	}
	seen := map[string]struct{}{}
	for _, flow := range plan.GeneratedFlows {
		for _, ownerPack := range flow.RequiredPackDependencies {
			ownerPack = strings.TrimSpace(ownerPack)
			if ownerPack == "" || ownerPack == applyPackName {
				continue
			}
			seen[ownerPack] = struct{}{}
		}
	}
	out := make([]generatedPackDependency, 0, len(seen))
	for ownerPack := range seen {
		out = append(out, generatedPackDependency{Name: ownerPack})
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].Name < out[j].Name
	})
	return out
}

func marshalApplyPackJSON(value interface{}) ([]byte, error) {
	payload, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return nil, err
	}
	return append(payload, '\n'), nil
}
