package records

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

	"github.com/nexuscrm/automation/autonumber"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

// autonumberTokenResolveErr classifies autonumber engine errors for the HTTP
// layer. ErrTokenResolve is a user-recoverable parent-data condition (a field
// referenced by the autonumber format is NULL or the parent row is missing) —
// SF returns FIELD_INTEGRITY_EXCEPTION (400) for analogous cases. All other
// autonumber errors (including ErrChainResolverFailure for DB / wiring failures)
// remain 500 InternalError so infrastructure problems are not misreported as
// user-fixable data errors.
//
// The structured tokenResolveError (produced by cross_record.go:resolveSingleRef)
// carries the token literal; when present the message names the exact field.
// ErrChainResolverFailure does NOT wrap ErrTokenResolve — the first branch below
// catches it and routes to InternalError (500).
func autonumberTokenResolveErr(objectName, fieldAPIName string, err error) error {
	if !errors.Is(err, autonumber.ErrTokenResolve) {
		return appErrors.NewInternalError("failed to assign auto-numbers", err)
	}
	lit, ok := autonumber.TokenResolveDetail(err)
	if !ok {
		// ErrTokenResolve sentinel without the structured tokenResolveError type:
		// null FK on the source record, missing parent row, or a DB/resolver
		// failure inside LoadTarget. These are not distinguishably user-caused,
		// so fail-closed as 500 InternalError to avoid hiding infra failures.
		return appErrors.NewInternalError("failed to assign auto-numbers: parent record could not be loaded", err)
	}
	// Structured tokenResolveError: LoadTarget reached the parent record but
	// the target field is null/empty — user-recoverable, return 400.
	clean := strings.Trim(lit, "{}")
	msg := fmt.Sprintf("Cannot assign %s.%s: autonumber token %s could not be resolved (the referenced parent field is null or the parent record is missing). Populate %s on the parent record and retry.", objectName, fieldAPIName, clean, clean)
	return appErrors.NewFieldIntegrityError(msg, clean)
}

// assignAutoNumbers invokes the autonumber v2 engine for every
// `_System_Field` on `objectName` whose `AutoNumberFormat` is non-empty.
// Called inside the insert transaction BEFORE publishRecordEvent so
// triggers and flows see the rendered identifier. Replaces the deleted
// legacy generateAutoNumbers + CompoundAutoNumberListener path (PRD §7.2).
func (ps *PersistenceService) assignAutoNumbers(ctx context.Context, objectName string, data models.SObject) error {
	if ps.autoNumber == nil || ps.autoNumberMeta == nil {
		return nil
	}
	fields, err := ps.autoNumberFields(ctx, objectName)
	if err != nil {
		return appErrors.NewInternalError("failed to resolve auto-number fields", err)
	}
	if len(fields) == 0 {
		return nil
	}
	for _, fm := range fields {
		// Respect keep_existing semantics (PRD §3.3): do not overwrite
		// a non-blank value that the caller or a prior listener set.
		if existing, ok := data[fm.FieldAPIName]; ok && !isBlankValue(existing) {
			continue
		}
		rendered, err := ps.autoNumber.AssignOne(ctx, fm.FieldID, data)
		if err != nil {
			return autonumberTokenResolveErr(objectName, fm.FieldAPIName, err)
		}
		data[fm.FieldAPIName] = rendered
	}
	return nil
}

type previewAutoNumberEngine interface {
	PreviewOne(ctx context.Context, fieldID string, sourceRecord models.SObject) (string, error)
}

func (ps *PersistenceService) assignPreviewAutoNumbers(ctx context.Context, objectName string, data models.SObject) error {
	if ps.autoNumber == nil || ps.autoNumberMeta == nil {
		return nil
	}
	fields, err := ps.autoNumberFields(ctx, objectName)
	if err != nil {
		return appErrors.NewInternalError("failed to resolve auto-number fields", err)
	}
	if len(fields) == 0 {
		return nil
	}
	previewEngine, ok := ps.autoNumber.(previewAutoNumberEngine)
	if !ok {
		return appErrors.NewInternalError("failed to preview auto-numbers",
			fmt.Errorf("autonumber engine does not support no-DML preview"))
	}
	for _, fm := range fields {
		if existing, ok := data[fm.FieldAPIName]; ok && !isBlankValue(existing) {
			continue
		}
		rendered, err := previewEngine.PreviewOne(ctx, fm.FieldID, data)
		if err != nil {
			return autonumberTokenResolveErr(objectName, fm.FieldAPIName, err)
		}
		data[fm.FieldAPIName] = rendered
	}
	return nil
}

// assignAutoNumbersBatch is the batch counterpart to assignAutoNumbers,
// invoked by BulkInsert. Uses engine.AssignBatch so a single reservation
// covers the whole batch for each field (PRD §5.3 fixpack F13). Records
// that already carry a non-blank value for a field are skipped — the
// engine still reserves for the rest so index alignment is preserved.
func (ps *PersistenceService) assignAutoNumbersBatch(ctx context.Context, objectName string, records []models.SObject) error {
	if ps.autoNumber == nil || ps.autoNumberMeta == nil {
		return nil
	}
	if len(records) == 0 {
		return nil
	}
	fields, err := ps.autoNumberFields(ctx, objectName)
	if err != nil {
		return appErrors.NewInternalError("failed to resolve auto-number fields", err)
	}
	if len(fields) == 0 {
		return nil
	}
	for _, fm := range fields {
		// Partition: records that need assignment vs records already
		// carrying a value. Only the former go to AssignBatch.
		var needIdx []int
		var needRecords []models.SObject
		for i, r := range records {
			if existing, ok := r[fm.FieldAPIName]; ok && !isBlankValue(existing) {
				continue
			}
			needIdx = append(needIdx, i)
			needRecords = append(needRecords, r)
		}
		if len(needRecords) == 0 {
			continue
		}
		values, err := ps.autoNumber.AssignBatch(ctx, fm.FieldID, needRecords)
		if err != nil {
			return autonumberTokenResolveErr(objectName, fm.FieldAPIName, err)
		}
		if len(values) != len(needRecords) {
			return appErrors.NewInternalError("autonumber assign-batch: engine returned wrong count",
				fmt.Errorf("%s.%s: got %d want %d", objectName, fm.FieldAPIName, len(values), len(needRecords)))
		}
		for j, i := range needIdx {
			records[i][fm.FieldAPIName] = values[j]
		}
	}
	return nil
}
