package metadata

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/formulastorage"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/formula"
	domainSchema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/kernel/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// GenerateID creates a new UUID string
func GenerateID() string {
	return utils.GenerateID()
}

// GenerateObjectID generates a standardized ID for an object based on its API Name
func GenerateObjectID(apiName string) string {
	return domainSchema.GenerateObjectID(apiName)
}

// GenerateFieldID generates a standardized ID for a field
func GenerateFieldID(objectAPIName, fieldAPIName string) string {
	return domainSchema.GenerateFieldID(objectAPIName, fieldAPIName)
}

// GetPolymorphicTypeColumnName returns the standardized column name for the type discriminator
func GetPolymorphicTypeColumnName(fieldAPIName string) string {
	return schemautil.GetPolymorphicTypeColumnName(fieldAPIName)
}

// GenerateAutoNumberID generates a standardized ID for an auto-number sequence
func GenerateAutoNumberID(objectAPIName, fieldAPIName string) string {
	return domainSchema.GenerateAutoNumberID(objectAPIName, fieldAPIName)
}

// resolveRollupSourceFieldSQLType looks up the physical SQL type of the source
// field referenced by a RollupConfig. For COUNT (which has no source field), it
// returns an empty string. Nested rollupsummary source fields are resolved
// recursively through the same authoring contract so metadata CRUD can validate
// rollup-on-rollup chains using the same physical storage type that DDL would
// apply.
func (ms *MetadataService) resolveRollupSourceFieldSQLType(ctx context.Context, cfg *models.RollupConfig) (string, error) {
	return ms.resolveRollupSourceFieldSQLTypeSeen(ctx, cfg, nil)
}

func (ms *MetadataService) rollupSchemaLookupFunc(ctx context.Context) domainSchema.SchemaLookupFunc {
	return func(objectAPIName string) (*models.ObjectMetadata, error) {
		trimmed := strings.TrimSpace(objectAPIName)
		if trimmed == "" {
			return nil, nil
		}
		return ms.repo.GetSchemaByAPIName(ctx, trimmed)
	}
}

func (ms *MetadataService) resolveFieldStorageType(ctx context.Context, objectAPIName string, field *models.FieldMetadata) (string, *models.RollupConfig, error) {
	if !domainSchema.IsRollupSummaryType(field.Type) {
		if field.RollupConfig != nil {
			return "", nil, fmt.Errorf("rollupConfig requires field type rollupsummary")
		}
		return ms.schemaMgr.MapFieldTypeToSQL(string(field.Type)), nil, nil
	}
	if field.RollupConfig == nil {
		return "", nil, fmt.Errorf("rollupsummary fields require a rollupConfig")
	}

	sourceFieldSQLType, err := ms.resolveRollupSourceFieldSQLType(ctx, field.RollupConfig)
	if err != nil {
		return "", nil, fmt.Errorf("cannot resolve source field SQL type for rollupsummary: %v", err)
	}
	resolved, err := domainSchema.ResolveRollup(objectAPIName, *field.RollupConfig, ms.rollupSchemaLookupFunc(ctx), sourceFieldSQLType)
	if err != nil {
		return "", nil, fmt.Errorf("invalid rollupsummary config: %v", err)
	}
	applyResolvedRollupResult(field, resolved)

	return resolved.SQLType, domainSchema.CloneRollupConfig(&resolved.CanonicalConfig), nil
}

func applyResolvedRollupResult(field *models.FieldMetadata, resolved domainSchema.ResolvedRollup) {
	if field == nil {
		return
	}
	returnType := resolved.Result.Type
	field.ReturnType = &returnType
	field.Precision = cloneResolvedRollupInt(resolved.Result.Precision)
	field.Scale = cloneResolvedRollupInt(resolved.Result.Scale)
}

func cloneResolvedRollupInt(value *int) *int {
	if value == nil {
		return nil
	}
	cloned := *value
	return &cloned
}

func (ms *MetadataService) validateRollupGraphForObject(ctx context.Context, obj *models.ObjectMetadata) error {
	if obj == nil || !objectHasRollupFields(obj) {
		return nil
	}

	allSchemas, err := ms.repo.GetAllSchemas(ctx)
	if err != nil {
		return apperrors.NewInternalError("failed to list schemas for rollup graph validation", err)
	}

	replaced := false
	for i, existing := range allSchemas {
		if existing != nil && strings.EqualFold(existing.APIName, obj.APIName) {
			allSchemas[i] = obj
			replaced = true
			break
		}
	}
	if !replaced {
		allSchemas = append(allSchemas, obj)
	}

	if err := domainSchema.ValidateRollupGraph(allSchemas, obj.APIName); err != nil {
		return apperrors.NewValidationError("rollupConfig", err.Error())
	}
	return nil
}

func objectHasRollupFields(obj *models.ObjectMetadata) bool {
	for _, field := range obj.Fields {
		if field.RollupConfig != nil {
			return true
		}
	}
	return false
}

func buildFormulaSchemaMap(allSchemas []*models.ObjectMetadata, objectSchema *models.ObjectMetadata) map[string]*models.ObjectMetadata {
	out := make(map[string]*models.ObjectMetadata, len(allSchemas)+1)
	for _, schema := range allSchemas {
		if schema == nil {
			continue
		}
		apiName := strings.TrimSpace(schema.APIName)
		if apiName == "" {
			continue
		}
		out[apiName] = schema
	}
	if objectSchema != nil {
		apiName := strings.TrimSpace(objectSchema.APIName)
		if apiName != "" {
			out[apiName] = objectSchema
		}
	}
	return out
}

func formulaExpectedType(field *models.FieldMetadata) *models.FieldType {
	if field == nil {
		return nil
	}
	if field.ReturnType != nil && strings.TrimSpace(string(*field.ReturnType)) != "" {
		fieldType := models.FieldType(constants.NormalizeFieldType(string(*field.ReturnType)))
		return &fieldType
	}
	if strings.TrimSpace(string(field.Type)) == "" {
		return nil
	}
	fieldType := models.FieldType(constants.NormalizeFieldType(string(field.Type)))
	return &fieldType
}

func (ms *MetadataService) validateWriteMaterializedFormulaField(ctx context.Context, objectSchema *models.ObjectMetadata, field *models.FieldMetadata) error {
	if ms == nil || field == nil {
		return nil
	}
	resolved, err := formulastorage.Resolve(formulastorage.SpecFromField(field))
	if err != nil {
		return apperrors.NewValidationError(field.APIName, err.Error())
	}
	if !resolved.IsWriteMaterialized() {
		return nil
	}
	allSchemas, err := ms.repo.GetAllSchemas(ctx)
	if err != nil {
		return apperrors.NewInternalError("failed to list schemas for write-materialized formula validation", err)
	}
	if err := formula.ValidateWriteMaterializedFormulaRef(formula.WriteMaterializedFormulaRef{
		FormulaMode:            derefString(field.FormulaMode),
		FormulaStorageMode:     derefString(field.FormulaStorageMode),
		Formula:                derefString(field.Formula),
		HasProjectionConfig:    field.ProjectionConfig != nil,
		Schema:                 objectSchema,
		SchemasByObjectAPIName: buildFormulaSchemaMap(allSchemas, objectSchema),
		ExpectedType:           formulaExpectedType(field),
	}, formula.NewEngine()); err != nil {
		objectName := ""
		if objectSchema != nil {
			objectName = strings.TrimSpace(objectSchema.APIName)
		}
		if objectName != "" {
			return apperrors.NewValidationError(field.APIName, fmt.Sprintf("object %q field %q write-materialized formula validation failed: %v", objectName, field.APIName, err))
		}
		return apperrors.NewValidationError(field.APIName, fmt.Sprintf("field %q write-materialized formula validation failed: %v", field.APIName, err))
	}
	return nil
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func (ms *MetadataService) resolveRollupSourceFieldSQLTypeSeen(ctx context.Context, cfg *models.RollupConfig, seen map[string]struct{}) (string, error) {
	if cfg == nil {
		return "", nil
	}
	// COUNT aggregates don't reference a specific source field.
	if strings.EqualFold(cfg.CalcType, "COUNT") {
		return "", nil
	}
	summaryObjectName := strings.TrimSpace(cfg.SummaryObject)
	summaryFieldName := strings.TrimSpace(cfg.SummaryField)
	if summaryObjectName == "" || summaryFieldName == "" {
		return "", fmt.Errorf("summaryObject and summaryField are required for %s calcType", cfg.CalcType)
	}
	schemaLookup := ms.rollupSchemaLookupFunc(ctx)
	objMeta, err := schemaLookup(summaryObjectName)
	if err != nil {
		return "", fmt.Errorf("cannot look up summary object %q: %w", summaryObjectName, err)
	}
	if objMeta == nil {
		return "", fmt.Errorf("summary object %q not found", summaryObjectName)
	}
	for _, f := range objMeta.Fields {
		if strings.EqualFold(f.APIName, summaryFieldName) {
			if domainSchema.IsRollupSummaryType(f.Type) {
				if f.RollupConfig == nil {
					return "", fmt.Errorf("rollupsummary source field %q on object %q is missing rollupConfig", f.APIName, objMeta.APIName)
				}
				seenKey := strings.ToLower(strings.TrimSpace(objMeta.APIName)) + "." + strings.ToLower(strings.TrimSpace(f.APIName))
				if seen != nil {
					if _, ok := seen[seenKey]; ok {
						return "", fmt.Errorf("detected cycle while resolving source field %s.%s", objMeta.APIName, f.APIName)
					}
				}
				nextSeen := make(map[string]struct{}, len(seen)+1)
				for key := range seen {
					nextSeen[key] = struct{}{}
				}
				nextSeen[seenKey] = struct{}{}

				nestedSourceSQLType, err := ms.resolveRollupSourceFieldSQLTypeSeen(ctx, f.RollupConfig, nextSeen)
				if err != nil {
					return "", err
				}
				resolved, err := domainSchema.ResolveRollup(objMeta.APIName, *f.RollupConfig, schemaLookup, nestedSourceSQLType)
				if err != nil {
					return "", err
				}
				return resolved.SQLType, nil
			}
			return domainSchema.ResolveSchemaFieldSQLType(&f, ms.schemaMgr.MapFieldTypeToSQL)
		}
	}
	return "", fmt.Errorf("source field %q not found on object %q", summaryFieldName, objMeta.APIName)
}
