package metadata

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

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

// BatchCreateFields creates or reconciles fields for an object using batch DDL.
// Returns the list of fields that were successfully processed/validated for further action (e.g. permissions)
func (ms *MetadataService) BatchCreateFields(ctx context.Context, objectAPIName string, fields []models.FieldMetadata) ([]*models.FieldMetadata, error) {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	if len(fields) == 0 {
		return nil, nil
	}

	// 1. Get the object
	obj, err := ms.repo.GetSchemaByAPIName(ctx, objectAPIName)
	if err != nil || obj == nil {
		return nil, apperrors.NewNotFoundError("Object", objectAPIName)
	}
	objectAPIName = obj.APIName

	var batchFields []models.FieldMetadata
	var validFields []*models.FieldMetadata
	declaredFieldNames := make(map[string]bool, len(fields))
	discriminatorParents := make(map[string]string)
	for _, field := range fields {
		fieldKey := strings.ToLower(field.APIName)
		if declaredFieldNames[fieldKey] {
			return nil, apperrors.NewValidationError("fields", fmt.Sprintf("field %q is duplicated in the create request", field.APIName))
		}
		declaredFieldNames[fieldKey] = true
		// Use the resolver (authored-write) so multi-target lookups and
		// references derive polymorphic ownership through the same SSoT.
		f := field
		if models.ResolveFieldRelationshipSemantics(&f, models.LifecycleAuthoredWrite).IsPolymorphic {
			discriminatorParents[strings.ToLower(GetPolymorphicTypeColumnName(field.APIName))] = field.APIName
		}
	}
	// This entrypoint also owns idempotent reconciliation, unlike CreateField.
	// Replaying an existing field keeps its persisted incarnation. Only a new
	// metadata row (including adoption of an orphan physical column) gets a new
	// identity; caller-supplied IDs are never authoritative in either case.
	existingIDs := make(map[string]string, len(obj.Fields))
	for _, existing := range obj.Fields {
		existingIDs[strings.ToLower(existing.APIName)] = existing.ID
	}
	assignIncarnation := func(field *models.FieldMetadata) {
		if id, exists := existingIDs[strings.ToLower(field.APIName)]; exists {
			field.ID = id
		} else {
			mintInteractiveFieldIncarnation(field)
		}
	}

	// 2. Process and Validate each field
	for i := range fields {
		field := &fields[i]
		if field.TrackHistory {
			return nil, apperrors.NewValidationError(constants.FieldSysField_TrackHistory, "new fields must be created with trackHistory disabled; enable it in a separate committed transition")
		}
		normalizeCreatedFormulaField(field)
		assignIncarnation(field)

		if err := ValidateFormulaAuthoringContract(obj.APIName, field.APIName, field.Type, field.Formula); err != nil {
			return nil, err
		}

		// Basic Validation (Copy of CreateField logic)
		if ms.validationSvc != nil {
			if err := ms.validationSvc.ValidateFieldMetadata(field); err != nil {
				return nil, apperrors.NewValidationError(field.APIName, fmt.Sprintf("invalid field: %v", err))
			}
		}
		if err := ms.validateWriteMaterializedFormulaField(ctx, obj, field); err != nil {
			return nil, err
		}
		if field.ProjectionConfig != nil {
			return nil, apperrors.NewValidationError(field.APIName, "field projection config is internal and can only be declared by standard metadata")
		}

		// Regex Validation
		validName := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
		if !validName.MatchString(field.APIName) {
			return nil, apperrors.NewValidationError("api_name", fmt.Sprintf("invalid field API name '%s'", field.APIName))
		}

		// Allow existing fields to pass through to Repo for adoption (e.g. Ensure Unique Constraint)
		// Repo handles idempotency.
		// Resolve polymorphic ownership through the single SSoT. Normalize
		// the type so persisted multi-target lookup rows round-trip as
		// polymorphic after reload.
		if authoredSem := models.ResolveFieldRelationshipSemantics(field, models.LifecycleAuthoredWrite); authoredSem.IsPolymorphic {
			field.IsPolymorphic = true
			field.Type = authoredSem.CanonicalType
		}
		isDiscriminatorField := false
		if discriminatorParent := discriminatorParents[strings.ToLower(field.APIName)]; discriminatorParent != "" {
			field.GeneratedForPolymorphicReference = discriminatorParent
			field.Createable = false
			field.Updateable = false
			isDiscriminatorField = true
		}

		batchFields = append(batchFields, *field) // Copy for batch
		if !isDiscriminatorField {
			validFields = append(validFields, field) // Ptr for return
		}

		// Polymorphic Extra Column. A transitional single-target reference may
		// explicitly set IsPolymorphic so the discriminator exists before public
		// multi-target CRUD is enabled.
		if field.Type == constants.FieldTypeReference && field.IsPolymorphic {
			typeColName := GetPolymorphicTypeColumnName(field.APIName)
			if declaredFieldNames[strings.ToLower(typeColName)] {
				continue
			}

			// Synthesize FieldMetadata for the type column
			typeField := models.FieldMetadata{
				APIName:                          typeColName,
				Label:                            field.Label + " Type",
				Type:                             constants.FieldTypeString,
				Required:                         false,
				IsSystem:                         false,
				Createable:                       false,
				Updateable:                       false,
				GeneratedForPolymorphicReference: field.APIName,
			}
			assignIncarnation(&typeField)
			// Note: CreateField previously used `AddColumn` with `ColumnDefinition`.
			// `AddColumn` calls `registerField` which registers it.
			// So we MUST include it in `batchFields` to be created and registered.

			batchFields = append(batchFields, typeField)
		}
	}

	if len(batchFields) == 0 {
		return validFields, nil
	}

	proposedObj := objectWithFieldCandidates(obj, validFields)
	if err := validateSiblingMaxFieldsForObject(proposedObj, validFields); err != nil {
		return nil, err
	}

	batchAuthorsRollup := false
	for _, field := range validFields {
		if constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeRollupSummary {
			batchAuthorsRollup = true
			break
		}
	}

	if batchAuthorsRollup {
		if err := ms.validateRollupGraphForObject(ctx, proposedObj); err != nil {
			return nil, err
		}
	}

	var layoutAdmissionRollbacks []func() error
	rollbackLayoutAdmissions := func(cause error) error {
		var rollbackErrs []error
		for i := len(layoutAdmissionRollbacks) - 1; i >= 0; i-- {
			rollback := layoutAdmissionRollbacks[i]
			if rollback == nil {
				continue
			}
			if rollbackErr := rollback(); rollbackErr != nil {
				log.Printf("⚠️ Failed to roll back layout admission for %s batch field create: %v", objectAPIName, rollbackErr)
				rollbackErrs = append(rollbackErrs, rollbackErr)
			}
		}
		if len(rollbackErrs) > 0 {
			return fieldCreateFailureWithLayoutRollbackFailures(cause, rollbackErrs...)
		}
		return cause
	}
	for _, field := range validFields {
		rollback, err := ms.addFieldToLayoutWithRollback(ctx, objectAPIName, field)
		if err != nil {
			return nil, rollbackLayoutAdmissions(err)
		}
		layoutAdmissionRollbacks = append(layoutAdmissionRollbacks, rollback)
		rollback, err = ms.addRelationshipFieldToParentLayoutsWithRollback(ctx, obj, field)
		if err != nil {
			return nil, rollbackLayoutAdmissions(err)
		}
		layoutAdmissionRollbacks = append(layoutAdmissionRollbacks, rollback)
	}

	// 3. Execute Batch DDL via Repository
	if err := ms.schemaMgr.BatchAddColumns(objectAPIName, batchFields); err != nil {
		var returnedErr error
		var appErr apperrors.AppError
		if errors.As(err, &appErr) {
			returnedErr = err
		} else {
			returnedErr = apperrors.NewInternalError("batch creation failed", err)
		}
		return nil, rollbackLayoutAdmissions(returnedErr)
	}

	// Index relationship FK columns, mirroring CreateField and the object-create
	// paths. BatchAddColumns adds a FK constraint (which TiDB auto-indexes) only
	// for single-target, non-polymorphic, non-"parent" references; polymorphic /
	// "parent" lookups get no constraint and would otherwise full-scan
	// related-list counts. The helper skips non-relationship and system fields,
	// and is idempotent where a FK-constraint index already exists. `fields`
	// carries the reference fields (discriminators live only in batchFields).
	ms.ensureRelationshipFKIndexes(objectAPIName, fields)

	// 4. Post-Creation: Layouts & Auto-Numbers (Loop)
	sawAutoNumber := false
	for _, field := range validFields {
		// AutoNumber (v2 engine — PRD §7.2 step k)
		if field.Type == constants.FieldTypeAutonumber {
			format := "{0}"
			if field.DefaultValue != nil && *field.DefaultValue != "" {
				format = *field.DefaultValue
			}
			if err := ms.repo.ConfigureAutoNumberField(ctx, objectAPIName, field.APIName, format, 1); err != nil {
				log.Printf("⚠️ Failed to configure auto-number field %s.%s: %v", objectAPIName, field.APIName, err)
			}
			sawAutoNumber = true
		}
	}
	// Round-5 Codex V1-P2 / V2-H3: invalidate the autonumber engine's
	// MetadataSource once per batch so the new fields become visible
	// to LookupByObject without a process restart. Done once after
	// the loop to avoid N full refreshes for a large batch.
	if sawAutoNumber && ms.autoNumberInvalidator != nil {
		if err := ms.autoNumberInvalidator.InvalidateObject(ctx, objectAPIName); err != nil {
			log.Printf("⚠️ Failed to invalidate autonumber metadata for %s: %v",
				objectAPIName, err)
		}
	}

	ms.invalidateCacheLocked()

	// Publish FieldCreated events for downstream listeners (for example formula views).
	for _, field := range validFields {
		ms.publishMetadataEvent(ctx, sharedEvents.FieldCreated, sharedEvents.SchemaEventPayload{
			EventType:     sharedEvents.FieldCreated,
			ObjectAPIName: objectAPIName,
			FieldAPIName:  field.APIName,
			Timestamp:     time.Now().Format(time.RFC3339),
		})
	}

	return validFields, nil
}

func fieldCreateFailureWithLayoutRollbackFailures(cause error, rollbackErrs ...error) error {
	errs := make([]error, 0, len(rollbackErrs)+1)
	if cause != nil {
		errs = append(errs, cause)
	}
	for _, rollbackErr := range rollbackErrs {
		if rollbackErr == nil {
			continue
		}
		errs = append(errs, fmt.Errorf("page layout admission rollback failed: %w", rollbackErr))
	}
	if len(errs) <= 1 {
		return cause
	}
	return apperrors.NewInternalError(
		"field creation failed and page layout admission rollback failed",
		errors.Join(errs...),
	)
}
