package connector_delivery

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"strings"
	"time"

	connector_oauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
)

const (
	StateReserved    = "reserved"
	StateDispatching = "dispatching"
	StateAccepted    = "accepted"
	StateCompleted   = "completed"
	StateFailed      = "failed"
	StateUncertain   = "uncertain"

	DefaultDispatchLease = 2 * time.Minute
)

var (
	ErrInvalidInput          = errors.New("connected delivery input is invalid")
	ErrDependencyMissing     = errors.New("connected delivery dependency is missing")
	ErrRequestConflict       = errors.New("connected delivery request ID conflicts with existing input")
	ErrDispatchInProgress    = errors.New("connected delivery is already in progress")
	ErrCredentialUnavailable = errors.New("connected delivery credential is temporarily unavailable")
	ErrDeliveryFailed        = errors.New("connected delivery failed before remote acceptance")
	ErrDeliveryUncertain     = errors.New("connected delivery remote effect is uncertain")
	ErrCompletionInvalid     = errors.New("connected delivery cannot be completed from its current state")
	ErrDeliveryNotFound      = errors.New("connected delivery was not found")
	ErrOwnershipLost         = errors.New("connected delivery ownership was lost")
	ErrStateInvalid          = errors.New("connected delivery state is invalid")
)

type ProviderErrorKind string

const (
	// ProviderErrorRejected proves that the remote system did not accept the
	// delivery. It is terminal and never retried by this package.
	ProviderErrorRejected ProviderErrorKind = "rejected"
	// ProviderErrorAuthorization proves that no delivery was accepted because
	// the credential was rejected before the delivery operation took effect.
	ProviderErrorAuthorization ProviderErrorKind = "authorization"
	// ProviderErrorUncertain means the remote system may have accepted the
	// delivery. Automatic retry is forbidden.
	ProviderErrorUncertain ProviderErrorKind = "uncertain"
)

// ProviderError contains only allowlisted delivery failure metadata. Provider
// response bodies, headers, and credentials must not cross this boundary.
type ProviderError struct {
	Kind ProviderErrorKind
	Code string
}

func (e *ProviderError) Error() string {
	if e == nil {
		return "connected delivery provider failed"
	}
	return "connected delivery provider failed: " + safeCode(e.Code, "provider_error")
}

type DispatchRequest struct {
	AccessToken string
	Payload     []byte
}

type DispatchOutcome string

const (
	DispatchAccepted DispatchOutcome = "accepted"
	DispatchRejected DispatchOutcome = "rejected"
)

type DispatchResult struct {
	Outcome              DispatchOutcome
	RemoteReceiptID      string
	RemoteConversationID string
	ResponseStatus       int
}

// Provider performs exactly one externally visible delivery attempt.
// EffectTerminationBound is the maximum time Dispatch may take to return after
// its context is canceled. It covers every local and remote operation capable
// of producing the effect. Implementations must not detach requests, retry
// transparently, or continue effect-producing work after Dispatch returns.
type Provider interface {
	EffectTerminationBound() time.Duration
	Dispatch(context.Context, DispatchRequest) (DispatchResult, error)
}

type ProviderResolver interface {
	Provider(connectorKey string) (Provider, bool)
}

type OAuthResolver interface {
	Principal(context.Context, string, string) (*connector_oauth.ConnectedPrincipal, error)
	ResolveAccessToken(context.Context, connector_oauth.ResolveAccessTokenRequest) (string, error)
}

type Request struct {
	RequestID          string
	UserID             string
	ConnectorKey       string
	ExpectedGeneration int64
	Payload            []byte
	SourceObject       string
	SourceRecordID     string
	CompletionSnapshot json.RawMessage
}

type Result struct {
	RequestID            string
	State                string
	CompletionSnapshot   json.RawMessage
	RemoteReceiptID      string
	RemoteConversationID string
	ResponseStatus       int
	FailureCode          string
	AcceptedAt           *time.Time
}

type Job struct {
	RequestID            string
	ConnectedPrincipalID string
	PrincipalGeneration  int64
	OriginatingUserID    string
	State                string
	PayloadSHA256        string
	PayloadEncodedSize   int64
	SourceObject         string
	SourceRecordID       string
	CompletionSnapshot   json.RawMessage
	ClaimToken           string
	LeaseExpiresAt       *time.Time
	Fence                int64
	DispatchStartedAt    *time.Time
	AcceptedAt           *time.Time
	RemoteReceiptID      string
	RemoteConversationID string
	ResponseStatus       *int
	ResultEvidence       json.RawMessage
	FailureCode          string
	LocalAttemptCount    int
	NextLocalAttemptAt   *time.Time
	CompletedAt          *time.Time
	CreatedDate          time.Time
	LastModifiedDate     time.Time
}

type dispatchClaim struct {
	Job        Job
	ClaimToken string
	Fence      int64
}

func validateJobShape(job Job) error {
	if strings.TrimSpace(job.RequestID) == "" || strings.TrimSpace(job.ConnectedPrincipalID) == "" ||
		strings.TrimSpace(job.OriginatingUserID) == "" || job.PrincipalGeneration <= 0 ||
		len(job.PayloadSHA256) != 64 || job.PayloadEncodedSize <= 0 ||
		strings.TrimSpace(job.SourceObject) == "" || strings.TrimSpace(job.SourceRecordID) == "" ||
		!validJSONWithUniqueObjectKeys(job.CompletionSnapshot) || job.Fence < 0 || job.LocalAttemptCount != 0 ||
		job.NextLocalAttemptAt != nil || len(job.ResultEvidence) != 0 ||
		len(job.ClaimToken) > 255 || len(job.RemoteReceiptID) > 255 ||
		len(job.RemoteConversationID) > 255 || len(job.FailureCode) > 64 ||
		job.ClaimToken != strings.TrimSpace(job.ClaimToken) ||
		job.RemoteReceiptID != strings.TrimSpace(job.RemoteReceiptID) ||
		job.RemoteConversationID != strings.TrimSpace(job.RemoteConversationID) {
		return ErrStateInvalid
	}
	if _, err := hex.DecodeString(job.PayloadSHA256); err != nil {
		return ErrStateInvalid
	}
	if job.FailureCode != "" && safeCode(job.FailureCode, "") != job.FailureCode {
		return ErrStateInvalid
	}
	if job.ResponseStatus != nil && (*job.ResponseStatus < 100 || *job.ResponseStatus > 599) {
		return ErrStateInvalid
	}

	noClaim := job.ClaimToken == "" && job.LeaseExpiresAt == nil
	noRemoteResult := job.AcceptedAt == nil && job.RemoteReceiptID == "" &&
		job.RemoteConversationID == "" && job.ResponseStatus == nil
	switch job.State {
	case StateReserved:
		if !noClaim || job.DispatchStartedAt != nil || !noRemoteResult ||
			job.FailureCode != "" || job.CompletedAt != nil {
			return ErrStateInvalid
		}
	case StateDispatching:
		if job.ClaimToken == "" || job.LeaseExpiresAt == nil || job.Fence <= 0 ||
			job.DispatchStartedAt == nil || !job.LeaseExpiresAt.After(*job.DispatchStartedAt) ||
			!noRemoteResult || job.FailureCode != "" || job.CompletedAt != nil {
			return ErrStateInvalid
		}
	case StateAccepted, StateCompleted:
		if !noClaim || job.Fence <= 0 || job.DispatchStartedAt == nil || job.AcceptedAt == nil ||
			job.RemoteReceiptID == "" || job.ResponseStatus == nil || *job.ResponseStatus < 200 ||
			*job.ResponseStatus >= 300 || job.FailureCode != "" {
			return ErrStateInvalid
		}
		if (job.State == StateAccepted && job.CompletedAt != nil) ||
			(job.State == StateCompleted && job.CompletedAt == nil) {
			return ErrStateInvalid
		}
	case StateFailed:
		if !noClaim || job.AcceptedAt != nil || job.RemoteReceiptID != "" ||
			job.RemoteConversationID != "" || job.FailureCode == "" || job.CompletedAt == nil ||
			(job.DispatchStartedAt == nil &&
				(job.ResponseStatus != nil || job.FailureCode != "connection_generation_changed")) ||
			(job.DispatchStartedAt != nil && job.Fence <= 0) {
			return ErrStateInvalid
		}
	case StateUncertain:
		if !noClaim || job.Fence <= 0 || job.DispatchStartedAt == nil ||
			job.AcceptedAt != nil || job.FailureCode == "" || job.CompletedAt == nil {
			return ErrStateInvalid
		}
	default:
		return ErrStateInvalid
	}
	return nil
}
