// Package connector holds the pure, neutral data contracts for connector field
// mappings and the PushToConnector durable-metadata contract. It depends only
// on the Go standard library so it can be imported by any module without
// creating a dependency cycle back into the backend.
//
// These symbols were relocated verbatim out of internal/bootstrap as the first
// (AU) slice of the bootstrap-decoupling campaign. Behavior — in particular the
// durable JSON projection produced by ConnectorFieldMappingInput.MarshalJSON —
// is byte-for-byte identical to the bootstrap originals.
package connector

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strings"
)

// Direction constants for connector field mapping rows.
// 'legacy' is intentionally absent — it is a sentinel for pre-connector rows
// and must never appear in pack-managed connector input.
const (
	DirectionInbound       = "inbound"
	DirectionOutbound      = "outbound"
	DirectionBidirectional = "bidirectional"
)

// ConflictPolicy constants for connector field mapping rows.
// An empty string is valid and means "runtime default".
const (
	ConflictPolicyOverwrite = "overwrite"
	ConflictPolicySkipIfSet = "skip_if_set"
	ConflictPolicyError     = "error"
)

// OnUnmappedValue policy constants for connector field mapping rows.
// Controls runtime behavior when a ValueMap has no translation for a source
// value. An empty string means "error" (fail-closed default).
const (
	OnUnmappedValueError       = "error"
	OnUnmappedValueSkip        = "skip"
	OnUnmappedValuePassthrough = "passthrough"
)

// ConnectorFieldMappingInput is the input struct for pack-managed connector rows.
// The caller never supplies Id; it is derived deterministically from the
// canonical business key by UpsertConnectorFieldMapping.
type ConnectorFieldMappingInput struct {
	Source              string
	SourceObject        string
	TargetObject        string
	ActionType          string
	SourceField         string
	TargetField         string
	Direction           string
	TransformExpression string
	ConflictPolicy      string
	DefaultValue        string
	ValueMap            string
	OnUnmappedValue     string
	SortOrder           int
	ProjectionOnly      bool
}

// MarshalJSON keeps ConnectorFieldMappingInput's durable JSON projection stable
// for existing connector push fingerprints: empty ValueMap means "no translation"
// and must serialize like pre-ValueMap rows. Non-empty ValueMap participates in
// the projection so mappings with declared translations get a distinct hash.
func (row ConnectorFieldMappingInput) MarshalJSON() ([]byte, error) {
	type connectorFieldMappingInputJSON struct {
		Source              string `json:"Source"`
		SourceObject        string `json:"SourceObject"`
		TargetObject        string `json:"TargetObject"`
		ActionType          string `json:"ActionType"`
		SourceField         string `json:"SourceField"`
		TargetField         string `json:"TargetField"`
		Direction           string `json:"Direction"`
		TransformExpression string `json:"TransformExpression"`
		ConflictPolicy      string `json:"ConflictPolicy"`
		DefaultValue        string `json:"DefaultValue"`
		ValueMap            string `json:"ValueMap,omitempty"`
		OnUnmappedValue     string `json:"OnUnmappedValue,omitempty"`
		SortOrder           int    `json:"SortOrder"`
		ProjectionOnly      bool   `json:"ProjectionOnly,omitempty"`
	}
	return json.Marshal(connectorFieldMappingInputJSON{
		Source:              row.Source,
		SourceObject:        row.SourceObject,
		TargetObject:        row.TargetObject,
		ActionType:          row.ActionType,
		SourceField:         row.SourceField,
		TargetField:         row.TargetField,
		Direction:           row.Direction,
		TransformExpression: row.TransformExpression,
		ConflictPolicy:      row.ConflictPolicy,
		DefaultValue:        row.DefaultValue,
		ValueMap:            row.ValueMap,
		OnUnmappedValue:     row.OnUnmappedValue,
		SortOrder:           row.SortOrder,
		ProjectionOnly:      row.ProjectionOnly,
	})
}

// ResolveQuery carries the filter parameters for a direction-aware connector
// field mapping read. All fields must be non-blank except Direction, which is
// additionally validated against the allowed connector values.
type ResolveQuery struct {
	// Source is the connector's namespace / owner key. Must be non-blank.
	// A connector read must always scope to its own Source so one connector
	// cannot read another connector's rows.
	Source string

	// SourceObject and TargetObject are the SObject API names for the mapping
	// tuple.
	SourceObject string
	TargetObject string

	// ActionType scopes the mapping to the declared action.
	ActionType string

	// Direction is the caller's perspective. Must be one of: inbound,
	// outbound, bidirectional. Passing 'legacy' is rejected.
	//
	// Bidirectional fan-out: the query matches both rows whose Direction
	// equals q.Direction AND rows whose Direction is 'bidirectional', so a
	// caller resolving 'inbound' also picks up 'bidirectional' rows.
	Direction string
}

// ParseConnectorValueMap validates and decodes the metadata-owned source→target
// translation table used by connector field mappings. Source keys are stored as
// strings because inbound connector payloads arrive as untyped JSON values; the
// runtime canonicalises a scalar source value into the same key space before
// looking it up.
func ParseConnectorValueMap(raw string) (map[string]interface{}, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}

	dec := json.NewDecoder(bytes.NewReader([]byte(raw)))
	dec.UseNumber()
	tok, err := dec.Token()
	if err != nil {
		return nil, err
	}
	if delim, ok := tok.(json.Delim); !ok || delim != '{' {
		return nil, fmt.Errorf("must be a JSON object")
	}

	result := map[string]interface{}{}
	seen := map[string]struct{}{}
	for dec.More() {
		keyTok, err := dec.Token()
		if err != nil {
			return nil, err
		}
		key, ok := keyTok.(string)
		if !ok {
			return nil, fmt.Errorf("source keys must be strings")
		}
		key = strings.TrimSpace(key)
		if key == "" {
			return nil, fmt.Errorf("source keys must not be blank")
		}
		if _, dup := seen[key]; dup {
			return nil, fmt.Errorf("source key %q is duplicated", key)
		}
		seen[key] = struct{}{}

		var value interface{}
		if err := dec.Decode(&value); err != nil {
			return nil, err
		}
		switch value.(type) {
		case string, bool, json.Number:
			result[key] = value
		default:
			return nil, fmt.Errorf("target value for source key %q must be a string, number, or boolean scalar", key)
		}
	}
	if _, err := dec.Token(); err != nil {
		return nil, err
	}
	if tok, err := dec.Token(); err == nil {
		return nil, fmt.Errorf("unexpected trailing token %v", tok)
	} else if err != io.EOF {
		return nil, err
	}
	if len(result) == 0 {
		return nil, fmt.Errorf("must declare at least one translation")
	}
	return result, nil
}

// ValidateConnectorFieldMappingInputSet rejects ambiguous connector field
// mapping declarations across the union of one or more connector_field_mapping
// components. It is shared by per-payload parsing and pack-level cross-component
// validation so install, verify, sweep, and runtime all see one contract.
func ValidateConnectorFieldMappingInputSet(rows []ConnectorFieldMappingInput) error {
	if err := validateConnectorFieldMappingNoDuplicateBusinessKeys(rows); err != nil {
		return err
	}
	return validateConnectorFieldMappingNoDuplicateEffectiveTargets(rows)
}

func validateConnectorFieldMappingNoDuplicateBusinessKeys(rows []ConnectorFieldMappingInput) error {
	seen := make(map[string]int, len(rows))
	for i, row := range rows {
		key := connectorFieldMappingBusinessKey(
			strings.TrimSpace(row.Source),
			strings.TrimSpace(row.SourceObject),
			strings.TrimSpace(row.TargetObject),
			strings.TrimSpace(row.ActionType),
			strings.TrimSpace(row.SourceField),
			strings.TrimSpace(row.Direction),
		)
		if prev, dup := seen[key]; dup {
			return fmt.Errorf("connector_field_mapping row %d: duplicate business key (Source=%s, SourceObject=%s, TargetObject=%s, ActionType=%s, SourceField=%s, Direction=%s) — conflicts with row %d",
				i, row.Source, row.SourceObject, row.TargetObject, row.ActionType, row.SourceField, row.Direction, prev)
		}
		seen[key] = i
	}
	return nil
}

func validateConnectorFieldMappingNoDuplicateEffectiveTargets(rows []ConnectorFieldMappingInput) error {
	type keyInfo struct {
		index              int
		row                ConnectorFieldMappingInput
		effectiveDirection string
	}
	seen := make(map[string]keyInfo, len(rows))
	for i, row := range rows {
		if row.ProjectionOnly {
			continue
		}
		for _, effectiveDirection := range connectorFieldMappingEffectiveReadDirections(row.Direction) {
			key := strings.Join([]string{
				connectorFieldMappingKeySegment(row.Source),
				connectorFieldMappingKeySegment(row.SourceObject),
				connectorFieldMappingKeySegment(row.TargetObject),
				connectorFieldMappingKeySegment(row.ActionType),
				connectorFieldMappingKeySegment(effectiveDirection),
				connectorFieldMappingKeySegment(row.TargetField),
			}, "\x00")
			if prev, dup := seen[key]; dup {
				// Exact-direction rows take precedence over bidirectional rows by
				// SourceField. When the source field is identical, resolver
				// deduplication leaves one effective row, so there is no ambiguous
				// target write to reject.
				if connectorFieldMappingKeySegment(prev.row.SourceField) == connectorFieldMappingKeySegment(row.SourceField) {
					continue
				}
				return fmt.Errorf("connector_field_mapping row %d: duplicate effective target field (Source=%s, SourceObject=%s, TargetObject=%s, ActionType=%s, Direction=%s, TargetField=%s) for effective direction %s — SourceField %s conflicts with row %d SourceField %s",
					i,
					strings.TrimSpace(row.Source),
					strings.TrimSpace(row.SourceObject),
					strings.TrimSpace(row.TargetObject),
					strings.TrimSpace(row.ActionType),
					strings.TrimSpace(row.Direction),
					strings.TrimSpace(row.TargetField),
					effectiveDirection,
					strings.TrimSpace(row.SourceField),
					prev.index,
					strings.TrimSpace(prev.row.SourceField),
				)
			}
			seen[key] = keyInfo{index: i, row: row, effectiveDirection: effectiveDirection}
		}
	}
	return nil
}

func connectorFieldMappingEffectiveReadDirections(direction string) []string {
	switch strings.TrimSpace(direction) {
	case DirectionBidirectional:
		return []string{DirectionBidirectional, DirectionInbound, DirectionOutbound}
	default:
		return []string{strings.TrimSpace(direction)}
	}
}

// connectorFieldMappingBusinessKey returns a stable, canonical string derived
// from the six fields that form the unique business key for connector rows.
// _System_ObjectFieldMapping uses the repo-default case-insensitive collation,
// so code-side duplicate checks, deterministic IDs, and sweeper ID sets must
// fold case the same way for API-name/action segments.
//
// The separator \x00 is chosen to match other key-derivation helpers and to
// avoid collision between adjacent fields.
func connectorFieldMappingBusinessKey(source, sourceObject, targetObject, actionType, sourceField, direction string) string {
	return strings.Join([]string{
		connectorFieldMappingKeySegment(source),
		connectorFieldMappingKeySegment(sourceObject),
		connectorFieldMappingKeySegment(targetObject),
		connectorFieldMappingKeySegment(actionType),
		connectorFieldMappingKeySegment(sourceField),
		connectorFieldMappingKeySegment(direction),
	}, "\x00")
}

func connectorFieldMappingKeySegment(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}
