package metadata

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// NewValidationFormulaRelationResolver builds the lazy relationship resolver
// used by validation formulas. The resolver is metadata-driven for regular
// reference fields. In independently serving Runtime requests, RecordType is
// resolved from the exact immutable mutation view already bound to the
// request. Legacy compositions retain the physical `_System_RecordType`
// fallback.
func NewValidationFormulaRelationResolver(metadataSvc *MetadataService, recordRepo ports.RecordRepository) FormulaRelationResolver {
	if metadataSvc == nil || recordRepo == nil {
		return nil
	}

	return func(ctx context.Context, objectAPIName string, record models.SObject, relationName string) (models.SObject, error) {
		if record == nil || relationName == "" {
			return nil, nil
		}

		schemaCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
		defer cancel()

		schema := metadataSvc.GetSchema(schemaCtx, objectAPIName)
		fkField, targetObject := resolveValidationRelation(schema, record, relationName)
		if fkField == "" || targetObject == "" {
			return nil, nil
		}

		id, ok := record[fkField].(string)
		if !ok || strings.TrimSpace(id) == "" {
			return nil, nil
		}
		if targetObject == constants.ObjectRecordType {
			related, owned, err := resolveValidationRecordTypeFromRuntimeView(ctx, metadataSvc, objectAPIName, id)
			if owned || err != nil {
				return related, err
			}
		}

		relatedCtx, cancelRelated := context.WithTimeout(ctx, 5*time.Second)
		defer cancelRelated()

		lookupTable := targetObject
		if targetObject == "RecordType" {
			lookupTable = constants.TableRecordType
		}

		related, err := recordRepo.FindOne(relatedCtx, lookupTable, id)
		if err != nil || related == nil {
			return nil, err
		}
		prepareValidationRelatedRecordForFormulaEval(related, metadataSvc.GetSchema(schemaCtx, targetObject))

		return related, nil
	}
}

// resolveValidationRecordTypeFromRuntimeView returns owned=true whenever the
// metadata service is the independent Runtime service. That distinction is
// load-bearing: an admitted Runtime mutation must never fall back to a live
// side table when its request-bound Catalog view is absent, incompatible, or
// missing the referenced immutable identity. Legacy/in-process compositions
// return owned=false and preserve their physical lookup behavior.
func resolveValidationRecordTypeFromRuntimeView(
	ctx context.Context,
	metadataSvc *MetadataService,
	objectAPIName string,
	recordTypeID string,
) (models.SObject, bool, error) {
	if metadataSvc == nil || !metadataSvc.UsesRequestBoundRuntimeView() {
		return nil, false, nil
	}
	view, ok := metadataSvc.BoundRuntimeMetadataView(ctx)
	if !ok || view == nil {
		return nil, true, fmt.Errorf("record type %q has no request-bound runtime metadata view", recordTypeID)
	}
	if !view.SupportsMutationClosure() {
		return nil, true, fmt.Errorf("record type %q requires a mutation metadata view", recordTypeID)
	}
	object, ok := view.Object(objectAPIName)
	if !ok || object == nil {
		return nil, true, fmt.Errorf("record type %q references object %q outside the mutation metadata view", recordTypeID, objectAPIName)
	}
	for _, recordType := range view.RecordTypesForObject(object.APIName) {
		if recordType == nil || recordType.ID != recordTypeID {
			continue
		}
		return models.SObject{
			constants.FieldSysRecordType_ID:            recordType.ID,
			constants.FieldSysRecordType_ObjectID:      object.ID,
			constants.FieldSysRecordType_Name:          recordType.Name,
			constants.FieldSysRecordType_DeveloperName: recordType.DeveloperName,
			constants.FieldSysRecordType_IsActive:      recordType.IsActive,
			"SobjectType":                              recordType.ObjectAPIName,
		}, true, nil
	}
	return nil, true, fmt.Errorf(
		"record type %q is absent from the request-bound mutation metadata view for %s",
		recordTypeID,
		object.APIName,
	)
}

func prepareValidationRelatedRecordForFormulaEval(record models.SObject, schema *models.ObjectMetadata) {
	if record == nil || schema == nil {
		return
	}
	models.StampObjectType(record, schema.APIName)
	formula.PrepareRecordForEvaluation(map[string]interface{}(record), schema)
}

// resolveValidationRelation routes the relationship-FK resolution through
// the SHARED pkg/formula SSoT (PR #2057 round-6 F1, round-8 polymorphic
// FK fix). Pre-round-6, this function maintained its own four-branch FK
// resolver, parallel to records and automation. Pre-round-8, it then picked
// `field.ReferenceTo[0]` for target resolution, while records/automation
// FetchRelatedRecord consulted the polymorphic `<FK>_type` discriminator
// — same fact, two answers on Task.WhoId / Task.WhatId.
//
// Now both stages are routed through pkg/formula:
//  1. ResolveRelationFK: tier-ordered FK column resolution.
//  2. ResolveRelationTarget: polymorphic-discriminator-aware target,
//     shared with FetchRelatedRecord, so a Task.WhoId pointing at a Lead
//     is queried against Lead, not Contact (refTo[0]).
//
// Asymmetric Owner / CreatedBy / LastModifiedBy → User fallback handles
// the case where schema metadata has no ReferenceTo populated yet (e.g.
// metadata cache miss); the fallback only fires when ResolveRelationTarget
// returned indeterminate AND the standard FK→target table also missed.
func resolveValidationRelation(schema *models.ObjectMetadata, record models.SObject, relationName string) (string, string) {
	fkField := formula.ResolveRelationFK(schema, relationName)
	if fkField == "" {
		return "", ""
	}

	// Target derivation, in priority order:
	//   1. Polymorphic-aware schema lookup via formula.ResolveRelationTarget.
	//      Single ref → refTo[0]. Polymorphic → discriminator-validated.
	//      Indeterminate (missing/empty/unknown discriminator) → fail-closed
	//      so callers see nil rather than the wrong target's row.
	//   2. Asymmetric Owner / CreatedBy / LastModifiedBy → User.
	if field := validationFieldByKey(schema, fkField); field != nil {
		if t, ok := formula.ResolveRelationTarget(map[string]interface{}(record), fkField, field.ReferenceTo); ok {
			return fkField, t
		}
		// Schema has ReferenceTo but ResolveRelationTarget said
		// indeterminate (polymorphic FK, discriminator missing/unknown).
		// Fail closed on TARGET only: keep the resolved fkField so callers
		// can distinguish "unmappable relation segment" (fk="") from
		// "known FK with indeterminate target" (fk!="", target=""). Both
		// short-circuit the closure's `if fk == "" || target == ""`
		// gate to a nil parent — equivalent to FetchRelatedRecord's
		// indeterminate return on records/automation paths — but the
		// distinction helps log diagnosis.
		if len(field.ReferenceTo) > 0 {
			return fkField, ""
		}
	}
	switch relationName {
	case "Owner", "CreatedBy", "LastModifiedBy":
		return fkField, constants.TableUser
	}
	return "", ""
}

func validationFieldByKey(schema *models.ObjectMetadata, key string) *models.FieldMetadata {
	if schema == nil {
		return nil
	}
	if schema.FieldsMap != nil {
		if field := schema.FieldsMap[strings.ToLower(key)]; field != nil {
			return field
		}
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if strings.EqualFold(field.APIName, key) {
			return field
		}
	}
	return nil
}
