package analytics

import (
	"context"
	"errors"
	"sort"
	"strings"

	pkgErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/recorddisplay"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// ReadReferenceDisplayProjection reads the smallest record closure needed to
// render lookup labels: Id plus the authoritative name field (or its compound
// members). Object/FLS gates and query RLS retain their existing owners. Unlike
// generic Records.RetrieveMany this path never fetches an entire related
// record, performs per-field filtering over that record, or projects unrelated
// permission-grant fields.
func (qs *QueryService) ReadReferenceDisplayProjection(
	ctx context.Context,
	schema *models.ObjectMetadata,
	recordIDs []string,
	user *models.UserSession,
) (map[string]models.Record, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if qs == nil || qs.repo == nil || qs.permissions == nil || schema == nil {
		return nil, pkgErrors.NewInternalError("reference-display query dependencies are not configured", nil)
	}
	ids := canonicalReferenceDisplayIDs(recordIDs)
	if len(ids) == 0 {
		return map[string]models.Record{}, nil
	}
	if err := qs.checkObjectReadAccess(ctx, schema.APIName, user); err != nil {
		return nil, err
	}

	display, fields := referenceDisplayFieldClosure(schema)
	if len(fields) == 0 {
		return map[string]models.Record{}, nil
	}
	readable := make(map[string]bool, len(fields)+1)
	permissionFields := fields
	if display != nil {
		permissionFields = append([]string{display.APIName}, fields...)
	}
	for _, fieldName := range permissionFields {
		if readable[strings.ToLower(fieldName)] {
			continue
		}
		field := models.FindField(schema, fieldName)
		if field == nil {
			return nil, pkgErrors.NewInternalError("reference-display field closure disagrees with bound metadata", nil)
		}
		allowed, err := qs.fieldReadableForQueryWithSchema(ctx, schema.APIName, schema, *field, user)
		if err != nil {
			return nil, err
		}
		if !allowed {
			// A denied name parent or component produces no display, exactly as
			// the former FLS-filter + compound synthesis path did.
			return map[string]models.Record{}, nil
		}
		readable[strings.ToLower(field.APIName)] = true
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}

	values := make([]interface{}, len(ids))
	for i := range ids {
		values[i] = ids[i]
	}
	projection := append([]string{constants.FieldID}, fields...)
	rlsResult := qs.readRLSResult(ctx, schema, schema.APIName, user)
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	result, err := qs.repo.Find(ctx, schema, models.QueryRequest{
		ObjectAPIName: schema.APIName,
		Criteria: []models.QueryCriterion{{
			Field: constants.FieldID, Op: "IN", Val: values,
		}},
		Limit: len(ids), SkipTotalSize: true, SelectFields: projection,
	}, rlsResult, projection)
	if err != nil {
		if ctxErr := ctx.Err(); ctxErr != nil {
			return nil, ctxErr
		}
		var internal interface{ InternalCondition() bool }
		if errors.As(err, &internal) && internal.InternalCondition() {
			return nil, err
		}
		return nil, pkgErrors.NewInternalError("reference-display repository read failed", err)
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if result == nil {
		return map[string]models.Record{}, nil
	}

	rows := make(map[string]models.Record, len(result.Records))
	for _, raw := range result.Records {
		record := schemautil.FromStorageRecord(schema, raw, projection)
		if display != nil && display.IsVirtual && display.VirtualKind == constants.VirtualFieldCompound {
			schemautil.SynthesizeVirtualCompoundFields(schema, record, func(field string) bool {
				return readable[strings.ToLower(field)]
			})
		}
		id, _ := record[constants.FieldID].(string)
		id = strings.TrimSpace(id)
		if id == "" {
			return nil, pkgErrors.NewInternalError("reference-display row has no record identifier", nil)
		}
		rows[id] = record
	}
	return rows, nil
}

func canonicalReferenceDisplayIDs(ids []string) []string {
	seen := make(map[string]struct{}, len(ids))
	result := make([]string, 0, len(ids))
	for _, raw := range ids {
		id := strings.TrimSpace(raw)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		result = append(result, id)
	}
	sort.Strings(result)
	return result
}

func referenceDisplayFieldClosure(schema *models.ObjectMetadata) (*models.FieldMetadata, []string) {
	if schema == nil {
		return nil, nil
	}
	// User labels are a platform identity primitive, not whichever of the
	// persisted login/contact fields happens to appear first in schema order.
	// Keep this closure paired with recorddisplay.LookupName, the shared render
	// authority used by ordinary query lookup hydration.
	if strings.EqualFold(schema.APIName, constants.TableUser) {
		return nil, recorddisplay.LookupFieldNames(schema)
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if !field.IsNameField {
			continue
		}
		if field.IsVirtual && field.VirtualKind == constants.VirtualFieldCompound {
			return field, schemautil.CompoundMemberNames(schema, field.APIName)
		}
		return field, []string{field.APIName}
	}
	return nil, nil
}
