package metadata

import (
	"context"
	"strings"
	"time"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/pkg/formula"
	"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 and carries one compatibility special-case for RecordType,
// whose live rows are stored in `_System_RecordType`.
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
		}

		// Prefer already-hydrated display values when the caller passed them in.
		if related := relatedFromFlatDisplayValues(record, fkField, relationName); related != nil {
			return related, nil
		}

		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
		}

		// Validation rules commonly reference RecordType.DeveloperName, while the
		// current metadata repository surfaces the canonical Name only. Until the
		// RecordType metadata stack grows a dedicated DeveloperName column, expose
		// Name as the compatibility fallback so rules stop being silently skipped.
		if targetObject == "RecordType" {
			if _, ok := related["DeveloperName"]; !ok {
				if name, ok := related[constants.FieldName].(string); ok && strings.TrimSpace(name) != "" {
					related["DeveloperName"] = name
				}
			}
		}

		return related, nil
	}
}

// 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. Standard FK → target table (RecordTypeId → RecordType,
	//      AccountId → Account, OwnerId/CreatedById/LastModifiedById → User,
	//      etc.) for cases where ReferenceTo is not yet populated.
	//   3. Asymmetric Owner / CreatedBy / LastModifiedBy → User. Belt-and-
	//      braces for older orgs where step 2's table also misses.
	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, ""
		}
	}
	if target, ok := formula.LookupStandardRelationTarget(fkField); ok {
		return fkField, target
	}
	switch relationName {
	case "Owner", "CreatedBy", "LastModifiedBy":
		return fkField, constants.TableUser
	}
	return "", ""
}

func relatedFromFlatDisplayValues(record models.SObject, fkField, relationName string) models.SObject {
	name, hasName := record[fkField+"_Name"].(string)
	id, hasID := record[fkField].(string)
	if !hasName || !hasID || strings.TrimSpace(id) == "" {
		return nil
	}
	related := models.SObject{
		constants.FieldID:   id,
		constants.FieldName: name,
	}
	if relationName == "RecordType" {
		related["DeveloperName"] = name
	}
	return related
}

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
}
