// Package connector holds the connector field-mapping and PushToConnector
// durable-metadata parse/validate logic that depends on sibling kernel modules
// (kernel/pkg/formula, shared/pkg/constants) in addition to the pure data
// contracts in github.com/pingcap-inc/nexuscrm-contract/pkg/connector.
//
// These symbols were relocated verbatim out of internal/bootstrap as the first
// (AU) slice of the bootstrap-decoupling campaign. The pure DTOs/enums/parsers
// they reference live in the contract package; this package adds only the logic
// that cannot live there because it imports formula/shared.
package connector

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

	"github.com/nexuscrm/kernel/pkg/formula"
	contract "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

const (
	legacyObjectFieldMappingOwner = "legacy"
	maxObjectFieldMappingOwnerLen = 64
)

var validDirections = map[string]struct{}{
	contract.DirectionInbound:       {},
	contract.DirectionOutbound:      {},
	contract.DirectionBidirectional: {},
}

var validConflictPolicies = map[string]struct{}{
	contract.ConflictPolicyOverwrite: {},
	contract.ConflictPolicySkipIfSet: {},
	contract.ConflictPolicyError:     {},
}

var validOnUnmappedValuePolicies = map[string]struct{}{
	contract.OnUnmappedValueError:       {},
	contract.OnUnmappedValueSkip:        {},
	contract.OnUnmappedValuePassthrough: {},
}

// ValidateConnectorFieldMappingRow validates a connector field mapping input.
// It is fail-closed: any unrecognised or sentinel value is rejected.
// It does NOT touch the database; install/verify add live target schema
// validation after the source-backed object metadata phase has run.
func ValidateConnectorFieldMappingRow(row contract.ConnectorFieldMappingInput) error {
	return ValidateConnectorFieldMappingRowWithFormulaEngine(row, formula.NewEngine())
}

// ValidateConnectorFieldMappingRowWithFormulaEngine validates a connector
// field mapping against the exact formula authority supplied by its caller.
// Catalog callers use a revision-local engine so validation cannot silently
// depend on process-global Runtime bundles; legacy pack parsing retains the
// existing ValidateConnectorFieldMappingRow behavior above.
func ValidateConnectorFieldMappingRowWithFormulaEngine(
	row contract.ConnectorFieldMappingInput,
	formulaEngine *formula.Engine,
) error {
	if formulaEngine == nil {
		return fmt.Errorf("connector field mapping: formula engine must not be nil")
	}
	// Required business-key fields must be non-blank.
	if strings.TrimSpace(row.Source) == "" {
		return fmt.Errorf("connector field mapping: Source must not be blank")
	}
	if strings.TrimSpace(row.SourceObject) == "" {
		return fmt.Errorf("connector field mapping: SourceObject must not be blank")
	}
	if strings.TrimSpace(row.TargetObject) == "" {
		return fmt.Errorf("connector field mapping: TargetObject must not be blank")
	}
	if strings.TrimSpace(row.ActionType) == "" {
		return fmt.Errorf("connector field mapping: ActionType must not be blank")
	}
	if strings.TrimSpace(row.SourceField) == "" {
		return fmt.Errorf("connector field mapping: SourceField must not be blank")
	}
	if !row.ProjectionOnly && strings.TrimSpace(row.TargetField) == "" {
		return fmt.Errorf("connector field mapping: TargetField must not be blank")
	}

	// Direction must be one of the connector values; 'legacy' and blank are rejected.
	dir := strings.TrimSpace(row.Direction)
	if dir == "" {
		return fmt.Errorf("connector field mapping: Direction must not be blank")
	}
	if dir == legacyObjectFieldMappingOwner {
		return fmt.Errorf("connector field mapping: Direction 'legacy' is reserved for pre-connector rows; use inbound, outbound, or bidirectional")
	}
	if _, ok := validDirections[dir]; !ok {
		return fmt.Errorf("connector field mapping: Direction %q is not valid; allowed values: inbound, outbound, bidirectional", dir)
	}
	if row.ProjectionOnly {
		if dir != contract.DirectionInbound {
			return fmt.Errorf("connector field mapping: projectionOnly rows must use inbound direction")
		}
		if strings.TrimSpace(row.TargetField) != "" {
			return fmt.Errorf("connector field mapping: projectionOnly rows must not declare TargetField")
		}
		if strings.TrimSpace(row.TransformExpression) != "" ||
			strings.TrimSpace(row.ConflictPolicy) != "" ||
			strings.TrimSpace(row.DefaultValue) != "" ||
			strings.TrimSpace(row.ValueMap) != "" ||
			strings.TrimSpace(row.OnUnmappedValue) != "" {
			return fmt.Errorf("connector field mapping: projectionOnly rows must not declare write semantics")
		}
		return nil
	}

	// ConflictPolicy, if non-empty, must be one of the defined values.
	if cp := strings.TrimSpace(row.ConflictPolicy); cp != "" {
		if _, ok := validConflictPolicies[cp]; !ok {
			return fmt.Errorf("connector field mapping: ConflictPolicy %q is not valid; allowed values: overwrite, skip_if_set, error (or empty for runtime default)", cp)
		}
	}

	if vm := strings.TrimSpace(row.ValueMap); vm != "" {
		if _, err := contract.ParseConnectorValueMap(vm); err != nil {
			return fmt.Errorf("connector field mapping: ValueMap is invalid: %w", err)
		}
	}

	if oumv := strings.TrimSpace(row.OnUnmappedValue); oumv != "" {
		if _, ok := validOnUnmappedValuePolicies[oumv]; !ok {
			return fmt.Errorf("connector field mapping: OnUnmappedValue %q is not valid; allowed values: error, skip, passthrough (or empty for error default)", oumv)
		}
	}

	// TransformExpression validation: the connector executor evaluates with
	// Record-only context (no Prior map, no session globals, no Fetcher for
	// relationship traversal). A single AST-walking validator in the formula
	// package — which normalizes the expression first and then rejects, as the
	// complement of a closed allowed-set, every construct the flat-record
	// context cannot supply (prior-state, $-globals, relationship traversal,
	// foreign namespaces) plus parse/unknown-function errors. Sharing the
	// runtime's normalizer + AST keeps the install-time guard from diverging
	// from the evaluator on any syntactic form.
	if expr := strings.TrimSpace(row.TransformExpression); expr != "" {
		if err := formulaEngine.ValidateConnectorTransformExpression(expr); err != nil {
			return fmt.Errorf("connector field mapping: TransformExpression %q is invalid: %w", expr, err)
		}
	}

	return nil
}

// CanonicalConnectorValueMap canonicalises a raw ValueMap JSON document into its
// stable encoded form, returning "" for an empty document.
func CanonicalConnectorValueMap(raw json.RawMessage) (string, error) {
	if len(bytes.TrimSpace(raw)) == 0 {
		return "", nil
	}
	parsed, err := contract.ParseConnectorValueMap(string(raw))
	if err != nil {
		return "", err
	}
	canonical, err := json.Marshal(parsed)
	if err != nil {
		return "", err
	}
	return string(canonical), nil
}

// ConnectorFieldMappingGroup represents a group of connector field mappings
// between two objects for a given action type and direction. Direction is
// required and must be one of: inbound, outbound, bidirectional.
//
// This type is DISTINCT from FieldMappingGroup. Do NOT use FieldMappingGroup
// or its helpers for connector mappings; doing so silently drops Direction.
type ConnectorFieldMappingGroup struct {
	SourceObject string                        `json:"sourceObject"`
	TargetObject string                        `json:"targetObject"`
	ActionType   string                        `json:"actionType"`
	Direction    string                        `json:"direction"`
	Mappings     []ConnectorFieldMappingRowDef `json:"mappings"`
}

// ConnectorFieldMappingRowDef is a single source→target row within a group.
// TransformExpression, ConflictPolicy, DefaultValue, and ValueMap are optional.
type ConnectorFieldMappingRowDef struct {
	SourceField         string          `json:"sourceField"`
	TargetField         string          `json:"targetField"`
	TransformExpression string          `json:"transformExpression,omitempty"`
	ConflictPolicy      string          `json:"conflictPolicy,omitempty"`
	DefaultValue        string          `json:"defaultValue,omitempty"`
	ValueMap            json.RawMessage `json:"valueMap,omitempty"`
	OnUnmappedValue     string          `json:"onUnmappedValue,omitempty"`
	ProjectionOnly      bool            `json:"projectionOnly,omitempty"`
}

// parseConnectorFieldMappingGroupsPayload decodes a JSON array of
// ConnectorFieldMappingGroup. It rejects:
//   - unknown JSON keys (DisallowUnknownFields — prevents silent downgrade if
//     a caller forgets "direction" and it is treated as a zero-value blank).
//   - trailing content after the JSON value (ensureJSONEOF guard).
func parseConnectorFieldMappingGroupsPayload(raw []byte) ([]ConnectorFieldMappingGroup, error) {
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.DisallowUnknownFields()

	var groups []ConnectorFieldMappingGroup
	if err := dec.Decode(&groups); err != nil {
		return nil, fmt.Errorf("connector_field_mapping: failed to parse payload: %w", err)
	}

	// Reject trailing garbage after the JSON value — prevents a payload where
	// an extra object is silently ignored after the closing ].
	if tok, err := dec.Token(); err == nil {
		return nil, fmt.Errorf("connector_field_mapping: unexpected trailing token %v in payload", tok)
	} else if err != io.EOF {
		return nil, fmt.Errorf("connector_field_mapping: invalid trailing JSON in payload: %w", err)
	}

	return groups, nil
}

// ParseConnectorFieldMappingInputsFromPayload parses the JSON payload and
// returns the []ConnectorFieldMappingInput it declares for the given owner,
// WITHOUT touching the database. All validation is applied; any invalid row
// causes an error and no rows are returned.
//
// This is used by buildComponents (packs layer) at pack-load time to collect
// the full union of declared rows for the pack-level sweeper. It reuses the
// same parse+validate pipeline as InitializeConnectorFieldMappingsFromPayloadForOwner.
func ParseConnectorFieldMappingInputsFromPayload(raw []byte, owner string) ([]contract.ConnectorFieldMappingInput, error) {
	owner = strings.TrimSpace(owner)
	if owner == "" {
		return nil, fmt.Errorf("connector_field_mapping: owner must not be blank")
	}
	if len(owner) > maxObjectFieldMappingOwnerLen {
		return nil, fmt.Errorf("connector_field_mapping: owner %q exceeds %d characters", owner, maxObjectFieldMappingOwnerLen)
	}

	groups, err := parseConnectorFieldMappingGroupsPayload(raw)
	if err != nil {
		return nil, err
	}

	var rows []contract.ConnectorFieldMappingInput
	sortOrder := 0
	for _, group := range groups {
		for _, m := range group.Mappings {
			valueMap, err := CanonicalConnectorValueMap(m.ValueMap)
			if err != nil {
				return nil, fmt.Errorf("connector_field_mapping row %d: ValueMap is invalid: %w", sortOrder, err)
			}
			rows = append(rows, contract.ConnectorFieldMappingInput{
				Source:              owner,
				SourceObject:        group.SourceObject,
				TargetObject:        group.TargetObject,
				ActionType:          group.ActionType,
				Direction:           group.Direction,
				SourceField:         m.SourceField,
				TargetField:         m.TargetField,
				TransformExpression: m.TransformExpression,
				ConflictPolicy:      m.ConflictPolicy,
				DefaultValue:        m.DefaultValue,
				ValueMap:            valueMap,
				OnUnmappedValue:     m.OnUnmappedValue,
				SortOrder:           sortOrder,
				ProjectionOnly:      m.ProjectionOnly,
			})
			sortOrder++
		}
	}

	for i, row := range rows {
		if err := ValidateConnectorFieldMappingRow(row); err != nil {
			return nil, fmt.Errorf("connector_field_mapping row %d: %w", i, err)
		}
	}
	if err := contract.ValidateConnectorFieldMappingInputSet(rows); err != nil {
		return nil, err
	}
	return rows, nil
}
