package dialectsfcrm

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

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

// ApprovalProcessParentDisposition says whether the canonical executor owns the
// parent row or receives an already-written ownerless parent from metadata
// deploy. The pre-applied disposition is intentionally inert until the shared
// executor and host transaction adapter land.
type ApprovalProcessParentDisposition string

const (
	ApprovalProcessParentManagedUpsert       ApprovalProcessParentDisposition = "managed_upsert"
	ApprovalProcessParentPreAppliedOwnerless ApprovalProcessParentDisposition = "pre_applied_ownerless"
)

// ApprovalProcessProjectionRequest identifies one approval process, its parent
// disposition, and its already-decoded canonical metadata. RawMetadata is
// retained byte-for-byte so persistence and drift verification share one L1
// identity fact.
type ApprovalProcessProjectionRequest struct {
	ObjectAPIName      string
	DeveloperName      string
	ParentDisposition  ApprovalProcessParentDisposition
	OwnerPack          string
	PreAppliedParentID string
	RawMetadata        string
	Metadata           *ApprovalProcessMetadata
}

type ApprovalProcessBusinessKey struct {
	ObjectAPIName string
	DeveloperName string
}

type ApprovalProcessNodeBusinessKey struct {
	Process       ApprovalProcessBusinessKey
	DeveloperName string
}

// ApprovalProcessPersistencePlan is pure row intent for a later persistence
// provider. Existing ProcessNode rows matching Nodes' business keys stay live;
// all other live nodes for Parent are soft-deleted. ProcessAction rows are
// replaced as one set because their node binding is part of this plan.
type ApprovalProcessPersistencePlan struct {
	ParentDisposition        ApprovalProcessParentDisposition
	PreAppliedParentID       string
	Parent                   ApprovalProcessParentRowIntent
	Nodes                    []ApprovalProcessNodeRowIntent
	Actions                  []ApprovalProcessActionRowIntent
	SoftDeleteMissingNodes   bool
	ReplaceProcessActionRows bool
}

type ApprovalProcessParentRowIntent struct {
	BusinessKey              ApprovalProcessBusinessKey
	OwnerPack                string
	Name                     string
	Description              string
	State                    string
	EntryFilter              string
	LockType                 string
	FinalApprovalRecordLock  bool
	FinalRejectionRecordLock bool
	AllowRecall              bool
	IsActive                 bool
	ProcessOrder             *int
	RawMetadata              string
	MetadataSHA256           string
}

type ApprovalProcessNodeRowIntent struct {
	BusinessKey        ApprovalProcessNodeBusinessKey
	Name               string
	Description        string
	ApproverType       string
	ApproverReferences []string
	AllowComments      bool
	RequireComments    bool
	StepOrder          int
	RejectBehavior     string
}

type ApprovalProcessActionRowIntent struct {
	Name            string
	ProcessNode     *ApprovalProcessNodeBusinessKey
	ActionType      string
	ActionEvent     string
	FieldAPIName    string
	FieldValue      *string
	EmailTemplateID string
	APIName         string
	Order           int
}

// PlanApprovalProcessPersistence projects the canonical CRM metadata model;
// it does not decode XML/JSON and has no database or control-plane dependency.
func PlanApprovalProcessPersistence(request ApprovalProcessProjectionRequest) (ApprovalProcessPersistencePlan, error) {
	objectAPIName := strings.TrimSpace(request.ObjectAPIName)
	developerName := strings.TrimSpace(request.DeveloperName)
	ownerPack := request.OwnerPack
	if objectAPIName == "" || developerName == "" {
		return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process projection requires object API name and developer name")
	}
	if !metadataDeploySafeIdentifier.MatchString(objectAPIName) || !metadataDeploySafeIdentifier.MatchString(developerName) {
		return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process business key contains an invalid object or developer name")
	}
	if err := validateApprovalProcessParentDisposition(request.ParentDisposition, ownerPack, request.PreAppliedParentID); err != nil {
		return ApprovalProcessPersistencePlan{}, err
	}
	if request.Metadata == nil {
		return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process %s/%s projection requires decoded metadata", objectAPIName, developerName)
	}
	if strings.TrimSpace(request.RawMetadata) == "" {
		return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process %s/%s projection requires raw metadata", objectAPIName, developerName)
	}

	key := ApprovalProcessBusinessKey{ObjectAPIName: objectAPIName, DeveloperName: developerName}
	metadataHash := sha256.Sum256([]byte(request.RawMetadata))
	name := strings.TrimSpace(request.Metadata.Label)
	if name == "" {
		name = developerName
	}
	plan := ApprovalProcessPersistencePlan{
		ParentDisposition:  request.ParentDisposition,
		PreAppliedParentID: request.PreAppliedParentID,
		Parent: ApprovalProcessParentRowIntent{
			BusinessKey:              key,
			OwnerPack:                ownerPack,
			Name:                     name,
			Description:              strings.TrimSpace(request.Metadata.Description),
			State:                    approvalProcessState(request.Metadata.Active),
			EntryFilter:              approvalProcessProjectionEntryFilter(request.Metadata.EntryCriteria),
			LockType:                 strings.TrimSpace(request.Metadata.RecordEditability),
			FinalApprovalRecordLock:  request.Metadata.FinalApprovalRecordLock,
			FinalRejectionRecordLock: request.Metadata.FinalRejectionRecordLock,
			AllowRecall:              request.Metadata.EffectiveAllowRecall(),
			IsActive:                 request.Metadata.Active,
			ProcessOrder:             cloneApprovalProcessOrder(request.Metadata.ProcessOrder),
			RawMetadata:              request.RawMetadata,
			MetadataSHA256:           hex.EncodeToString(metadataHash[:]),
		},
		SoftDeleteMissingNodes:   true,
		ReplaceProcessActionRows: true,
	}

	seenNodes := map[string]int{}
	for index, step := range request.Metadata.Steps {
		stepDeveloperName := strings.TrimSpace(step.Name)
		if stepDeveloperName == "" {
			return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process %s/%s step %d requires a name", objectAPIName, developerName, index+1)
		}
		if !metadataDeploySafeIdentifier.MatchString(stepDeveloperName) {
			return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process %s/%s step %d has an invalid developer name %q", objectAPIName, developerName, index+1, stepDeveloperName)
		}
		folded := strings.ToLower(stepDeveloperName)
		if previous, exists := seenNodes[folded]; exists {
			return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process %s/%s step %d name %q duplicates step %d", objectAPIName, developerName, index+1, stepDeveloperName, previous)
		}
		seenNodes[folded] = index + 1
		node, err := projectApprovalProcessNode(key, step, index+1)
		if err != nil {
			return ApprovalProcessPersistencePlan{}, fmt.Errorf("approval process %s/%s step %d: %w", objectAPIName, developerName, index+1, err)
		}
		plan.Nodes = append(plan.Nodes, node)
		plan.Actions, err = appendApprovalProcessActionIntents(plan.Actions, key, stepDeveloperName, constants.ApprovalActionEventStepApproval, step.StepApprovalActions)
		if err != nil {
			return ApprovalProcessPersistencePlan{}, err
		}
		plan.Actions, err = appendApprovalProcessActionIntents(plan.Actions, key, stepDeveloperName, constants.ApprovalActionEventStepRejection, step.StepRejectionActions)
		if err != nil {
			return ApprovalProcessPersistencePlan{}, err
		}
	}

	for _, group := range []struct {
		event   string
		actions []ApprovalProcessAction
	}{
		{constants.ApprovalActionEventSubmit, request.Metadata.InitialSubmissionActions},
		{constants.ApprovalActionEventStepApproval, request.Metadata.StepApprovalActions},
		{constants.ApprovalActionEventFinalApproval, request.Metadata.FinalApprovalActions},
		{constants.ApprovalActionEventFinalRejection, request.Metadata.FinalRejectionActions},
		{constants.ApprovalActionEventRecall, request.Metadata.RecallActions},
		{constants.ApprovalActionEventReassign, request.Metadata.ReassignActions},
	} {
		var err error
		plan.Actions, err = appendApprovalProcessActionIntents(plan.Actions, key, "", group.event, group.actions)
		if err != nil {
			return ApprovalProcessPersistencePlan{}, err
		}
	}
	sortApprovalProcessActionIntents(plan.Actions)
	if err := plan.Validate(); err != nil {
		return ApprovalProcessPersistencePlan{}, err
	}
	return plan, nil
}

func validateApprovalProcessParentDisposition(disposition ApprovalProcessParentDisposition, ownerPack, preAppliedParentID string) error {
	switch disposition {
	case ApprovalProcessParentManagedUpsert:
		if ownerPack == "" || strings.TrimSpace(ownerPack) != ownerPack {
			return fmt.Errorf("managed approval process projection requires a canonical owner pack")
		}
		if preAppliedParentID != "" {
			return fmt.Errorf("managed approval process projection must not declare a pre-applied parent ID")
		}
	case ApprovalProcessParentPreAppliedOwnerless:
		if ownerPack != "" {
			return fmt.Errorf("pre-applied ownerless approval process projection must not declare an owner pack")
		}
		if preAppliedParentID == "" || strings.TrimSpace(preAppliedParentID) != preAppliedParentID {
			return fmt.Errorf("pre-applied ownerless approval process projection requires a canonical parent ID")
		}
	default:
		return fmt.Errorf("approval process projection has unsupported parent disposition %q", disposition)
	}
	return nil
}

func projectApprovalProcessNode(process ApprovalProcessBusinessKey, step ApprovalProcessStep, order int) (ApprovalProcessNodeRowIntent, error) {
	name := strings.TrimSpace(step.Label)
	if name == "" {
		name = strings.TrimSpace(step.Name)
	}
	allowComments := true
	if step.AllowComments != nil {
		allowComments = *step.AllowComments
	}
	requireComments := false
	if step.RequireComments != nil {
		requireComments = *step.RequireComments
	}
	node := ApprovalProcessNodeRowIntent{
		BusinessKey:     ApprovalProcessNodeBusinessKey{Process: process, DeveloperName: strings.TrimSpace(step.Name)},
		Name:            name,
		Description:     strings.TrimSpace(step.Description),
		AllowComments:   allowComments,
		RequireComments: requireComments,
		StepOrder:       order,
		RejectBehavior:  constants.NormalizeApprovalProcessNodeRejectBehavior(step.RejectBehavior),
	}
	if len(step.Approvers) > 0 {
		node.ApproverType = constants.NormalizeApprovalApproverType(step.Approvers[0].Type)
		if !approvalProcessManagedApproverType(node.ApproverType) {
			return ApprovalProcessNodeRowIntent{}, fmt.Errorf("unsupported approver type %q", step.Approvers[0].Type)
		}
		if len(step.Approvers) > 1 && node.ApproverType != constants.ApprovalApproverTypeUser {
			return ApprovalProcessNodeRowIntent{}, fmt.Errorf("multiple approvers are representable only for user approvers")
		}
		for _, approver := range step.Approvers {
			if constants.NormalizeApprovalApproverType(approver.Type) != node.ApproverType {
				return ApprovalProcessNodeRowIntent{}, fmt.Errorf("multiple approvers must use one approver type")
			}
			reference := strings.TrimSpace(approver.Name)
			switch node.ApproverType {
			case constants.ApprovalApproverTypeUser, constants.ApprovalApproverTypeQueue,
				constants.ApprovalApproverTypeRole, constants.ApprovalApproverTypeRelatedUserField:
				if reference == "" {
					return ApprovalProcessNodeRowIntent{}, fmt.Errorf("approver type %q requires a reference", node.ApproverType)
				}
				node.ApproverReferences = append(node.ApproverReferences, reference)
			case constants.ApprovalApproverTypeHierarchy, constants.ApprovalApproverTypeUserHierarchyField,
				constants.ApprovalApproverTypePreviousApprover, constants.ApprovalApproverTypeAdhoc:
				if reference != "" {
					return ApprovalProcessNodeRowIntent{}, fmt.Errorf("approver type %q must not carry a reference", node.ApproverType)
				}
			}
		}
	}
	return node, nil
}

func approvalProcessManagedApproverType(value string) bool {
	switch value {
	case constants.ApprovalApproverTypeUser, constants.ApprovalApproverTypeQueue,
		constants.ApprovalApproverTypeRole, constants.ApprovalApproverTypeHierarchy,
		constants.ApprovalApproverTypeUserHierarchyField, constants.ApprovalApproverTypeRelatedUserField,
		constants.ApprovalApproverTypePreviousApprover, constants.ApprovalApproverTypeAdhoc:
		return true
	default:
		return false
	}
}

func appendApprovalProcessActionIntents(out []ApprovalProcessActionRowIntent, process ApprovalProcessBusinessKey, nodeName, event string, actions []ApprovalProcessAction) ([]ApprovalProcessActionRowIntent, error) {
	for order, action := range actions {
		actionType := normalizeApprovalActionType(action.Type)
		apiName := strings.TrimSpace(action.APIName)
		switch actionType {
		case constants.ProcessActionTypeFieldUpdate, constants.ProcessActionTypeAlert, constants.ProcessActionTypeFlow:
		default:
			return nil, NewValidationError(constants.FieldProcessAction_ActionType, fmt.Sprintf("unsupported approval action type %q", action.Type))
		}
		authoredBody := action.bodyAuthorship
		authoredBody.fieldAPIName = authoredBody.fieldAPIName || strings.TrimSpace(action.FieldAPIName) != ""
		authoredBody.fieldValue = authoredBody.fieldValue || action.HasFieldValue || action.FieldValue != ""
		authoredBody.emailTemplateID = authoredBody.emailTemplateID || strings.TrimSpace(action.EmailTemplateID) != ""
		authoredBody.apiName = authoredBody.apiName || apiName != ""
		if action.unknownBodyAuthored {
			return nil, NewValidationError(constants.FieldProcessAction_ActionType, "approval action payload contains unsupported fields")
		}
		if field := incompatibleApprovalProcessActionBodyField(actionType, authoredBody); field != "" {
			return nil, NewValidationError(field, fmt.Sprintf("%s is not valid for approval %s actions", field, actionType))
		}
		if actionType == constants.ProcessActionTypeAlert {
			return nil, NewValidationError("ActionType", "approval Alert actions are retired after the Flow-native email notification cutover; bind approval notifications to a Flow action with apiName instead of authoring WorkflowAlert metadata")
		}
		if actionType == constants.ProcessActionTypeFlow && apiName == "" {
			return nil, NewValidationError(constants.FieldProcessAction_APIName, "ApiName is required for approval Flow actions")
		}
		name := strings.TrimSpace(action.Name)
		if name == "" {
			name = fmt.Sprintf("#%d", order+1)
		}
		intent := ApprovalProcessActionRowIntent{
			Name:            name,
			ActionType:      actionType,
			ActionEvent:     event,
			FieldAPIName:    strings.TrimSpace(action.FieldAPIName),
			EmailTemplateID: strings.TrimSpace(action.EmailTemplateID),
			APIName:         apiName,
			Order:           order,
		}
		if nodeName != "" {
			key := ApprovalProcessNodeBusinessKey{Process: process, DeveloperName: nodeName}
			intent.ProcessNode = &key
		}
		if action.HasFieldValue || action.FieldValue != "" {
			fieldValue := action.FieldValue
			intent.FieldValue = &fieldValue
		}
		out = append(out, intent)
	}
	return out, nil
}

func incompatibleApprovalProcessActionBodyField(actionType string, authored approvalProcessActionBodyAuthorship) string {
	switch actionType {
	case constants.ProcessActionTypeFieldUpdate:
		if authored.emailTemplateID {
			return constants.FieldProcessAction_EmailTemplateID
		}
		if authored.apiName {
			return constants.FieldProcessAction_APIName
		}
	case constants.ProcessActionTypeFlow:
		if authored.fieldAPIName {
			return constants.FieldProcessAction_FieldAPIName
		}
		if authored.fieldValue {
			return constants.FieldProcessAction_FieldValue
		}
		if authored.emailTemplateID {
			return constants.FieldProcessAction_EmailTemplateID
		}
	case constants.ProcessActionTypeAlert:
		if authored.fieldAPIName {
			return constants.FieldProcessAction_FieldAPIName
		}
		if authored.fieldValue {
			return constants.FieldProcessAction_FieldValue
		}
		if authored.apiName {
			return constants.FieldProcessAction_APIName
		}
	}
	return ""
}

func (plan ApprovalProcessPersistencePlan) Validate() error {
	if err := validateApprovalProcessParentDisposition(plan.ParentDisposition, plan.Parent.OwnerPack, plan.PreAppliedParentID); err != nil {
		return err
	}
	if !plan.SoftDeleteMissingNodes || !plan.ReplaceProcessActionRows {
		return fmt.Errorf("approval process persistence plan must soft-delete missing nodes and replace process action rows")
	}
	if !metadataDeploySafeIdentifier.MatchString(plan.Parent.BusinessKey.ObjectAPIName) || !metadataDeploySafeIdentifier.MatchString(plan.Parent.BusinessKey.DeveloperName) {
		return fmt.Errorf("approval process parent has an invalid business key")
	}
	liveNodes := make(map[ApprovalProcessNodeBusinessKey]struct{}, len(plan.Nodes))
	seenNodeNames := make(map[string]struct{}, len(plan.Nodes))
	for _, node := range plan.Nodes {
		if node.BusinessKey.Process != plan.Parent.BusinessKey || !metadataDeploySafeIdentifier.MatchString(node.BusinessKey.DeveloperName) {
			return fmt.Errorf("approval process node has an invalid business key")
		}
		folded := strings.ToLower(node.BusinessKey.DeveloperName)
		if _, exists := seenNodeNames[folded]; exists {
			return fmt.Errorf("approval process node business key %s is duplicated", node.BusinessKey.DeveloperName)
		}
		if !node.AllowComments && node.RequireComments {
			return fmt.Errorf("approval process node %s cannot require disallowed comments", node.BusinessKey.DeveloperName)
		}
		if node.ApproverType == "" {
			if len(node.ApproverReferences) != 0 {
				return fmt.Errorf("approval process node %s has an approver reference without a type", node.BusinessKey.DeveloperName)
			}
		} else if !approvalProcessManagedApproverType(node.ApproverType) {
			return fmt.Errorf("approval process node %s has unsupported approver type %q", node.BusinessKey.DeveloperName, node.ApproverType)
		} else {
			for _, reference := range node.ApproverReferences {
				if reference == "" || strings.TrimSpace(reference) != reference {
					return fmt.Errorf("approval process node %s approver type %q requires canonical references", node.BusinessKey.DeveloperName, node.ApproverType)
				}
			}
			switch node.ApproverType {
			case constants.ApprovalApproverTypeUser:
				if len(node.ApproverReferences) == 0 {
					return fmt.Errorf("approval process node %s user approver requires at least one reference", node.BusinessKey.DeveloperName)
				}
			case constants.ApprovalApproverTypeQueue, constants.ApprovalApproverTypeRole, constants.ApprovalApproverTypeRelatedUserField:
				if len(node.ApproverReferences) != 1 {
					return fmt.Errorf("approval process node %s approver type %q requires exactly one reference", node.BusinessKey.DeveloperName, node.ApproverType)
				}
			case constants.ApprovalApproverTypeHierarchy, constants.ApprovalApproverTypeUserHierarchyField,
				constants.ApprovalApproverTypePreviousApprover, constants.ApprovalApproverTypeAdhoc:
				if len(node.ApproverReferences) != 0 {
					return fmt.Errorf("approval process node %s approver type %q must not carry references", node.BusinessKey.DeveloperName, node.ApproverType)
				}
			}
		}
		liveNodes[node.BusinessKey] = struct{}{}
		seenNodeNames[folded] = struct{}{}
	}
	for _, action := range plan.Actions {
		if action.ProcessNode == nil {
			continue
		}
		if _, exists := liveNodes[*action.ProcessNode]; !exists {
			return fmt.Errorf("approval process action %q references missing ProcessNode %s", action.Name, action.ProcessNode.DeveloperName)
		}
	}
	return nil
}

func approvalProcessProjectionEntryFilter(criteria *ApprovalProcessCriteria) string {
	if criteria == nil {
		return ""
	}
	if formula := strings.TrimSpace(criteria.Formula); formula != "" {
		payload, _ := json.Marshal(struct {
			Formula string `json:"formula"`
		}{formula})
		return string(payload)
	}
	type criteriaItem struct {
		Field     string `json:"field"`
		Operation string `json:"operation"`
		Value     string `json:"value"`
	}
	items := make([]criteriaItem, 0, len(criteria.CriteriaItems))
	for _, item := range criteria.CriteriaItems {
		items = append(items, criteriaItem{Field: strings.TrimSpace(item.Field), Operation: strings.TrimSpace(item.Operation), Value: strings.TrimSpace(item.Value)})
	}
	booleanFilter := strings.TrimSpace(criteria.BooleanFilter)
	if booleanFilter == "" && len(items) == 0 {
		return ""
	}
	payload, _ := json.Marshal(struct {
		BooleanFilter string         `json:"booleanFilter,omitempty"`
		CriteriaItems []criteriaItem `json:"criteriaItems,omitempty"`
	}{booleanFilter, items})
	return string(payload)
}

func cloneApprovalProcessOrder(order *int) *int {
	if order == nil {
		return nil
	}
	copy := *order
	return &copy
}

func sortApprovalProcessActionIntents(actions []ApprovalProcessActionRowIntent) {
	sort.Slice(actions, func(i, j int) bool {
		leftNode, rightNode := "", ""
		if actions[i].ProcessNode != nil {
			leftNode = actions[i].ProcessNode.DeveloperName
		}
		if actions[j].ProcessNode != nil {
			rightNode = actions[j].ProcessNode.DeveloperName
		}
		if leftNode != rightNode {
			return leftNode < rightNode
		}
		if actions[i].ActionEvent != actions[j].ActionEvent {
			return actions[i].ActionEvent < actions[j].ActionEvent
		}
		if actions[i].Order != actions[j].Order {
			return actions[i].Order < actions[j].Order
		}
		return actions[i].Name < actions[j].Name
	})
}
