package uiapi

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"golang.org/x/sync/errgroup"
)

// GetRecordUI aggregates records, object info, and layouts for the given IDs and object type.
// typeHint is required in NexusCRM unless we have a global index.
func (s *Service) GetRecordUI(ctx context.Context, recordIDs []string, typeHint string, user *models.UserSession, originalTypeHints ...string) (*RecordUI, error) {
	response := &RecordUI{
		Records:     make(map[string]Record),
		Layouts:     make(map[string]map[string]Layout),
		ObjectInfos: make(map[string]ObjectInfo),
	}
	var mu sync.Mutex // Protects response maps

	if len(recordIDs) == 0 {
		return response, nil
	}

	// 1. Resolve Object Type
	if typeHint == "" {
		return nil, apperrors.NewValidationError("typeHint", "object type hint is required for NexusCRM RecordUI")
	}

	// 2. Check access once (all records share the same type)
	if err := s.permSvc.CheckPermissionOrErrorWithUser(ctx, typeHint, "Read", user); err != nil {
		return nil, err
	}

	// Phase 1: Fetch Records + Object Info in parallel
	// Use a separate variable for the errgroup context — errgroup cancels its
	// derived context after Wait() returns, which would break Phase 2's DB queries.
	g1, g1Ctx := errgroup.WithContext(ctx)

	// A. Fetch Records (Parallel)
	for _, id := range recordIDs {
		recordID := id
		g1.Go(func() error {
			uiRecord, err := s.processRecordForUI(g1Ctx, typeHint, recordID, user)
			if err != nil {
				return err
			}

			mu.Lock()
			response.Records[recordID] = *uiRecord
			mu.Unlock()
			return nil
		})
	}

	// B. Fetch Object Info
	g1.Go(func() error {
		objInfo, err := s.GetObjectInfo(g1Ctx, typeHint, user)
		if err != nil {
			return err
		}
		if objInfo == nil {
			return nil
		}

		mu.Lock()
		response.ObjectInfos[typeHint] = *objInfo
		mu.Unlock()
		return nil
	})

	if err := g1.Wait(); err != nil {
		return nil, err
	}

	// Resolve canonical APIName from schema for response keys.
	// typeHint may be lowercase (handler normalizes for DB), but
	// the frontend expects PascalCase keys matching the URL.
	responseKey := typeHint
	schema := s.metadataSvc.GetSchema(ctx, typeHint)
	if schema != nil {
		responseKey = schema.APIName
	} else if len(originalTypeHints) > 0 && originalTypeHints[0] != "" {
		// Schema not found in cache — use the original casing from the request
		// to prevent case mismatch between response keys and frontend lookups.
		responseKey = originalTypeHints[0]
	}

	// Re-key objectInfos with canonical name if it differs
	if responseKey != typeHint {
		if oi, ok := response.ObjectInfos[typeHint]; ok {
			delete(response.ObjectInfos, typeHint)
			response.ObjectInfos[responseKey] = oi
		}
	}

	// Phase 2: Extract RecordTypeId from first record, then resolve layout
	// Phase 2 guarantees single-record Detail/Create/Edit path.
	// Mixed-RecordType multi-record resolution is not a Phase 2 goal.
	var recordTypeID string
	if len(recordIDs) > 0 {
		if rec, ok := response.Records[recordIDs[0]]; ok {
			if rtField, ok := rec.Fields["RecordTypeId"]; ok {
				if rtID, ok := rtField.Value.(string); ok && rtID != "" {
					recordTypeID = rtID
				}
			}
		}
	}

	// C. Fetch Layout with RecordType context for assignment-based resolution
	uiLayout, err := s.GetLayoutWithRecordType(ctx, typeHint, user, recordTypeID)
	if err != nil {
		return nil, err
	}
	if uiLayout != nil {
		response.Layouts[responseKey] = map[string]Layout{
			uiLayout.Id: *uiLayout,
		}
	}

	return response, nil
}

type lookupTarget struct {
	fieldName string
	relName   string
	targetObj string
	targetID  string
}

func inferLookupRelationshipName(fieldDef *models.FieldMetadata) string {
	if fieldDef == nil {
		return ""
	}
	if fieldDef.RelationshipName != nil && *fieldDef.RelationshipName != "" {
		return *fieldDef.RelationshipName
	}

	apiName := fieldDef.APIName
	switch {
	case strings.HasSuffix(apiName, "__c") && len(apiName) > len("__c"):
		return strings.TrimSuffix(apiName, "__c") + "__r"
	case strings.HasSuffix(apiName, "Id") && len(apiName) > len("Id"):
		return strings.TrimSuffix(apiName, "Id")
	default:
		return ""
	}
}

func (s *Service) processRecordForUI(ctx context.Context, typeHint, recordID string, user *models.UserSession) (*Record, error) {
	// Use RetrieveWithAccess to compute record-level access on the RAW (unfiltered)
	// record before FLS. This ensures criteria-based sharing rules evaluate against
	// complete field values, not FLS-filtered ones.
	rec, access, err := s.recordSvc.RetrieveWithAccess(ctx, typeHint, recordID, user)
	if err != nil {
		return nil, err
	}
	if rec == nil {
		return nil, apperrors.NewNotFoundError("record", recordID)
	}

	schema := s.metadataSvc.GetSchema(ctx, typeHint)
	if schema == nil {
		return nil, apperrors.NewNotFoundError("schema", typeHint)
	}

	// Build field index for O(1) lookup
	fieldIndex := make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		fieldIndex[schema.Fields[i].APIName] = &schema.Fields[i]
	}

	// Initialize UI Record
	uiRecord := Record{
		ApiName: schema.APIName,
		Id:      recordID,
		Fields:  make(map[string]FieldValue),
	}
	if lmd, ok := rec[constants.FieldLastModifiedDate].(string); ok {
		uiRecord.LastModifiedDate = lmd
	}
	if sms, ok := rec[constants.FieldSystemModstamp].(string); ok {
		uiRecord.SystemModstamp = sms
	}

	// Pass 1: Format values and collect lookup targets
	lookups := s.mapRecordFieldsToUI(rec, fieldIndex, &uiRecord)

	// Pass 2: Batch-resolve all lookup targets
	if err := s.resolveRecordLookups(ctx, lookups, &uiRecord, user); err != nil {
		return nil, err
	}
	uiRecord.ConversionState = s.buildConversionState(ctx, schema, rec, &uiRecord)

	// Record-level access was computed on the raw record (pre-FLS) by RetrieveWithAccess
	if access != nil {
		hasEditAccess := access.HasEditAccess
		if hasEditAccess {
			hasEditAccess = !s.recordEditBlockedByValidationRulesForID(ctx, schema, recordID, rec)
		}
		uiRecord.RecordAccess = &RecordAccess{
			HasReadAccess:   access.HasReadAccess,
			HasEditAccess:   hasEditAccess,
			HasDeleteAccess: access.HasDeleteAccess,
		}
	}

	return &uiRecord, nil
}

func (s *Service) recordEditBlockedByValidationRulesForID(ctx context.Context, schema *models.ObjectMetadata, recordID string, fallback models.SObject) bool {
	if s == nil || s.recordSvc == nil || schema == nil || recordID == "" {
		return s.recordEditBlockedByValidationRules(ctx, schema, fallback)
	}

	// Update validation evaluates rules against the raw prior image. UI API uses
	// the same image for editability so hidden lock fields do not split access.
	if rawRecord, err := s.recordSvc.GetRaw(ctx, schema.APIName, recordID); err == nil && rawRecord != nil {
		return s.recordEditBlockedByValidationRules(ctx, schema, rawRecord)
	}

	return s.recordEditBlockedByValidationRules(ctx, schema, fallback)
}

func (s *Service) recordEditBlockedByValidationRules(ctx context.Context, schema *models.ObjectMetadata, record models.SObject) bool {
	if s == nil || s.metadataSvc == nil || s.validator == nil || schema == nil || record == nil {
		return false
	}

	rules := s.metadataSvc.GetValidationRules(ctx, schema.APIName)
	if len(rules) == 0 {
		return false
	}

	return s.validator.BlocksRecordUpdateWithPriorOnlyRules(record, schema, rules, nil)
}

func (s *Service) buildConversionState(ctx context.Context, schema *models.ObjectMetadata, record models.SObject, uiRecord *Record) *ConversionState {
	if s == nil || s.metadataSvc == nil || schema == nil || record == nil {
		return nil
	}

	descriptor := s.metadataSvc.ConvertedStateMetadata(ctx, schema.APIName)
	if descriptor == nil {
		return nil
	}

	state := &ConversionState{
		IsConverted: truthyRecordValue(record[descriptor.StateField.APIName]),
	}
	if !state.IsConverted {
		return state
	}

	if descriptor.DateField != nil {
		if convertedDate := firstStringRecordValue(record[descriptor.DateField.APIName], uiRecord, descriptor.DateField.APIName); convertedDate != "" {
			state.ConvertedDate = convertedDate
		}
	}
	if descriptor.ActorField != nil {
		if convertedByID := strings.TrimSpace(asString(record[descriptor.ActorField.APIName])); convertedByID != "" {
			state.ConvertedByID = convertedByID
		}
		if convertedByName := lookupDisplayValue(uiRecord, descriptor.ActorField.APIName); convertedByName != "" {
			state.ConvertedByName = convertedByName
		}
	}

	for _, field := range descriptor.LookupFields {
		value, ok := record[field.APIName].(string)
		if !ok {
			continue
		}
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		targetObject := firstNonEmpty(field.ReferenceTo)
		if targetObject == "" {
			continue
		}
		state.Links = append(state.Links, ConversionLink{
			FieldName:    field.APIName,
			Label:        conversionLinkLabel(field, targetObject),
			TargetObject: targetObject,
			RecordID:     value,
		})
	}

	return state
}

func firstStringRecordValue(raw interface{}, uiRecord *Record, fieldName string) string {
	if uiRecord != nil {
		if field, ok := uiRecord.Fields[fieldName]; ok && field.DisplayValue != nil {
			if value := strings.TrimSpace(*field.DisplayValue); value != "" {
				return value
			}
		}
	}
	return strings.TrimSpace(asString(raw))
}

func lookupDisplayValue(uiRecord *Record, fieldName string) string {
	if uiRecord == nil {
		return ""
	}
	if field, ok := uiRecord.Fields[fieldName]; ok && field.DisplayValue != nil {
		if value := strings.TrimSpace(*field.DisplayValue); value != "" {
			return value
		}
	}
	if strings.HasSuffix(fieldName, "Id") {
		relationshipName := strings.TrimSuffix(fieldName, "Id")
		if field, ok := uiRecord.Fields[relationshipName]; ok && field.DisplayValue != nil {
			return strings.TrimSpace(*field.DisplayValue)
		}
	}
	return ""
}

func asString(value interface{}) string {
	switch v := value.(type) {
	case string:
		return v
	case fmt.Stringer:
		return v.String()
	default:
		return ""
	}
}

func truthyRecordValue(value interface{}) bool {
	switch v := value.(type) {
	case bool:
		return v
	case int:
		return v != 0
	case int64:
		return v != 0
	case float64:
		return v != 0
	case string:
		normalized := strings.TrimSpace(strings.ToLower(v))
		return normalized == "true" || normalized == "1"
	default:
		return false
	}
}

func firstNonEmpty(values []string) string {
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" {
			return value
		}
	}
	return ""
}

func conversionLinkLabel(field models.FieldMetadata, fallback string) string {
	label := strings.TrimSpace(field.Label)
	if label == "" {
		label = fallback
	}
	if len(label) >= len("Converted ") && strings.EqualFold(label[:len("Converted ")], "Converted ") {
		label = strings.TrimSpace(label[len("Converted "):])
	}
	if len(label) >= len(" ID") && strings.EqualFold(label[len(label)-len(" ID"):], " ID") {
		label = strings.TrimSpace(label[:len(label)-len(" ID")])
	}
	if label == "" {
		return fallback
	}
	return label
}

func (s *Service) mapRecordFieldsToUI(rec models.SObject, fieldIndex map[string]*models.FieldMetadata, uiRecord *Record) []lookupTarget {
	var lookups []lookupTarget

	for k, v := range rec {
		fieldDef := fieldIndex[k]
		var displayValue *string

		if fieldDef != nil {
			// Coerce string→float64 for numeric fields whose values arrived as strings
			// from the DB scanner (DECIMAL columns return []byte→string via scanner.go).
			// This mirrors the conversion in schema_utils.go:FromStorageRecord.
			switch fieldDef.Type {
			case "double", "currency", "percent":
				if s, ok := v.(string); ok && s != "" {
					if f, err := strconv.ParseFloat(s, 64); err == nil {
						v = f
					}
				}
			}

			switch fieldDef.Type {
			case "date":
				if t, ok := v.(string); ok && len(t) >= 10 {
					dv := t[:10]
					displayValue = &dv
				}
			case "datetime":
				if t, ok := v.(string); ok && len(t) >= 19 {
					dv := t[:19]
					displayValue = &dv
				}
			case "currency":
				if f, ok := utils.ToFloat64(v); ok {
					dv := fmt.Sprintf("$%.2f", f)
					displayValue = &dv
				}
			case "percent":
				if f, ok := utils.ToFloat64(v); ok {
					dv := fmt.Sprintf("%.0f%%", f)
					displayValue = &dv
				}
			}
		}

		uiRecord.Fields[k] = FieldValue{
			DisplayValue: displayValue,
			Value:        v,
		}

		// Collect lookup for batch resolution
		if fieldDef != nil && constants.IsReferenceType(fieldDef.Type) {
			idStr, ok := v.(string)
			if !ok || idStr == "" || len(fieldDef.ReferenceTo) == 0 {
				continue
			}
			relName := inferLookupRelationshipName(fieldDef)
			if relName == "" {
				continue
			}
			targetObj := fieldDef.ReferenceTo[0]
			if fieldDef.IsPolymorphic && fieldDef.APIName == "OwnerId" {
				targetObj = constants.TableUser
			}
			lookups = append(lookups, lookupTarget{
				fieldName: k,
				relName:   relName,
				targetObj: targetObj,
				targetID:  idStr,
			})
		}
	}
	return lookups
}

func (s *Service) resolveRecordLookups(ctx context.Context, lookups []lookupTarget, uiRecord *Record, user *models.UserSession) error {
	type resolveKey struct{ obj, id string }
	resolvedCache := make(map[resolveKey]map[string]interface{})

	// Bypass FLS for lookup display-name resolution — lookup names (RecordType.Name,
	// User.Name, etc.) must always be visible regardless of FLS on the target object,
	// matching Salesforce behavior. CRUD and RLS checks remain enforced.
	lookupCtx := constants.WithSecurityContext(ctx, constants.ElevatedContext, "uiapi: resolve lookup display names")

	for _, lt := range lookups {
		key := resolveKey{lt.targetObj, lt.targetID}
		if _, exists := resolvedCache[key]; exists {
			continue
		}
		targetRec, err := s.recordSvc.Retrieve(lookupCtx, lt.targetObj, lt.targetID, user)
		if err != nil || targetRec == nil {
			resolvedCache[key] = nil
			continue
		}
		resolvedCache[key] = targetRec
	}

	// Emit relationship fields from resolved cache
	for _, lt := range lookups {
		key := resolveKey{lt.targetObj, lt.targetID}
		targetRec := resolvedCache[key]
		if targetRec == nil {
			continue
		}

		resolvedName := resolveLookupDisplayName(targetRec)

		nestedRecord := Record{
			ApiName: lt.targetObj,
			Id:      lt.targetID,
			Fields: map[string]FieldValue{
				"Name": {Value: resolvedName, DisplayValue: nil},
			},
		}

		var relDisplayValue *string
		if resolvedName != "" {
			relDisplayValue = &resolvedName
		}

		uiRecord.Fields[lt.relName] = FieldValue{
			DisplayValue: relDisplayValue,
			Value:        nestedRecord,
		}
	}
	return nil
}

func resolveLookupDisplayName(targetRec map[string]interface{}) string {
	if name := strings.TrimSpace(stringValue(targetRec["Name"])); name != "" {
		return name
	}

	first := strings.TrimSpace(stringValue(targetRec["FirstName"]))
	last := strings.TrimSpace(stringValue(targetRec["LastName"]))
	if first != "" || last != "" {
		return strings.TrimSpace(first + " " + last)
	}

	if devName := strings.TrimSpace(stringValue(targetRec["DeveloperName"])); devName != "" {
		return devName
	}
	if username := strings.TrimSpace(stringValue(targetRec["Username"])); username != "" {
		return username
	}

	return ""
}

func stringValue(v interface{}) string {
	if s, ok := v.(string); ok {
		return s
	}
	return ""
}
