package mru

import (
	"context"
	"strconv"
	"sync"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

const trackRecordAccessAsyncTimeout = 2 * time.Second

const (
	objectInteractionFlushDelay   = 100 * time.Millisecond
	objectInteractionFlushTimeout = 2 * time.Second
)

// Service handles MRU (Most Recently Used) tracking for search scope and relevant items
type Service struct {
	repo               ports.MRURepository
	objectInteractions *objectInteractionCoalescer
}

// NewService creates a new MRU service
func NewService(repo ports.MRURepository) *Service {
	return &Service{
		repo:               repo,
		objectInteractions: newObjectInteractionCoalescer(repo),
	}
}

// ScopeOrderItem represents an item in the search scope order.
type ScopeOrderItem struct {
	Type      string
	Name      string
	Label     string
	KeyPrefix string
}

// RelevantItem represents a single recently accessed record.
type RelevantItem struct {
	ObjectAPIName string
	RecordID      string
	Name          string
}

// DefaultScopeObjects returns the default search scope objects.
// ARCHITECTURE NOTE: This is a fallback for when metadata is unavailable.
// Prefer calling MetadataService.GetSearchableObjects() for dynamic scope.
// Only core system objects are listed here as ultimate fallback.
func DefaultScopeObjects() []string {
	return []string{
		constants.TableUser, // Core system table - always present
	}
}

// TrackObjectInteraction tracks when a user interacts with an object type
// This is called when searching, viewing lists, etc.
func (s *Service) TrackObjectInteraction(ctx context.Context, userID, objectAPIName string) {
	if userID == "" || objectAPIName == "" {
		return
	}

	if err := s.repo.TrackObjectInteraction(ctx, userID, objectAPIName); err != nil {
		log.Warn().Err(err).Str("user", userID).Str("object", objectAPIName).Msg("Failed to track object interaction")
	}
}

// TrackRecordAccess tracks when a user accesses a specific record
// This is called when viewing, editing, or creating a record
func (s *Service) TrackRecordAccess(ctx context.Context, userID, objectAPIName, recordID string) {
	if userID == "" || objectAPIName == "" || recordID == "" {
		return
	}

	// Track both record access and object interaction
	if err := s.repo.TrackRecentItem(ctx, userID, objectAPIName, recordID); err != nil {
		log.Warn().Err(err).Str("user", userID).Str("object", objectAPIName).Str("record", recordID).Msg("Failed to track recent item")
	}

	// Also increment object interaction count
	s.TrackObjectInteraction(ctx, userID, objectAPIName)
}

// TrackRecordAccessAsync records fire-and-forget MRU activity without tying the
// write to the request's cancellation path. The bounded child context prevents
// leaked work, and the recover keeps best-effort MRU from crashing request
// handlers.
func (s *Service) TrackRecordAccessAsync(ctx context.Context, userID, objectAPIName, recordID string) {
	if s == nil || userID == "" || objectAPIName == "" || recordID == "" {
		return
	}
	traceparent, requestID := trackRecordAccessAsyncObservability(ctx)
	go func() {
		defer func() {
			if recovered := recover(); recovered != nil {
				log.Error().
					Interface("panic", recovered).
					Str("user", userID).
					Str("object", objectAPIName).
					Str("record", recordID).
					Msg("Recovered from async MRU tracking panic")
			}
		}()
		workCtx, cancel := trackRecordAccessAsyncContext(traceparent, requestID)
		defer cancel()
		if err := s.repo.TrackRecentItem(workCtx, userID, objectAPIName, recordID); err != nil {
			log.Warn().Err(err).Str("user", userID).Str("object", objectAPIName).Str("record", recordID).Msg("Failed to track recent item")
		}
		s.enqueueObjectInteraction(traceparent, requestID, userID, objectAPIName)
	}()
}

func (s *Service) enqueueObjectInteraction(traceparent, requestID, userID, objectAPIName string) {
	if s == nil || userID == "" || objectAPIName == "" {
		return
	}
	if s.objectInteractions == nil {
		workCtx, cancel := trackRecordAccessAsyncContext(traceparent, requestID)
		defer cancel()
		if err := s.repo.TrackObjectInteraction(workCtx, userID, objectAPIName); err != nil {
			log.Warn().Err(err).Str("user", userID).Str("object", objectAPIName).Msg("Failed to track object interaction")
		}
		return
	}
	s.objectInteractions.enqueue(traceparent, requestID, userID, objectAPIName)
}

func trackRecordAccessAsyncObservability(ctx context.Context) (string, string) {
	if ctx == nil {
		return "", ""
	}
	traceparent, _ := ctx.Value(constants.ContextKeyTraceparent).(string)
	requestID, _ := ctx.Value(constants.ContextKeyRequestID).(string)
	return traceparent, requestID
}

func trackRecordAccessAsyncContext(traceparent, requestID string) (context.Context, context.CancelFunc) {
	ctx := context.Background()
	if traceparent != "" {
		ctx = context.WithValue(ctx, constants.ContextKeyTraceparent, traceparent)
	}
	if requestID != "" {
		ctx = context.WithValue(ctx, constants.ContextKeyRequestID, requestID)
	}
	return context.WithTimeout(ctx, trackRecordAccessAsyncTimeout)
}

type objectInteractionCountRepository interface {
	TrackObjectInteractionCount(ctx context.Context, userID, objectAPIName string, count int) error
}

type objectInteractionKey struct {
	userID        string
	objectAPIName string
}

type objectInteractionPending struct {
	count       int
	traceparent string
	requestID   string
}

type objectInteractionCoalescer struct {
	repo         ports.MRURepository
	mu           sync.Mutex
	pending      map[objectInteractionKey]objectInteractionPending
	timer        *time.Timer
	flushDelay   time.Duration
	flushTimeout time.Duration
}

func newObjectInteractionCoalescer(repo ports.MRURepository) *objectInteractionCoalescer {
	if repo == nil {
		return nil
	}
	return &objectInteractionCoalescer{
		repo:         repo,
		pending:      make(map[objectInteractionKey]objectInteractionPending),
		flushDelay:   objectInteractionFlushDelay,
		flushTimeout: objectInteractionFlushTimeout,
	}
}

func (c *objectInteractionCoalescer) enqueue(traceparent, requestID, userID, objectAPIName string) {
	if c == nil || c.repo == nil || userID == "" || objectAPIName == "" {
		return
	}
	c.mu.Lock()
	key := objectInteractionKey{userID: userID, objectAPIName: objectAPIName}
	pending := c.pending[key]
	pending.count++
	if pending.traceparent == "" {
		pending.traceparent = traceparent
	}
	if pending.requestID == "" {
		pending.requestID = requestID
	}
	c.pending[key] = pending
	if c.timer == nil {
		c.timer = time.AfterFunc(c.flushDelay, c.flush)
	}
	c.mu.Unlock()
}

func (c *objectInteractionCoalescer) flush() {
	defer func() {
		if recovered := recover(); recovered != nil {
			log.Error().
				Interface("panic", recovered).
				Msg("Recovered from async MRU object interaction flush panic")
		}
	}()
	c.mu.Lock()
	pending := c.pending
	c.pending = make(map[objectInteractionKey]objectInteractionPending)
	c.timer = nil
	c.mu.Unlock()

	for key, item := range pending {
		if item.count <= 0 {
			continue
		}
		ctx, cancel := objectInteractionFlushContext(item.traceparent, item.requestID, c.flushTimeout)
		err := trackObjectInteractionCount(ctx, c.repo, key.userID, key.objectAPIName, item.count)
		cancel()
		if err != nil {
			log.Warn().Err(err).Str("user", key.userID).Str("object", key.objectAPIName).Int("count", item.count).Msg("Failed to track object interaction")
		}
	}
}

func objectInteractionFlushContext(traceparent, requestID string, timeout time.Duration) (context.Context, context.CancelFunc) {
	ctx := context.Background()
	if traceparent != "" {
		ctx = context.WithValue(ctx, constants.ContextKeyTraceparent, traceparent)
	}
	if requestID != "" {
		ctx = context.WithValue(ctx, constants.ContextKeyRequestID, requestID)
	}
	return context.WithTimeout(ctx, timeout)
}

func trackObjectInteractionCount(ctx context.Context, repo ports.MRURepository, userID, objectAPIName string, count int) error {
	if repo == nil || count <= 0 {
		return nil
	}
	if counter, ok := repo.(objectInteractionCountRepository); ok {
		return counter.TrackObjectInteractionCount(ctx, userID, objectAPIName, count)
	}
	for i := 0; i < count; i++ {
		if err := repo.TrackObjectInteraction(ctx, userID, objectAPIName); err != nil {
			return err
		}
	}
	return nil
}

// GetScopeOrder returns the user's search scope order.
func (s *Service) GetScopeOrder(ctx context.Context, userID string) []ScopeOrderItem {
	// Get user's scope order from database
	interactions, err := s.repo.GetScopeOrder(ctx, userID)
	if err != nil {
		log.Warn().Err(err).Str("user", userID).Msg("Failed to get scope order, using defaults")
		return s.buildScopeOrderResponse(ctx, DefaultScopeObjects())
	}

	if len(interactions) == 0 {
		return s.buildScopeOrderResponse(ctx, DefaultScopeObjects())
	}

	// Build ordered list
	objects := make([]string, len(interactions))
	for i, oi := range interactions {
		objects[i] = oi.ObjectAPIName
	}

	return s.buildScopeOrderResponse(ctx, objects)
}

// BuildScopeOrderResponse builds a ScopeOrderItem slice from object names.
// Exposed for handlers that need to construct scope order from metadata-sourced lists.
func (s *Service) BuildScopeOrderResponse(ctx context.Context, objects []string) []ScopeOrderItem {
	return s.buildScopeOrderResponse(ctx, objects)
}

func (s *Service) buildScopeOrderResponse(ctx context.Context, objects []string) []ScopeOrderItem {
	// Batch fetch metadata for all objects
	metadataMap, err := s.repo.GetObjectMetaBatch(ctx, objects)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to batch fetch object metadata for scope order")
		metadataMap = make(map[string]*models.ObjectMetadata)
	}

	result := make([]ScopeOrderItem, len(objects))
	for i, obj := range objects {
		label := obj // Default label is the API name
		if meta, ok := metadataMap[obj]; ok && meta != nil {
			label = meta.Label
		}

		keyPrefix := ""
		if meta, ok := metadataMap[obj]; ok && meta != nil && meta.KeyPrefix != nil {
			keyPrefix = *meta.KeyPrefix
		}

		result[i] = ScopeOrderItem{
			Type:      obj,
			Name:      obj,
			Label:     label,
			KeyPrefix: keyPrefix,
		}
	}
	return result
}

// SetObjectPinned pins or unpins an object for a user
func (s *Service) SetObjectPinned(ctx context.Context, userID, objectAPIName string, pinned bool) error {
	return s.repo.SetObjectPinned(ctx, userID, objectAPIName, pinned)
}

// GetRelevantItems returns the user's recently accessed records.
func (s *Service) GetRelevantItems(ctx context.Context, userID string, sobjects []string, lastUpdatedID string) ([]RelevantItem, string, error) {
	// Parse lastUpdatedId
	var lastUpdatedInt int64
	if lastUpdatedID != "" {
		if parsed, err := strconv.ParseInt(lastUpdatedID, 10, 64); err == nil {
			lastUpdatedInt = parsed
		}
	}

	// Get recent items from database
	items, maxCounter, err := s.repo.GetRelevantItems(ctx, userID, sobjects, lastUpdatedInt)
	if err != nil {
		return nil, "", err
	}

	if len(items) == 0 {
		return []RelevantItem{}, strconv.FormatInt(maxCounter, 10), nil
	}

	// Group record IDs by object for batch Name lookup
	byObject := make(map[string][]string)
	for _, item := range items {
		byObject[item.ObjectAPIName] = append(byObject[item.ObjectAPIName], item.RecordID)
	}

	// Batch fetch record names (map of recordID -> Name)
	recordNames, err := s.repo.GetRecordNamesBatch(ctx, byObject)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to batch fetch record names, using IDs as names")
		recordNames = make(map[string]string)
	}

	result := make([]RelevantItem, 0, len(items))
	for _, item := range items {
		name := item.RecordID // Default to ID if name not found
		if n, ok := recordNames[item.RecordID]; ok && n != "" {
			name = n
		}

		result = append(result, RelevantItem{
			ObjectAPIName: item.ObjectAPIName,
			RecordID:      item.RecordID,
			Name:          name,
		})
	}

	return result, strconv.FormatInt(maxCounter, 10), nil
}
