package repository

import (
	"regexp"
	"strings"

	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/models"
)

// validateActionConfig checks a non-email (action/wait) node's typed config
// before it is persisted. This is a shape gate only; the kind↔action pairing and
// cross-step routing targets are handled elsewhere (the canvas sets kind+action
// together, and dangling routes resolve to "stop" at schedule time).
func validateActionConfig(a *models.ActionConfig) *errx.Error {
	if a == nil {
		return nil
	}
	switch a.Type {
	case "wait", "add_tag", "remove_tag", "add_to_segment", "remove_from_segment",
		"label_email", "unsubscribe",
		"notify", "create_task", "create_deal", "move_deal_stage",
		"run_automation", "fire_event", "end":
		// Type must be known. Sub-config (wait minutes, tag, event name, HTTP
		// request) is filled in the editor; an unconfigured node is a harmless
		// no-op at send time, so we don't block creating a draft node here. Keep
		// this list in sync with models.ActionConfig.Type and the task executor's
		// switch (internal/tasks/campaign_task.go, advanced/reply_actions.go).
		return nil
	case "switch":
		// A draft switch (no cases / no decider yet) is a no-op like other
		// unconfigured nodes; bound the surface of what IS set. Cases become
		// draggable dots on the node and route via ai_label branch conditions,
		// so their names share the branch-label bound.
		switch a.SwitchOn {
		case "", "ai", "value":
		default:
			return errx.ErrSequenceAction
		}
		if len(a.AIInstruction) > maxAIStepInstruction || len(a.SwitchValue) > maxSwitchValue {
			return errx.ErrSequenceAction
		}
		if len(a.SwitchCases) > maxSwitchCases {
			return errx.ErrSequenceAction
		}
		seen := map[string]bool{}
		for _, c := range a.SwitchCases {
			name := strings.ToLower(strings.TrimSpace(c))
			if name == "" || len(c) > maxAIStepName || seen[name] {
				return errx.ErrSequenceAction
			}
			seen[name] = true
			// A "/pattern/" case is a regex in value mode: reject patterns that
			// don't compile so a broken case never sits silently unmatched.
			if len(name) >= 3 && strings.HasPrefix(name, "/") && strings.HasSuffix(name, "/") {
				if _, err := regexp.Compile("(?i)" + name[1:len(name)-1]); err != nil {
					return errx.ErrSequenceAction
				}
			}
		}
		return nil
	case "ai_step":
		// AI agent step: an instruction plus a guarded allowlist of reversible
		// actions the model may call. A draft (no instruction / no actions) is a
		// harmless no-op like other unconfigured nodes, so bound only what IS set.
		if len(a.AIInstruction) > maxAIStepInstruction {
			return errx.ErrSequenceAction
		}
		if len(a.AIAllowedActions) > maxAIAllowedActions {
			return errx.ErrSequenceAction
		}
		for _, act := range a.AIAllowedActions {
			if !models.IsReversibleCampaignAction(strings.TrimSpace(act)) {
				return errx.ErrSequenceAction
			}
		}
		if len(a.AIAddTags) > maxAITagPool || len(a.AIRemoveTags) > maxAITagPool || len(a.AILabels) > maxAITagPool {
			return errx.ErrSequenceAction
		}
		pools := append([]models.AITagRef{}, a.AIAddTags...)
		pools = append(pools, a.AIRemoveTags...)
		pools = append(pools, a.AILabels...)
		for _, r := range pools {
			if strings.TrimSpace(r.ID) == "" || strings.TrimSpace(r.Name) == "" || len(r.Name) > maxAIStepName {
				return errx.ErrSequenceAction
			}
		}
		return nil
	default:
		return errx.ErrSequenceAction
	}
}

// Switch step bounds. Case names live on the node config AND on the ai_label
// branch conditions (branch_conditions.go), so they share maxAIStepName.
const (
	maxAIStepInstruction = 4000
	maxAIStepName        = 80
	maxSwitchValue       = 500
	maxSwitchCases       = 20
	maxAIAllowedActions  = 12
	maxAITagPool         = 40
)
