package analytics

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/kernel/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/referenceidentity"
)

type relationshipProjectionRequest struct {
	field            models.FieldMetadata
	relationshipName string
	subFields        map[string]struct{}
}

func expandRelationshipSelectFields(schema *models.ObjectMetadata, selectFields []string) []string {
	if len(selectFields) == 0 || schema == nil {
		return selectFields
	}
	expanded := make([]string, 0, len(selectFields))
	for _, field := range selectFields {
		root, _, ok := splitRelationshipSelectField(field)
		if ok {
			if relField := findRelationshipField(schema, root); relField != nil {
				expanded = append(expanded, relField.APIName)
				continue
			}
		}
		expanded = append(expanded, field)
	}
	return expanded
}

func splitRelationshipSelectField(field string) (string, string, bool) {
	field = strings.TrimSpace(field)
	if field == "" || strings.Contains(field, "(") {
		return "", "", false
	}
	if idx := strings.IndexAny(field, " \t"); idx >= 0 {
		field = strings.TrimSpace(field[:idx])
	}
	parts := strings.Split(field, ".")
	if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
		return "", "", false
	}
	return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), true
}

func findRelationshipField(schema *models.ObjectMetadata, relationshipName string) *models.FieldMetadata {
	if schema == nil || strings.TrimSpace(relationshipName) == "" {
		return nil
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if !constants.IsReferenceType(field.Type) || len(field.ReferenceTo) == 0 {
			continue
		}
		if strings.EqualFold(relationshipNameForField(*field), relationshipName) {
			return field
		}
	}
	return nil
}

func relationshipProjectionRequests(schema *models.ObjectMetadata, selectFields []string) []relationshipProjectionRequest {
	requestsByName := map[string]*relationshipProjectionRequest{}
	var ordered []string
	for _, selectField := range selectFields {
		root, subField, ok := splitRelationshipSelectField(selectField)
		if !ok {
			continue
		}
		field := findRelationshipField(schema, root)
		if field == nil {
			continue
		}
		relName := relationshipNameForField(*field)
		key := strings.ToLower(relName)
		req := requestsByName[key]
		if req == nil {
			requestsByName[key] = &relationshipProjectionRequest{
				field:            *field,
				relationshipName: relName,
				subFields:        map[string]struct{}{},
			}
			ordered = append(ordered, key)
			req = requestsByName[key]
		}
		req.subFields[subField] = struct{}{}
	}
	requests := make([]relationshipProjectionRequest, 0, len(ordered))
	for _, key := range ordered {
		requests = append(requests, *requestsByName[key])
	}
	return requests
}

func (qs *QueryService) projectRelationshipSelectFields(ctx context.Context, schema *models.ObjectMetadata, rows []models.SObject, selectFields []string, currentUser *models.UserSession) error {
	requests := relationshipProjectionRequests(schema, selectFields)
	if len(requests) == 0 || len(rows) == 0 {
		return nil
	}
	for _, req := range requests {
		if len(req.field.ReferenceTo) == 0 {
			continue
		}
		targetObject := lookupRefObject(req.field)
		targetSchema := qs.metadata.GetSchema(ctx, targetObject)
		if targetSchema == nil {
			continue
		}
		idSet := map[string]struct{}{}
		for _, row := range rows {
			if id := querySObjectString(row[req.field.APIName]); id != "" {
				idSet[id] = struct{}{}
			}
		}
		if len(idSet) == 0 {
			continue
		}
		ids := make([]string, 0, len(idSet))
		for id := range idSet {
			ids = append(ids, id)
		}
		fields, err := qs.requestedRelationshipTargetFields(ctx, targetObject, targetSchema, req.subFields, currentUser)
		if err != nil {
			return err
		}
		bypassUserAccess := scopedReadBypassesUserAccess(ctx, targetObject)
		bypassFieldVisibility := scopedReadBypassesFieldVisibility(ctx, targetObject)
		requiresRecordAccessCheck := !bypassUserAccess && currentUser != nil && qs.permissions != nil
		if requiresRecordAccessCheck {
			fields = appendRecordAccessInputFields(fields, targetSchema)
		}
		physicalIDs := ids
		var targetRows []models.SObject
		var physicalRows []models.SObject
		immutableIDs := make(map[string]struct{})
		if authority, bound := referenceidentity.FromContext(ctx); bound {
			resolution, resolveErr := authority.ResolveReferenceIdentities(ctx, targetObject, ids, fields)
			if resolveErr != nil {
				return fmt.Errorf("resolve %s relationship identities: %w", targetObject, resolveErr)
			}
			physicalIDs = make([]string, 0, len(ids))
			targetRows = make([]models.SObject, 0, len(ids))
			for _, id := range ids {
				identity, ok := resolution.Identities[id]
				if !ok || identity.Disposition == referenceidentity.Unowned {
					physicalIDs = append(physicalIDs, id)
					continue
				}
				switch identity.Disposition {
				case referenceidentity.Immutable:
					resolvedID, _ := identity.Record[constants.FieldID].(string)
					if identity.Record == nil || strings.TrimSpace(resolvedID) != id {
						return fmt.Errorf("reference identity authority returned an invalid immutable record for %s/%s", targetObject, id)
					}
					immutableIDs[id] = struct{}{}
					targetRows = append(targetRows, identity.Record)
				case referenceidentity.OwnedMissing:
					return fmt.Errorf("authoritative %s relationship identity %q does not exist", targetObject, id)
				default:
					return fmt.Errorf("reference identity authority returned an invalid disposition for %s/%s", targetObject, id)
				}
			}
		}
		if len(physicalIDs) != 0 {
			physicalRows, err = qs.repo.GetRecordsByIDs(ctx, targetSchema, physicalIDs, fields)
			if err != nil {
				return err
			}
			targetRows = append(targetRows, physicalRows...)
		}
		byID := make(map[string]models.SObject, len(targetRows))
		if requiresRecordAccessCheck && len(physicalRows) != 0 {
			qs.permissions.PrimeReadAccessInputCache(ctx, targetSchema.APIName, physicalRows, currentUser)
		}
		for _, targetRow := range targetRows {
			id := querySObjectString(targetRow[constants.FieldID])
			_, immutable := immutableIDs[id]
			// Immutable identities come from a closed, request-bound Catalog
			// projection that exposes only safe display fields. They are not
			// physical records and therefore have no row/FLS grants to re-check.
			// Legacy physical rows keep the ordinary record authorization path.
			if requiresRecordAccessCheck && !immutable && !qs.permissions.CheckRecordAccess(ctx, targetSchema, targetRow, constants.PermRead, currentUser) {
				continue
			}
			if id != "" {
				byID[id] = targetRow
			}
		}
		for _, row := range rows {
			id := querySObjectString(row[req.field.APIName])
			if id == "" {
				continue
			}
			targetRow, ok := byID[id]
			if !ok {
				continue
			}
			_, immutable := immutableIDs[id]
			nested := map[string]interface{}{
				constants.FieldID: id,
			}
			models.StampObjectType(models.SObject(nested), targetObject)
			for subField := range req.subFields {
				if !immutable && !bypassFieldVisibility {
					if qs.permissions == nil {
						continue
					}
					visible, err := qs.permissions.CheckFieldVisibilityWithUser(ctx, targetObject, subField, currentUser)
					if err != nil {
						return err
					}
					if !visible {
						continue
					}
				}
				if strings.EqualFold(subField, constants.FieldName) {
					if value := extractLookupDisplayName(targetSchema, targetRow); value != "" {
						nested[constants.FieldName] = value
					} else if value, ok := row[req.field.APIName+"_Name"]; ok {
						nested[constants.FieldName] = value
					}
					continue
				}
				if value, ok := targetRow[subField]; ok {
					nested[subField] = value
				}
			}
			row[req.relationshipName] = nested
		}
	}
	return nil
}

func (qs *QueryService) requestedRelationshipTargetFields(ctx context.Context, objectName string, schema *models.ObjectMetadata, subFields map[string]struct{}, currentUser *models.UserSession) ([]string, error) {
	fields := []string{constants.FieldID}
	add := func(field string) {
		field = strings.TrimSpace(field)
		if field == "" || utils.ContainsString(fields, field) {
			return
		}
		fields = append(fields, field)
	}
	for subField := range subFields {
		if strings.EqualFold(subField, constants.FieldName) {
			displayFields, err := qs.lookupDisplayFieldsForRead(ctx, objectName, schema, currentUser)
			if err != nil {
				return nil, err
			}
			for _, displayField := range displayFields {
				add(displayField)
			}
			continue
		}
		field := models.FindField(schema, subField)
		if field != nil && qs.fieldReadBlockedWithSchema(ctx, objectName, schema, *field) {
			continue
		}
		add(subField)
	}
	return fields, nil
}
