package connector_inbound

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
)

// BatchReadClient is the contract the worker holds against the L0 outbound-
// callout engine (connector_callout, NCRM-INT-10, not yet on main).
//
// When connector_callout lands, its Engine.BatchRead method satisfies this
// interface and callers pass *connector_callout.Engine in production wiring.
//
// The worker batch-fetches detail records by source ID before invoking Flow.
// Returns a map from source-id → opaque record JSON. An absent entry means
// record enrichment is unavailable for that source ID; payload-only startup
// contracts may still route the event, but the platform must not treat absent
// enrichment as a deletion signal. [ErrSourceRecordInactive] is the distinct,
// explicit outcome for a source whose inactivity was confirmed by a successful
// pack-declared probe.
type BatchReadClient interface {
	BatchRead(ctx context.Context, namespace, objectKind string, sourceIDs []string) (map[string]json.RawMessage, error)
}

// BatchReadOutcome carries independent source results from one provider batch.
// Details contains successfully read records. SourceErrors contains only
// source-scoped failures; a confirmed inactive source wraps
// [ErrSourceRecordInactive]. A non-nil method error remains batch-global and
// means no source may consume the response as authoritative.
type BatchReadOutcome struct {
	Details      map[string]json.RawMessage
	SourceErrors map[string]error
}

// BatchReadOutcomeClient is an additive capability for clients that can
// preserve independent outcomes when one provider batch covers many sources.
// BatchReadClient remains unchanged for existing custom integrations.
type BatchReadOutcomeClient interface {
	BatchReadOutcome(ctx context.Context, namespace, objectKind string, sourceIDs []string) (BatchReadOutcome, error)
}

// BatchReadSourceErrors transports per-source outcomes through a wrapper that
// exposes only BatchReadClient. It never includes provider records or source IDs
// in Error(), while Unwrap preserves errors.Is/errors.As classification.
type BatchReadSourceErrors struct {
	sourceErrors map[string]error
}

func (e *BatchReadSourceErrors) Error() string {
	if e == nil {
		return "connector_inbound: per-source batch read outcomes"
	}
	return fmt.Sprintf("connector_inbound: %d per-source batch read outcomes", len(e.sourceErrors))
}

func (e *BatchReadSourceErrors) Unwrap() []error {
	if e == nil {
		return nil
	}
	causes := make([]error, 0, len(e.sourceErrors))
	for _, cause := range e.sourceErrors {
		if cause != nil {
			causes = append(causes, cause)
		}
	}
	return causes
}

// SourceErrors returns a defensive copy of the source-scoped failures.
func (e *BatchReadSourceErrors) SourceErrors() map[string]error {
	if e == nil {
		return nil
	}
	return cloneBatchReadSourceErrors(e.sourceErrors)
}

// ReadBatchOutcome consumes the additive outcome capability when present and
// reconstructs it from BatchReadSourceErrors when an ordinary wrapper carried
// the same evidence through BatchReadClient.
func ReadBatchOutcome(
	ctx context.Context,
	client BatchReadClient,
	namespace string,
	objectKind string,
	sourceIDs []string,
) (BatchReadOutcome, error) {
	if outcomeClient, ok := client.(BatchReadOutcomeClient); ok {
		outcome, err := outcomeClient.BatchReadOutcome(ctx, namespace, objectKind, sourceIDs)
		if err != nil {
			return BatchReadOutcome{}, err
		}
		return normalizeBatchReadOutcome(outcome), nil
	}
	details, err := client.BatchRead(ctx, namespace, objectKind, sourceIDs)
	outcome := BatchReadOutcome{Details: details}
	var sourceErrors *BatchReadSourceErrors
	if errors.As(err, &sourceErrors) && sourceErrors != nil {
		outcome.SourceErrors = sourceErrors.SourceErrors()
		return normalizeBatchReadOutcome(outcome), nil
	}
	if err != nil && len(sourceIDs) == 1 {
		outcome.SourceErrors = map[string]error{sourceIDs[0]: err}
		return normalizeBatchReadOutcome(outcome), nil
	}
	if err != nil {
		return BatchReadOutcome{}, err
	}
	return normalizeBatchReadOutcome(outcome), nil
}

// CollapseBatchReadOutcome preserves the legacy BatchReadClient surface. One
// singleton failure is returned directly; a multi-source call uses the
// aggregate carrier so wrappers never collapse one source failure onto every
// sibling in the provider batch.
func CollapseBatchReadOutcome(outcome BatchReadOutcome, sourceIDs []string) (map[string]json.RawMessage, error) {
	outcome = normalizeBatchReadOutcome(outcome)
	if len(outcome.SourceErrors) == 0 {
		return outcome.Details, nil
	}
	if len(sourceIDs) == 1 {
		if cause := outcome.SourceErrors[sourceIDs[0]]; cause != nil {
			return outcome.Details, cause
		}
	}
	return outcome.Details, &BatchReadSourceErrors{
		sourceErrors: cloneBatchReadSourceErrors(outcome.SourceErrors),
	}
}

func normalizeBatchReadOutcome(outcome BatchReadOutcome) BatchReadOutcome {
	if outcome.Details == nil {
		outcome.Details = map[string]json.RawMessage{}
	}
	outcome.SourceErrors = cloneBatchReadSourceErrors(outcome.SourceErrors)
	return outcome
}

func cloneBatchReadSourceErrors(source map[string]error) map[string]error {
	if len(source) == 0 {
		return nil
	}
	cloned := make(map[string]error, len(source))
	for sourceID, cause := range source {
		if cause != nil {
			cloned[sourceID] = cause
		}
	}
	if len(cloned) == 0 {
		return nil
	}
	return cloned
}

// noopBatchReadClient returns empty maps. Used when a Worker is constructed
// without an explicit client (test fixtures, dispatcher-only integrations).
// Production workers MUST inject the real INT-10 engine.
type noopBatchReadClient struct{}

func (noopBatchReadClient) BatchRead(_ context.Context, _, _ string, _ []string) (map[string]json.RawMessage, error) {
	return map[string]json.RawMessage{}, nil
}

func NewNoopBatchReadClient() BatchReadClient {
	return noopBatchReadClient{}
}
