package analytics

import (
	"context"
	"log"
	"strings"

	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
	"github.com/nexuscrm/backend/pkg/auth"
	pkgErrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func buildQueryFields(schema *models.ObjectMetadata, req models.QueryRequest, visibleFields []string) []string {
	if len(req.SelectFields) == 0 {
		return visibleFields
	}

	visible := make(map[string]bool, len(visibleFields))
	for _, field := range visibleFields {
		visible[field] = true
	}

	queryFields := make([]string, 0, len(req.SelectFields)+4)
	added := make(map[string]bool)
	addField := func(field string) {
		if field == "" || added[field] || !visible[field] {
			return
		}
		queryFields = append(queryFields, field)
		added[field] = true
	}

	addField(constants.FieldID)

	for _, field := range req.SelectFields {
		if strings.HasSuffix(field, "_Name") {
			base := strings.TrimSuffix(field, "_Name")
			addField(base)
			continue
		}
		addField(field)
	}

	return queryFields
}

// Helper methods

func (qs *QueryService) hydrateVirtualFields(
	ctx context.Context,
	rows []models.SObject,
	schema *models.ObjectMetadata,
	visibleFields []string,
	currentUser *models.UserSession,
) []models.SObject {
	lookupFields := make([]models.FieldMetadata, 0)

	for _, field := range schema.Fields {
		if !utils.ContainsString(visibleFields, field.APIName) {
			continue
		}

		isLookup := strings.EqualFold(string(field.Type), string(constants.FieldTypeReference))

		if isLookup && field.ReferenceTo != nil && len(field.ReferenceTo) > 0 {
			lookupFields = append(lookupFields, field)
		}
	}

	// Hydrate each row (static type conversion only; formula computation is handled by TiDB Views)
	for i := range rows {
		record := rows[i]
		record = persistence.FromStorageRecord(schema, record, visibleFields)
		rows[i] = record
	}

	// Hydrate lookup field display names (batch query for efficiency)
	if len(lookupFields) > 0 {
		rows = qs.hydrateLookupNames(ctx, rows, lookupFields, currentUser)
	}

	return rows
}

// getLookupIdField determines the ID field to use for lookups from metadata
// Returns the SSoT ID field (constants.FieldID).
// Legacy support for ExternalId overriding has been removed to enforce strict FK integrity.
func getLookupIdField(schema *models.ObjectMetadata) string {
	return constants.FieldID
}

func getLookupDisplayFields(schema *models.ObjectMetadata) []string {
	fields := []string{constants.FieldID}

	if strings.EqualFold(schema.APIName, constants.TableUser) {
		for _, fieldName := range []string{
			constants.FieldName,
			constants.FieldFirstName,
			constants.FieldLastName,
			constants.FieldUsername,
		} {
			fields = appendLookupDisplayField(fields, fieldName)
		}
		return fields
	}

	nameField := constants.FieldName
	for _, f := range schema.Fields {
		if f.IsNameField {
			nameField = f.APIName
			break
		}
	}
	fields = appendLookupDisplayField(fields, nameField)

	return fields
}

func extractLookupDisplayName(schema *models.ObjectMetadata, rec models.SObject) string {
	if schema == nil {
		return ""
	}

	return auth.ResolveLookupDisplayName(schema.APIName, rec)
}

func appendLookupDisplayField(fields []string, fieldName string) []string {
	if fieldName == "" || utils.ContainsString(fields, fieldName) {
		return fields
	}

	return append(fields, fieldName)
}

// hydrateLookupNames resolves lookup field UUIDs to display names
// It batches queries per referenced object for efficiency.
// Records the user cannot access are silently omitted (names not populated).
func (qs *QueryService) hydrateLookupNames(ctx context.Context, rows []models.SObject, lookupFields []models.FieldMetadata, currentUser *models.UserSession) []models.SObject {
	if len(rows) == 0 || len(lookupFields) == 0 {
		return rows
	}

	// For each lookup field, collect all unique IDs and their reference object
	// Map: referenceObject -> Set of IDs
	refObjectIDs := make(map[string]map[string]bool)

	for _, field := range lookupFields {
		refObject := field.ReferenceTo[0] // Primary reference object (for polymorphic, we use first)
		if _, exists := refObjectIDs[refObject]; !exists {
			refObjectIDs[refObject] = make(map[string]bool)
		}

		for _, row := range rows {
			if val, ok := row[field.APIName]; ok && val != nil {
				if id, ok := val.(string); ok && id != "" {
					refObjectIDs[refObject][id] = true
				}
			}
		}
	}

	// For each reference object, fetch the names of all IDs
	// Map: ID -> Name
	idToName := make(map[string]string)

	for refObject, idSet := range refObjectIDs {
		if len(idSet) == 0 {
			continue
		}

		// Get the schema for the referenced object to find the name and ID fields
		refSchema := qs.metadata.GetSchema(ctx, refObject)
		if refSchema == nil {
			continue
		}

		// Determine ID field from metadata (not hardcoded)
		idField := getLookupIdField(refSchema)

		// Collect IDs into slice
		ids := make([]string, 0, len(idSet))
		for id := range idSet {
			ids = append(ids, id)
		}

		results, err := qs.repo.GetRecordsByIDs(ctx, refSchema, ids, getLookupDisplayFields(refSchema))
		if err != nil {
			log.Printf("⚠️ Failed to hydrate lookup names for %s: %v", refObject, err)
			continue
		}

		// Build ID -> display name map using the same idField.
		// Apply record-level access check (RLS) to omit names for records the user cannot read.
		// SystemContext and metadata-owned scoped reads bypass this check.
		bypassUserVisibility := scopedReadBypassesUserAccess(ctx, refSchema.APIName)
		for _, rec := range results {
			if !bypassUserVisibility && currentUser != nil && !qs.permissions.CheckRecordAccess(ctx, refSchema, rec, constants.PermRead, currentUser) {
				continue // silently omit — lookup name will not be populated
			}
			if id, ok := rec[idField].(string); ok {
				if display := extractLookupDisplayName(refSchema, rec); display != "" {
					idToName[id] = display
				}
			}
		}
	}

	// Populate _Name fields on each row
	for i, row := range rows {
		for _, field := range lookupFields {
			if val, ok := row[field.APIName]; ok && val != nil {
				if id, ok := val.(string); ok {
					if name, found := idToName[id]; found {
						row[field.APIName+"_Name"] = name
					}
				}
			}
		}
		rows[i] = row
	}

	return rows
}

// Calculate returns the record as-is; formula computation is handled by TiDB Views.
func (qs *QueryService) Calculate(
	ctx context.Context,
	objectName string,
	record models.SObject,
	user *models.UserSession,
) (models.SObject, error) {
	schema := qs.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return nil, pkgErrors.NewNotFoundError("Object", objectName)
	}

	record = persistence.NormalizeSObject(schema, record)

	return record, nil
}
