package automation

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

type pushToConnectorParams struct {
	ProfileName         string
	ProfileOwnerPack    string
	SourceObject        string
	TargetObject        string
	BodyRoot            string
	DestinationSlotID   string
	MappingActionType   string
	UpdatePathValue     interface{}
	RemoteID            string
	UpdatePathTemplate  string
	SourceRecord        models.SObject
	SourceRecordIsRoot  bool
	RoutingSourceFields []string
	// PayloadSourceFields is an optional, action-scoped projection over the
	// contract-owned mapping closure. It is resolved from the Flow at save time;
	// an empty projection keeps the complete closure for backwards compatibility.
	PayloadSourceFields []string
	// ContractMappings retains the complete contract-owned mapping closure while
	// PayloadSourceFields narrows the payload for this execution. Durable links
	// attest to the full closure, not an individual sparse PATCH.
	ContractMappings []connector.ConnectorFieldMappingInput
}

func connectorPushAllowsSameOriginPatchExisting(contract *connector.ConnectorPushDurableMetadataContract) bool {
	return contract != nil && strings.TrimSpace(contract.SameOriginWritePolicy) == connector.ConnectorPushSameOriginWritePolicyAllowPatchExisting
}

func connectorPushSystemUser(user *models.UserSession) bool {
	return user != nil && strings.EqualFold(strings.TrimSpace(user.ID), constants.SystemUserID)
}

func requirePushToConnectorPostCommitRootExecution(ctx context.Context, flowMeta *models.FlowMetadata) error {
	if !isRecordTriggerRootExecution(ctx) {
		return fmt.Errorf("PushToConnector: v1a requires synchronous record-trigger root execution")
	}
	if !isPushToConnectorPostCommitSafeRecordTrigger(flowMeta) || isBeforeSaveFlow(ctx) {
		return fmt.Errorf("PushToConnector: v1a requires a post-commit-safe record trigger root")
	}
	if isWithinAtomicFlowBoundary(ctx) {
		return fmt.Errorf("PushToConnector: v1a cannot run inside an atomic Flow transaction")
	}
	return nil
}

func requirePushToConnectorSynchronousUserExecution(ctx context.Context, flowMeta *models.FlowMetadata, user *models.UserSession) error {
	if err := requirePushToConnectorPostCommitRootExecution(ctx, flowMeta); err != nil {
		return err
	}
	if user == nil {
		return fmt.Errorf("PushToConnector: originating user is required for source row read and source field FLS authorization")
	}
	if connectorPushSystemUser(user) {
		return fmt.Errorf("PushToConnector: system automation user cannot authorize outbound source row or field reads")
	}
	return nil
}

func validatePushToConnectorContractDrift(ctx context.Context, flowMeta *models.FlowMetadata, action *models.FlowActionCall, params pushToConnectorParams, contract *connector.ConnectorPushDurableMetadataContract) error {
	if contract == nil {
		return fmt.Errorf("PushToConnector: connector_push_contract is required")
	}
	if action == nil || strings.TrimSpace(action.ActionType) != constants.ActionTypePushToConnector {
		actionType := ""
		if action != nil {
			actionType = strings.TrimSpace(action.ActionType)
		}
		return fmt.Errorf("PushToConnector: Flow action type %q does not match connector_push_contract", actionType)
	}
	check := func(name, got, want string, allowBlank bool) error {
		got = strings.TrimSpace(got)
		want = strings.TrimSpace(want)
		if allowBlank && got == "" {
			return nil
		}
		if got != want {
			return fmt.Errorf("PushToConnector: Flow %s %q does not match connector_push_contract %q; PATCH fails closed", name, got, want)
		}
		return nil
	}
	if err := check("profileOwnerPack", params.ProfileOwnerPack, contract.ProfileOwnerPack, true); err != nil {
		return err
	}
	if err := check("profileName", params.ProfileName, contract.ProfileName, false); err != nil {
		return err
	}
	if err := check("sourceObject", params.SourceObject, contract.SourceObject, false); err != nil {
		return err
	}
	if err := check("targetObject", params.TargetObject, contract.TargetObject, false); err != nil {
		return err
	}
	if err := check("bodyRoot", params.BodyRoot, contract.BodyRoot, true); err != nil {
		return err
	}
	if err := check("destinationSlotId", params.DestinationSlotID, contract.DestinationSlotID, false); err != nil {
		return err
	}
	if err := check("actionType", params.MappingActionType, contract.ActionType, false); err != nil {
		return err
	}
	pathTemplate, err := literalPushToConnectorContractOwnedInput("updatePathTemplate", params.UpdatePathValue)
	if err != nil {
		return err
	}
	if err := check("updatePathTemplate", pathTemplate, contract.UpdatePathTemplate, false); err != nil {
		return err
	}
	return validatePushToConnectorSourceRecordObject(ctx, flowMeta, params)
}

func pushToConnectorParamsFromContract(params pushToConnectorParams, contract *connector.ConnectorPushDurableMetadataContract) pushToConnectorParams {
	params.ProfileOwnerPack = strings.TrimSpace(contract.ProfileOwnerPack)
	params.ProfileName = strings.TrimSpace(contract.ProfileName)
	params.SourceObject = strings.TrimSpace(contract.SourceObject)
	params.TargetObject = strings.TrimSpace(contract.TargetObject)
	params.BodyRoot = strings.TrimSpace(contract.BodyRoot)
	params.DestinationSlotID = strings.TrimSpace(contract.DestinationSlotID)
	params.MappingActionType = strings.TrimSpace(contract.ActionType)
	params.UpdatePathTemplate = strings.TrimSpace(contract.UpdatePathTemplate)
	params.UpdatePathValue = params.UpdatePathTemplate
	params.RoutingSourceFields = nil
	return params
}

func isPushToConnectorPostCommitSafeRecordTrigger(flowMeta *models.FlowMetadata) bool {
	if flowMeta == nil || flowMeta.Start == nil {
		return false
	}
	switch triggerType := strings.TrimSpace(flowMeta.Start.TriggerType); {
	case strings.EqualFold(triggerType, "RecordAfterSave"):
		return true
	case strings.EqualFold(triggerType, "onAllChanges"):
		return true
	default:
		return false
	}
}

func (e *FlowExecutor) resolvePushToConnectorParams(ctx context.Context, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (pushToConnectorParams, error) {
	raw := make(map[string]interface{}, len(action.InputParameters))
	var routingFields []string
	sourceRecordIsRoot := false
	for _, p := range action.InputParameters {
		if p.Name == "sourceRecord" && isPushToConnectorRootRecordReference(p.Value) {
			sourceRecordIsRoot = true
		}
		if p.Name == "profileOwnerPack" {
			profileOwnerPack, err := literalPushToConnectorProfileOwnerPack(p.Value)
			if err != nil {
				return pushToConnectorParams{}, err
			}
			raw[p.Name] = profileOwnerPack
			continue
		}
		if p.Name == "destinationSlotId" {
			destinationSlotID, err := literalPushToConnectorStableSlot("destinationSlotId", p.Value)
			if err != nil {
				return pushToConnectorParams{}, err
			}
			raw[p.Name] = destinationSlotID
			continue
		}
		if p.Name == "bodyRoot" {
			bodyRoot, err := literalPushToConnectorBodyRoot(p.Value)
			if err != nil {
				return pushToConnectorParams{}, err
			}
			raw[p.Name] = bodyRoot
			continue
		}
		if p.Name == "profileName" || p.Name == "sourceObject" || p.Name == "targetObject" || p.Name == "actionType" {
			literal, err := literalPushToConnectorContractOwnedInput(p.Name, p.Value)
			if err != nil {
				return pushToConnectorParams{}, err
			}
			raw[p.Name] = literal
			continue
		}
		if p.Name == "updatePathTemplate" {
			literal, err := literalPushToConnectorContractOwnedInput(p.Name, p.Value)
			if err != nil {
				return pushToConnectorParams{}, err
			}
			raw[p.Name] = literal
			continue
		}
		resolved, err := e.deepResolveFlowResource(ctx, interview, user, p.Value)
		if err != nil {
			return pushToConnectorParams{}, err
		}
		raw[p.Name] = resolved
	}

	params := pushToConnectorParams{
		ProfileName:         trimmedString(raw["profileName"]),
		ProfileOwnerPack:    trimmedString(raw["profileOwnerPack"]),
		SourceObject:        trimmedString(raw["sourceObject"]),
		TargetObject:        trimmedString(raw["targetObject"]),
		BodyRoot:            trimmedString(raw["bodyRoot"]),
		DestinationSlotID:   trimmedString(raw["destinationSlotId"]),
		MappingActionType:   trimmedString(raw["actionType"]),
		UpdatePathValue:     raw["updatePathTemplate"],
		SourceRecord:        coerceFlowRecord(raw["sourceRecord"]),
		SourceRecordIsRoot:  sourceRecordIsRoot,
		RoutingSourceFields: routingFields,
	}
	payloadSourceFields, err := parsePushToConnectorPayloadSourceFields(raw["payloadSourceFields"])
	if err != nil {
		return params, err
	}
	params.PayloadSourceFields = payloadSourceFields
	if params.SourceRecord == nil {
		params.SourceRecord = coerceFlowRecord(interview.ContextData["$Record"])
		params.SourceRecordIsRoot = params.SourceRecord != nil
	}
	if params.SourceObject == "" && flowMeta != nil && flowMeta.Start != nil {
		params.SourceObject = strings.TrimSpace(flowMeta.Start.Object)
	}
	if params.MappingActionType == "" {
		params.MappingActionType = constants.ActionTypePushToConnector
	}
	if !strings.EqualFold(params.MappingActionType, constants.ActionTypePushToConnector) {
		return params, fmt.Errorf("PushToConnector: actionType must be blank or %s", constants.ActionTypePushToConnector)
	}

	switch {
	case params.ProfileName == "":
		return params, fmt.Errorf("PushToConnector: profileName must not be blank")
	case params.SourceObject == "":
		return params, fmt.Errorf("PushToConnector: sourceObject must not be blank")
	case params.TargetObject == "":
		return params, fmt.Errorf("PushToConnector: targetObject must not be blank")
	case params.DestinationSlotID == "":
		return params, fmt.Errorf("PushToConnector: destinationSlotId must not be blank")
	case params.MappingActionType == "":
		return params, fmt.Errorf("PushToConnector: actionType must not be blank")
	case params.UpdatePathValue == nil:
		return params, fmt.Errorf("PushToConnector: updatePathTemplate must not be blank")
	case params.SourceRecord == nil:
		return params, fmt.Errorf("PushToConnector: sourceRecord or $Record must be present")
	default:
		return params, validatePushToConnectorSourceRecordObject(ctx, flowMeta, params)
	}
}

func parsePushToConnectorPayloadSourceFields(value interface{}) ([]string, error) {
	if value == nil {
		return nil, nil
	}
	raw, ok := value.(string)
	if !ok {
		return nil, fmt.Errorf("PushToConnector: payloadSourceFields must resolve to a comma-separated string")
	}
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	return validatePushToConnectorPayloadSourceFieldList(strings.Split(raw, ","))
}

func validatePushToConnectorPayloadSourceFieldList(fields []string) ([]string, error) {
	if len(fields) == 0 {
		return nil, nil
	}
	seen := make(map[string]struct{}, len(fields))
	out := make([]string, 0, len(fields))
	for _, field := range fields {
		field = strings.TrimSpace(field)
		if field == "" {
			return nil, fmt.Errorf("PushToConnector: payloadSourceFields contains a blank field")
		}
		if _, duplicate := seen[field]; duplicate {
			return nil, fmt.Errorf("PushToConnector: payloadSourceFields contains duplicate field %q", field)
		}
		seen[field] = struct{}{}
		out = append(out, field)
	}
	return out, nil
}

func literalPushToConnectorProfileOwnerPack(value interface{}) (string, error) {
	if value == nil {
		return "", nil
	}
	if literal, wasLiteral := extractTypedValue(value); wasLiteral {
		return validatePushToConnectorProfileOwnerPackLiteral(literal)
	}
	if _, ok := pushToConnectorElementReference(value); ok {
		return "", fmt.Errorf("PushToConnector: profileOwnerPack must be a literal pack name")
	}
	if _, ok := value.(map[string]interface{}); ok {
		return "", fmt.Errorf("PushToConnector: profileOwnerPack must be a literal pack name")
	}
	return validatePushToConnectorProfileOwnerPackLiteral(value)
}

func literalPushToConnectorStableSlot(name string, value interface{}) (string, error) {
	if value == nil {
		return "", nil
	}
	if literal, wasLiteral := extractTypedValue(value); wasLiteral {
		value = literal
	}
	if _, ok := pushToConnectorElementReference(value); ok {
		return "", fmt.Errorf("PushToConnector: %s must be a literal stable slot", name)
	}
	if _, ok := value.(map[string]interface{}); ok {
		return "", fmt.Errorf("PushToConnector: %s must be a literal stable slot", name)
	}
	literal := trimmedString(value)
	if literal == "" {
		return "", nil
	}
	if strings.HasPrefix(literal, "$") || strings.Contains(literal, ".") {
		return "", fmt.Errorf("PushToConnector: %s must be a literal stable slot", name)
	}
	return literal, nil
}

func validatePushToConnectorProfileOwnerPackLiteral(value interface{}) (string, error) {
	literal := trimmedString(value)
	if literal == "" {
		return "", nil
	}
	if strings.HasPrefix(literal, "$") {
		return "", fmt.Errorf("PushToConnector: profileOwnerPack must be a literal pack name")
	}
	return literal, nil
}

func literalPushToConnectorBodyRoot(value interface{}) (string, error) {
	if literal, wasLiteral := extractTypedValue(value); wasLiteral {
		value = literal
	}
	if _, ok := pushToConnectorElementReference(value); ok {
		return "", fmt.Errorf("PushToConnector: bodyRoot must be a connector_push_contract literal")
	}
	if _, ok := value.(map[string]interface{}); ok {
		return "", fmt.Errorf("PushToConnector: bodyRoot must be a connector_push_contract literal")
	}
	literal := trimmedString(value)
	if strings.HasPrefix(literal, "$") || strings.Contains(literal, "{!") {
		return "", fmt.Errorf("PushToConnector: bodyRoot must be a connector_push_contract literal")
	}
	return validatePushToConnectorBodyRootLiteral(literal)
}

func validatePushToConnectorBodyRootLiteral(value string) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", nil
	}
	for _, r := range value {
		if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
			continue
		}
		return "", fmt.Errorf("PushToConnector: bodyRoot must be a simple literal field name")
	}
	return value, nil
}

func literalPushToConnectorContractOwnedInput(name string, value interface{}) (string, error) {
	if literal, wasLiteral := extractTypedValue(value); wasLiteral {
		value = literal
	}
	if ref, ok := pushToConnectorElementReference(value); ok {
		return "", fmt.Errorf("PushToConnector: %s elementReference %q must not supply connector_push_contract-owned fields", name, ref)
	}
	if _, ok := value.(map[string]interface{}); ok {
		return "", fmt.Errorf("PushToConnector: %s must be a connector_push_contract literal", name)
	}
	literal := trimmedString(value)
	if strings.HasPrefix(literal, "$") || strings.Contains(literal, "{!") {
		return "", fmt.Errorf("PushToConnector: %s must be a connector_push_contract literal", name)
	}
	return literal, nil
}

func pushToConnectorElementReference(value interface{}) (string, bool) {
	m, ok := value.(map[string]interface{})
	if !ok {
		return "", false
	}
	ref, ok := m["elementReference"].(string)
	return strings.TrimSpace(ref), ok && strings.TrimSpace(ref) != ""
}

func isPushToConnectorRootRecordReference(value interface{}) bool {
	if ref, ok := value.(string); ok {
		return ref == "$Record"
	}
	m, ok := value.(map[string]interface{})
	if !ok {
		return false
	}
	ref, _ := m["elementReference"].(string)
	return ref == "$Record"
}

func validatePushToConnectorSourceRecordObject(ctx context.Context, flowMeta *models.FlowMetadata, params pushToConnectorParams) error {
	reportedObject := strings.TrimSpace(models.RecordObjectType(params.SourceRecord))
	if reportedObject != "" && !strings.EqualFold(reportedObject, params.SourceObject) {
		return fmt.Errorf("PushToConnector: source record object %s does not match sourceObject %s", reportedObject, params.SourceObject)
	}

	trustedObject := ""
	if params.SourceRecordIsRoot {
		trustedObject = strings.TrimSpace(flowRuntimeContextFromCtx(ctx).ObjectType())
		if trustedObject == "" && flowMeta != nil && flowMeta.Start != nil {
			trustedObject = strings.TrimSpace(flowMeta.Start.Object)
		}
	}
	if trustedObject == "" {
		return fmt.Errorf("PushToConnector: source record object identity is required for source row read and FLS authorization")
	}
	if !strings.EqualFold(trustedObject, params.SourceObject) {
		return fmt.Errorf("PushToConnector: flow source object %s does not match sourceObject %s", trustedObject, params.SourceObject)
	}
	return nil
}

func trustedPushToConnectorRootRecordID(ctx context.Context, sourceRecord models.SObject) (string, error) {
	trustedRecordID := recordTriggerRootRecordID(ctx)
	if trustedRecordID == "" {
		return "", fmt.Errorf("PushToConnector: trusted trigger record Id is required for committed current-row reload")
	}
	if sourceRecordID, _ := sourceRecord[constants.FieldID].(string); strings.TrimSpace(sourceRecordID) != "" && strings.TrimSpace(sourceRecordID) != trustedRecordID {
		return "", fmt.Errorf("PushToConnector: source record Id %s does not match trusted trigger record Id %s", strings.TrimSpace(sourceRecordID), trustedRecordID)
	}
	return trustedRecordID, nil
}
