// Package connector_outboundintent owns the Integration-side durable
// acceptance and provider-delivery lifecycle for Runtime outbound intents.
// Runtime owns only immutable intent creation and admission acknowledgement.
package connector_outboundintent

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
)

const (
	StatePending                = "pending"
	StateClaimed                = "claimed"
	StateDispatching            = "dispatching"
	StateRetry                  = "retry"
	stateDependencyRegistration = "dependency_registration"
	stateWaitingDependency      = "waiting_dependency"
	StateWriteback              = "writeback"
	StateSucceeded              = "succeeded"
	StateRejected               = "rejected"
	StateUncertain              = "uncertain"

	DefaultClaimLease                   = 2 * time.Minute
	dependencyKindAssociationRemoteLink = "association_remote_link"
	defaultDependencyReconcileInterval  = 2 * time.Minute
)

var (
	ErrInvalidInput     = errors.New("connector outbound intent input is invalid")
	ErrIdentityConflict = errors.New("connector outbound intent identity conflicts with durable content")
	ErrLeaseLost        = errors.New("connector outbound intent lease is no longer held")
	ErrCommandNotFound  = errors.New("connector outbound intent command was not found")
	ErrDependency       = errors.New("connector outbound intent dependency is unavailable")
	ErrSchemaUnverified = errors.New("connector outbound intent schema has not been verified")
)

// DurableIdentityConflictError correlates one command-scoped durable
// invariant failure without exposing provider payloads, credentials, remote
// identifiers, or the conflicting values themselves. DifferingFields contains
// only stable schema field names; every comparable value is a one-way
// fingerprint.
type DurableIdentityConflictError struct {
	Stage                        string
	CommandID                    string
	IntentID                     string
	LinkKey                      string
	CommandState                 string
	Operation                    string
	SourceObject                 string
	SourceRecordID               string
	PayloadFingerprint           string
	DurableIdentityFingerprint   string
	CandidateIdentityFingerprint string
	DurableMappingFingerprint    string
	CandidateMappingFingerprint  string
	DurableBodyFingerprint       string
	CandidateBodyFingerprint     string
	DifferingFields              []string
}

func (e *DurableIdentityConflictError) Error() string {
	if e == nil {
		return ErrIdentityConflict.Error()
	}
	return fmt.Sprintf(
		"%s: stage=%q command_id=%q intent_id=%q link_key=%q command_state=%q operation=%q source_object=%q source_record_id=%q payload_fingerprint=%q durable_identity_fingerprint=%q candidate_identity_fingerprint=%q durable_mapping_fingerprint=%q candidate_mapping_fingerprint=%q durable_body_fingerprint=%q candidate_body_fingerprint=%q differing_fields=%q",
		ErrIdentityConflict.Error(), strings.TrimSpace(e.Stage), strings.TrimSpace(e.CommandID),
		strings.TrimSpace(e.IntentID), strings.TrimSpace(e.LinkKey), strings.TrimSpace(e.CommandState),
		strings.TrimSpace(e.Operation), strings.TrimSpace(e.SourceObject), strings.TrimSpace(e.SourceRecordID),
		strings.TrimSpace(e.PayloadFingerprint), strings.TrimSpace(e.DurableIdentityFingerprint),
		strings.TrimSpace(e.CandidateIdentityFingerprint), strings.TrimSpace(e.DurableMappingFingerprint),
		strings.TrimSpace(e.CandidateMappingFingerprint), strings.TrimSpace(e.DurableBodyFingerprint),
		strings.TrimSpace(e.CandidateBodyFingerprint),
		strings.Join(e.DifferingFields, ","),
	)
}

func (e *DurableIdentityConflictError) Unwrap() error {
	return ErrIdentityConflict
}

// Scope is supplied by the authorized Integration process configuration and
// pinned IntegrationBindingView. It is never accepted from provider input.
type Scope struct {
	OrgID                string
	AppInstallationID    string
	AuthorizedWorkloadID string
	CatalogRevision      uint64
}

func (s Scope) validate() error {
	for _, value := range []string{s.OrgID, s.AppInstallationID, s.AuthorizedWorkloadID} {
		if value == "" || value != strings.TrimSpace(value) {
			return ErrInvalidInput
		}
	}
	if s.CatalogRevision == 0 {
		return ErrInvalidInput
	}
	return nil
}

type Command struct {
	CommandID          string
	AcceptanceSequence uint64
	LinkKey            string
	Scope              Scope
	Intent             outboundintentv1.Intent
	State              string
	AttemptCount       int
	ClaimedBy          string
	ClaimToken         string
	ClaimedAt          *time.Time
	LeaseExpiresAt     *time.Time
	DispatchStarted    *time.Time
	NextAttemptAt      *time.Time
	FailureCode        string
	ResponseStatus     int
	RemoteID           string
	CreatedAt          time.Time
	ModifiedAt         time.Time
}

// commandDependency is an Integration-owned, provider-neutral prerequisite for
// a command that has not crossed the provider-dispatch checkpoint. Key is the
// stable wake group; ReadinessKeys are the exact current metadata candidates
// that must be rechecked before sleeping. Both remain opaque to the scheduler.
type commandDependency struct {
	Kind          string
	Key           string
	ReadinessKeys []string
}

func validateDependency(value commandDependency) error {
	if !safeCode(value.Kind) || value.Key == "" || len(value.Key) > 255 ||
		value.Key != strings.TrimSpace(value.Key) || len(value.ReadinessKeys) == 0 {
		return ErrInvalidInput
	}
	seen := make(map[string]struct{}, len(value.ReadinessKeys))
	for _, key := range value.ReadinessKeys {
		if key == "" || len(key) > 255 || key != strings.TrimSpace(key) {
			return ErrInvalidInput
		}
		if _, duplicate := seen[key]; duplicate {
			return ErrInvalidInput
		}
		seen[key] = struct{}{}
	}
	return nil
}

type Claim struct {
	Command Command
	Token   string
}

// PreparationError is returned before provider I/O is allowed. Retryable
// errors release the claim; terminal errors reject the command. It must never
// be used for a failure after MarkDispatching.
type PreparationError struct {
	Code      string
	Retryable bool
	Cause     error
}

func (e *PreparationError) Error() string {
	if e == nil {
		return "connector outbound intent preparation failed"
	}
	if e.Cause != nil {
		return "connector outbound intent preparation failed: " + e.Cause.Error()
	}
	return "connector outbound intent preparation failed"
}

func (e *PreparationError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

// dependencyPreparationError is a pre-dispatch wait instruction for an
// Integration-owned prerequisite. Unlike PreparationError it has no deadline:
// the dependency owner must wake the durable command when readiness changes.
type dependencyPreparationError struct {
	Code       string
	Dependency commandDependency
	Cause      error
}

func (e *dependencyPreparationError) Error() string {
	if e == nil {
		return "connector outbound intent dependency preparation failed"
	}
	if e.Cause != nil {
		return "connector outbound intent dependency preparation failed: " + e.Cause.Error()
	}
	return "connector outbound intent dependency preparation failed"
}

func (e *dependencyPreparationError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

type EffectOutcome string

const (
	EffectSucceeded EffectOutcome = "succeeded"
	EffectRejected  EffectOutcome = "rejected"
	// EffectRejectedNoEffect affirmatively proves that the operation and all
	// earlier attempts performed no provider mutation. A generic HTTP refusal,
	// expired lease, or transport failure is not such proof.
	EffectRejectedNoEffect      EffectOutcome = "rejected_no_effect"
	EffectNotDispatched         EffectOutcome = "not_dispatched"
	EffectRetryableAfterSuccess EffectOutcome = "retryable_after_success"
	EffectTerminalAfterSuccess  EffectOutcome = "terminal_after_success"
	EffectUncertain             EffectOutcome = "uncertain"
	// EffectTypedTerminal means the effect supplied one bounded, provider-neutral
	// terminal result through TypedTerminalResultCheckpoint. The checkpoint is
	// the durable authority; this outcome carries no result bytes itself.
	EffectTypedTerminal EffectOutcome = "typed_terminal"
)

type EffectResult struct {
	Outcome          EffectOutcome
	ResponseStatus   int
	RemoteID         string
	FailureCode      string
	RetryAfter       time.Duration
	LinkFinalization *RemoteLinkFinalization
}

// RemoteLinkFinalization is the complete provider-neutral snapshot required
// to atomically finalize a newly observed remote identity in the existing
// Integration-owned RemoteLink authority. Existing links set Create=false and
// are identity-checked in the same transaction as command completion.
type RemoteLinkFinalization struct {
	Create                  bool
	Scope                   Scope
	LinkKey                 string
	RemoteID                string
	ConnectorOwnerKey       string
	ActionSlotID            string
	SourceObject            string
	SourceRecordID          string
	DestinationSlotID       string
	RequesterOwnerPack      string
	ProfileOwnerPack        string
	FlowDeveloperName       string
	FlowActionName          string
	TargetObject            string
	ProfileName             string
	PackInstallationID      string
	CredentialRef           string
	EndpointClass           string
	RetryPolicyJSON         string
	ProfileContractHash     string
	CreatePathTemplate      string
	UpdatePathTemplate      string
	ResponseIDPath          string
	RemoteIDField           string
	RemoteIDProvenanceField string
	MappingFingerprint      string
	BodyFingerprint         string
}

// PreparedEffect contains no secret material and performs no I/O until
// Dispatch is called after the durable dispatch-start checkpoint commits.
type PreparedEffect interface {
	Dispatch(context.Context) EffectResult
}

// TypedTerminalResult is the bounded provider-neutral result produced by a
// non-resource effect. The Worker derives its digest and closes it over the
// immutable intent before allowing the checkpoint to persist it.
//
// CanonicalResult must not contain provider response bodies, credentials,
// tokens, headers, or other secret material.
type TypedTerminalResult struct {
	ResultFormatVersion uint32
	CanonicalResult     []byte
}

// TypedTerminalResultCheckpoint durably records a typed terminal result on the
// existing outbound command before Runtime writeback. A callback error may
// mean the transaction committed but its response was lost, so effects must
// not repeat provider I/O after calling it.
type TypedTerminalResultCheckpoint func(context.Context, int, TypedTerminalResult) error

// TypedTerminalResultEffect is the additive prepared-effect interface for
// non-resource operations. Existing PreparedEffect implementations remain
// source-compatible. The effect may return an ordinary pre-dispatch outcome
// without calling the checkpoint; EffectTypedTerminal requires exactly one
// successful checkpoint call.
type TypedTerminalResultEffect interface {
	DispatchWithTypedTerminalResultCheckpoint(context.Context, TypedTerminalResultCheckpoint) EffectResult
}

// ProviderSuccessCheckpoint persists the provider identity before any
// follow-on provider operation is allowed. The callback is Integration-owned;
// provider code cannot commit Runtime state or manufacture durable authority.
type ProviderSuccessCheckpoint func(
	context.Context,
	int,
	RemoteLinkFinalization,
	string,
) error

// ProviderSuccessCheckpointEffect is implemented by effects that perform
// follow-on provider I/O after the main object mutation succeeds. The Worker
// supplies the durable checkpoint owner before that I/O begins.
type ProviderSuccessCheckpointEffect interface {
	DispatchWithProviderSuccessCheckpoint(context.Context, ProviderSuccessCheckpoint) EffectResult
}

type Preparer interface {
	Prepare(context.Context, Command) (PreparedEffect, error)
}

// ResultCommitter invokes Runtime's idempotent, metadata-authorized local
// projection after provider success has already been durably recorded by
// Integration. Repeating the same request is always safe; it never performs
// provider I/O.
type ResultCommitter interface {
	CommitOutboundResult(context.Context, outboundintentv1.CommitResultRequest) (outboundintentv1.CommitResultResponse, error)
}
