package automation

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"

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

var errConnectorPushAssociationTargetLinkPending = errors.New("connector push association target link pending")

type connectorPushAssociationTargetLinkPendingError struct {
	contract       connector.ConnectorPushAssociationContract
	sourceRecordID string
}

func (e *connectorPushAssociationTargetLinkPendingError) Error() string {
	return fmt.Sprintf("%s: PushToConnector: finalized association target link %s/%s is required", errConnectorPushAssociationTargetLinkPending, e.contract.SourceObject, e.sourceRecordID)
}

func (e *connectorPushAssociationTargetLinkPendingError) Unwrap() error {
	return errConnectorPushAssociationTargetLinkPending
}

const (
	connectorPushPatchDispatchStateAssociationDecision = "association_decision"
	connectorPushPatchDispatchStateAwaitingCreateLink  = "awaiting_create_link"
	connectorPushPatchDispatchStateFrozen              = "frozen"
)

func connectorPushAssociationLookupFields(contracts []connector.ConnectorPushAssociationContract) []string {
	fields := make([]string, 0, len(contracts))
	for _, contract := range contracts {
		if field := strings.TrimSpace(contract.SourceLookupField); field != "" {
			fields = append(fields, field)
		}
	}
	return fields
}

func connectorPushPayloadSourceFieldsFromIntent(intent *ConnectorPushPatchDispatchIntent) ([]string, error) {
	if intent == nil || len(intent.PayloadSourceFields) == 0 {
		return nil, nil
	}
	return validatePushToConnectorPayloadSourceFieldList(intent.PayloadSourceFields)
}

func requireConnectorPushAwaitingCreateLinkIntent(snapshot ConnectorPushDurableSnapshot) error {
	intent := snapshot.PatchDispatchIntent
	if intent == nil || strings.TrimSpace(intent.State) != connectorPushPatchDispatchStateAwaitingCreateLink {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link intent is required")
	}
	if strings.TrimSpace(snapshot.RequestMethod) != http.MethodPatch {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link intent requires PATCH request method")
	}
	if strings.TrimSpace(snapshot.RequestRelativePath) != "" {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link intent must not freeze a request path")
	}
	if intent.AssociationWorkRequired || len(intent.AssociationRequests) != 0 {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link intent does not permit associations")
	}
	if _, err := connectorPushPayloadSourceFieldsFromIntent(intent); err != nil {
		return err
	}
	if err := requireConnectorPushBodyHash("request body", snapshot.RequestBody, snapshot.RequestBodyHash); err != nil {
		return err
	}
	if intent.RequestBodyHash != snapshot.RequestBodyHash {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link request body hash mismatch")
	}
	var body map[string]interface{}
	if err := json.Unmarshal(snapshot.RequestBody, &body); err != nil {
		return fmt.Errorf("decode connector push durable awaiting-create-link PATCH body: %w", err)
	}
	if intent.PatchRequired != connectorPushMapHasFields(body) {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link patch-required drift")
	}
	fingerprint, err := connectorPushPatchDispatchIntentFingerprint(intent)
	if err != nil {
		return err
	}
	if strings.TrimSpace(intent.Fingerprint) == "" || intent.Fingerprint != fingerprint {
		return fmt.Errorf("connector push durable PATCH awaiting-create-link fingerprint mismatch")
	}
	return nil
}

func requireConnectorPushAssociationDecisionIntent(snapshot ConnectorPushDurableSnapshot) error {
	intent := snapshot.PatchDispatchIntent
	if intent == nil || strings.TrimSpace(intent.State) != connectorPushPatchDispatchStateAssociationDecision {
		return fmt.Errorf("connector push durable PATCH association decision intent is required")
	}
	if strings.TrimSpace(snapshot.RequestMethod) != http.MethodPatch {
		return fmt.Errorf("connector push durable PATCH association decision requires PATCH request method")
	}
	if intent.PatchRequired || intent.RequestBodyHash != "" || intent.AssociationRequests != nil || intent.Fingerprint != "" {
		return fmt.Errorf("connector push durable PATCH association decision contains frozen dispatch state")
	}
	if _, err := connectorPushPayloadSourceFieldsFromIntent(intent); err != nil {
		return err
	}
	return nil
}

func requireConnectorPushPatchDispatchIntent(snapshot ConnectorPushDurableSnapshot) error {
	intent := snapshot.PatchDispatchIntent
	if intent == nil {
		return fmt.Errorf("connector push durable PATCH dispatch intent is required")
	}
	if strings.TrimSpace(intent.State) != connectorPushPatchDispatchStateFrozen {
		return fmt.Errorf("connector push durable PATCH dispatch intent is not frozen")
	}
	if strings.TrimSpace(snapshot.RequestMethod) != http.MethodPatch {
		return fmt.Errorf("connector push durable PATCH dispatch requires PATCH request method")
	}
	if intent.AssociationRequests == nil {
		return fmt.Errorf("connector push durable PATCH association request set is not frozen")
	}
	if _, err := connectorPushPayloadSourceFieldsFromIntent(intent); err != nil {
		return err
	}
	if err := requireConnectorPushBodyHash("request body", snapshot.RequestBody, snapshot.RequestBodyHash); err != nil {
		return err
	}
	if strings.TrimSpace(intent.RequestBodyHash) == "" || intent.RequestBodyHash != snapshot.RequestBodyHash {
		return fmt.Errorf("connector push durable PATCH dispatch request body hash mismatch")
	}
	var body map[string]interface{}
	if err := json.Unmarshal(snapshot.RequestBody, &body); err != nil {
		return fmt.Errorf("decode connector push durable frozen PATCH body: %w", err)
	}
	if intent.PatchRequired != connectorPushMapHasFields(body) {
		return fmt.Errorf("connector push durable PATCH dispatch patch-required drift")
	}
	for i, request := range intent.AssociationRequests {
		if !connectorPushAssociationMethodIsDurablyIdempotent(request.Method) {
			return fmt.Errorf("connector push durable PATCH association request %d method %q is not idempotent", i, request.Method)
		}
		if strings.TrimSpace(request.RelativePath) == "" {
			return fmt.Errorf("connector push durable PATCH association request %d path is required", i)
		}
		if err := requireConnectorPushBodyHash(fmt.Sprintf("association request %d body", i), request.Body, request.BodyHash); err != nil {
			return err
		}
	}
	fingerprint, err := connectorPushPatchDispatchIntentFingerprint(intent)
	if err != nil {
		return err
	}
	if strings.TrimSpace(intent.Fingerprint) == "" || intent.Fingerprint != fingerprint {
		return fmt.Errorf("connector push durable PATCH dispatch fingerprint mismatch")
	}
	return nil
}

func connectorPushPatchDispatchIntentFingerprint(intent *ConnectorPushPatchDispatchIntent) (string, error) {
	if intent == nil {
		return "", fmt.Errorf("connector push durable PATCH dispatch intent is required")
	}
	canonical := *intent
	canonical.Fingerprint = ""
	encoded, err := json.Marshal(canonical)
	if err != nil {
		return "", fmt.Errorf("encode connector push durable PATCH dispatch intent: %w", err)
	}
	return connectorPushSHA256Hex(encoded), nil
}

func connectorPushAssociationMethodIsDurablyIdempotent(method string) bool {
	return strings.EqualFold(strings.TrimSpace(method), http.MethodPut)
}

func connectorPushAssociationTemplateReferencesFrom(template interface{}) bool {
	switch v := template.(type) {
	case nil:
		return false
	case map[string]interface{}:
		for key, value := range v {
			if strings.Contains(key, "{fromObject}") || strings.Contains(key, "{fromId}") || connectorPushAssociationTemplateReferencesFrom(value) {
				return true
			}
		}
	case []interface{}:
		for _, value := range v {
			if connectorPushAssociationTemplateReferencesFrom(value) {
				return true
			}
		}
	case string:
		return strings.Contains(v, "{fromObject}") || strings.Contains(v, "{fromId}")
	}
	return false
}

func connectorPushMapHasFields(payload map[string]interface{}) bool {
	for _, value := range payload {
		switch typed := value.(type) {
		case map[string]interface{}:
			if len(typed) > 0 {
				return true
			}
		case models.SObject:
			if len(typed) > 0 {
				return true
			}
		default:
			return true
		}
	}
	return false
}
