package automation

import (
	"fmt"
	"strings"

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

// canonicalizeMultiPicklist normalizes any multi-picklist value into the
// canonical []string token form regardless of source shape. This is the
// single entry point for multi-picklist value-shape handling on the
// field-aware comparison path; comparators (includesForField,
// excludesForField) consume []string only and never re-inspect the raw
// `any`.
//
// The `field` parameter, when non-nil, gates the canonicalizer to the
// multi-picklist field type. Callers that already know the field is a
// multi-picklist (from upstream metadata threading) pass it through so a
// caller bug — feeding a Text or Number field through this path —
// surfaces as an explicit error rather than silently coercing the value
// to a token list. When `field` is nil the canonicalizer trusts the
// caller and proceeds.
//
// The value-shape handling intentionally reuses the persistence layer's
// metadata-aware multi-picklist normalizer. Bare strings are treated as
// author-authored values, so declared option literals such as `["Others"]`
// win over compatibility JSON decoding. Authoritative storage shapes
// (NormalizedMultiPicklistJSON and []byte driver round-trips) are decoded
// as storage envelopes. The wrapper exists to (a) give the function a
// name that matches its role at the comparator boundary, (b) thread field
// metadata for future routing rules (e.g., rejecting active-computed
// multi-picklists for the #903 substrate), and (c) provide an error path
// for caller bugs.
//
// Returns ([], nil) for nil, empty string, or any shape that
// canonicalizes to an empty set; (nil, error) only when `field` is
// non-nil and not FieldTypeMultiPicklist.
func canonicalizeMultiPicklist(value any, field *models.FieldMetadata) ([]string, error) {
	if field != nil && constants.NormalizeFieldType(string(field.Type)) != constants.FieldTypeMultiPicklist {
		return nil, fmt.Errorf(
			"canonicalizeMultiPicklist: field %q is type %s, not multi-picklist",
			field.APIName,
			field.Type,
		)
	}
	return multiPicklistWireItems(canonicalMultiPicklistStorageValue(value, field), field), nil
}

func canonicalizeMultiPicklistAuthoredLiteral(value any, field *models.FieldMetadata) ([]string, error) {
	if field != nil && constants.NormalizeFieldType(string(field.Type)) != constants.FieldTypeMultiPicklist {
		return nil, fmt.Errorf(
			"canonicalizeMultiPicklistAuthoredLiteral: field %q is type %s, not multi-picklist",
			field.APIName,
			field.Type,
		)
	}
	switch typed := value.(type) {
	case nil:
		return nil, nil
	case string:
		return dedupeTrimmed(strings.Split(typed, ";")), nil
	case []string:
		return dedupeTrimmed(typed), nil
	case []interface{}:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			if item == nil {
				continue
			}
			out = append(out, fmt.Sprintf("%v", item))
		}
		return dedupeTrimmed(out), nil
	case []byte, schemautil.NormalizedMultiPicklistJSON:
		return multiPicklistWireItems(typed, field), nil
	default:
		return dedupeTrimmed(strings.Split(fmt.Sprintf("%v", value), ";")), nil
	}
}

func canonicalMultiPicklistStorageValue(value any, field *models.FieldMetadata) any {
	switch value.(type) {
	case nil:
		return nil
	case []byte, schemautil.NormalizedMultiPicklistJSON:
		return value
	default:
		return schemautil.NormalizeMultiPicklistStorageForField(value, field)
	}
}

func multiPicklistWireItems(value any, field *models.FieldMetadata) []string {
	if value == nil {
		return nil
	}
	return dedupeTrimmed(strings.Split(schemautil.MultiPicklistWireValueForField(value, field), ";"))
}

// multiPicklistSetsEqual treats two multi-picklist values as equal when the
// set of selected items matches, regardless of the serialization the caller
// happens to hold. Needed because the records layer stores JSON arrays in
// TiDB, the FromStorageRecord layer returns a semicolon-joined scalar for
// Salesforce wire-format compatibility, and flow-input wiring can produce
// `[]interface{}` or `[]string` directly. A byte-literal comparison would
// flag semantically-identical values as changed and, under an AfterSave flow
// that writes the record's own fields, re-opens the issue #1683 self-update
// loop. Mirrors the records-layer helper introduced in PR #1684.
func multiPicklistSetsEqual(a, b interface{}) bool {
	return schemautil.CanonicalMultiPicklistSetKey(a) == schemautil.CanonicalMultiPicklistSetKey(b)
}

func dedupeTrimmed(in []string) []string {
	if len(in) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(in))
	out := make([]string, 0, len(in))
	for _, s := range in {
		t := strings.TrimSpace(s)
		if t == "" {
			continue
		}
		if _, dup := seen[t]; dup {
			continue
		}
		seen[t] = struct{}{}
		out = append(out, t)
	}
	return out
}

// picklistAwareEqual implements generic Flow EqualTo with a multi-picklist
// storage-shape adapter. It is the comparison used by Decision-condition
// EqualTo / NotEqualTo, where the evaluator does not have field metadata
// and therefore cannot prove whether the operands are multi-picklist or
// ordinary Text/ID/Formula values.
//
// Strategy:
//
//   - If at least one side is in an unambiguous multi-picklist *storage*
//     shape (a Go-typed `[]interface{}` or `[]string`), fall through to
//     schemautil.CanonicalMultiPicklistSetKey equality. This covers the
//     original #229 case where inline flow-input wiring or unmarshalled JSON
//     storage produces `[]interface{}{"Meeting"}` and the same set
//     arrives as `"Meeting"` on the wire — byte-equal compare would
//     falsely reject them.
//   - Otherwise both sides are plain scalars (string, number, bool, nil,
//     []byte). Use strict generic equality via `fmt.Sprintf("%v", ...)`.
//     Crucially, we do NOT canonicalise plain scalars — including strings
//     that happen to start with `[` — because canonical set-key comparison
//     trims whitespace, splits on `;`, dedupes tokens, conflates
//     nil/blank, and unmarshals JSON arrays. Each of those is wrong for
//     Text/ID/Formula EqualTo where the scalar may legitimately be a
//     JSON-shaped string (`"[\"B\",\"A\"]"`) without being a
//     multi-picklist storage shape.
//
// The remaining gap — two `;`-joined plain scalars whose set is identical
// but whose byte order differs (`"Email;Meeting"` vs `"Meeting;Email"`),
// or two JSON-array-shaped plain strings with different element order —
// can only be resolved with field metadata that proves the field is
// FieldTypeMultiPicklist. Decision conditions in Flow can reference any
// variable (record field, formula result, $User reference, loop variable,
// inline literal), so surfacing per-condition fieldMeta into the generic
// evaluator is a separate, larger refactor outside the scope of #229.
// Until then, all such cases compare byte-literal — the conservative
// pre-#229 behaviour, fail-closed, never produces a false positive.
//
// See codex review on PR #263 for the false-positive set this guard
// closes: `"A;B"` ≠ `"B;A"`, `" A "` ≠ `"A"`, `"a;a"` ≠ `"a"`, `nil` ≠
// `""`, and `"[\"B\",\"A\"]"` ≠ `"[\"A\",\"B\"]"` (round-2 codex).
func picklistAwareEqual(a, b interface{}) bool {
	if isMultiPicklistShape(a) || isMultiPicklistShape(b) {
		return schemautil.CanonicalMultiPicklistSetKey(a) == schemautil.CanonicalMultiPicklistSetKey(b)
	}
	return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
}

// isMultiPicklistShape reports whether v is unambiguously a multi-picklist
// *storage* shape — i.e., a Go-typed slice that only the JSON-array storage
// path or inline flow-input wiring would produce. Plain scalars (string,
// number, bool, nil, []byte) return false so generic EqualTo preserves
// strict byte equality even when the scalar happens to look like a JSON
// array (e.g., a Text/Formula value `"[\"B\",\"A\"]"`); without field
// metadata we cannot prove such a string is multi-picklist storage rather
// than a user-typed value, so we fail-closed to byte equality.
//
// Codex Round-2 (PR #263) flagged that allowing string-with-`[` prefix or
// `[]byte`-with-`[` prefix into this gate would still canonicalise
// plain-string operands shaped like JSON arrays — `["B","A"]` would
// compare equal to `["A","B"]` for ordinary Text fields. Dropping those
// two branches closes the regression class.
func isMultiPicklistShape(v interface{}) bool {
	switch v.(type) {
	case []interface{}, []string:
		return true
	}
	return false
}
