// Package mictime runs room mic outbox consumers and exposes mic duration queries.
package mictime

import (
	"context"
	"log/slog"
	"time"

	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
	mictimedomain "hyapp/services/user-service/internal/domain/mictime"
)

// Repository applies room mic events and returns user duration aggregates.
type Repository interface {
	ProcessRoomMicEvent(ctx context.Context, event mictimedomain.RoomMicEvent) (mictimedomain.ApplyResult, error)
	CompensateOpenSessions(ctx context.Context, options mictimedomain.CompensationOptions) (mictimedomain.CompensationResult, error)
	GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error)
}

// RoomOutboxReader scans room-service RoomMicChanged facts in creation order.
type RoomOutboxReader interface {
	ListRoomMicEventsAfter(ctx context.Context, cursor mictimedomain.RoomOutboxCursor, batchSize int) ([]mictimedomain.RoomMicEvent, error)
}

// Service owns the user mic duration read model.
type Service struct {
	repository Repository
	reader     RoomOutboxReader
}

// WorkerOptions controls local room outbox polling.
type WorkerOptions struct {
	WorkerID     string
	PollInterval time.Duration
	BatchSize    int
}

// CompensationWorkerOptions controls abnormal open session closure.
type CompensationWorkerOptions struct {
	WorkerID                string
	BatchSize               int
	PendingPublishMaxAge    time.Duration
	PublishingSessionMaxAge time.Duration
}

// New creates a mic time service with persistence and optional room outbox reader.
func New(repository Repository, reader RoomOutboxReader) *Service {
	return &Service{repository: repository, reader: reader}
}

// GetLifetimeStats returns user-wide accumulated mic duration.
func (s *Service) GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error) {
	if s == nil || s.repository == nil {
		return mictimedomain.LifetimeStats{}, xerr.New(xerr.Unavailable, "mic time repository is not configured")
	}
	return s.repository.GetLifetimeStats(ctx, appCode, userID)
}

// RunRoomMicWorker consumes RoomMicChanged facts from room-service outbox.
func (s *Service) RunRoomMicWorker(ctx context.Context, options WorkerOptions) {
	if s == nil || s.repository == nil || s.reader == nil {
		logx.Warn(ctx, "worker_disabled", slog.String("worker", "room_mic"), slog.String("component", "user_mic_time"), slog.String("reason", "missing_dependency"))
		return
	}
	if options.PollInterval <= 0 {
		options.PollInterval = 5 * time.Second
	}
	if options.BatchSize <= 0 {
		options.BatchSize = 100
	}

	var cursor mictimedomain.RoomOutboxCursor
	for {
		processed, err := s.processBatch(ctx, &cursor, options.BatchSize)
		if err != nil {
			logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "room_mic"), slog.String("component", "user_mic_time"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize))
		}
		if ctx.Err() != nil {
			return
		}
		if processed >= options.BatchSize {
			// Backlog is still present; continue without sleeping until the cursor catches up.
			continue
		}
		select {
		case <-ctx.Done():
			return
		case <-time.After(options.PollInterval):
		}
	}
}

// ProcessOpenSessionCompensationBatch exposes one bounded compensation scan for cron-service.
func (s *Service) ProcessOpenSessionCompensationBatch(ctx context.Context, options CompensationWorkerOptions) (mictimedomain.CompensationResult, error) {
	if s == nil || s.repository == nil {
		return mictimedomain.CompensationResult{}, xerr.New(xerr.Unavailable, "mic time repository is not configured")
	}
	if options.BatchSize <= 0 {
		options.BatchSize = 100
	}
	if options.PendingPublishMaxAge <= 0 {
		options.PendingPublishMaxAge = 2 * time.Minute
	}
	if options.PublishingSessionMaxAge <= 0 {
		options.PublishingSessionMaxAge = 12 * time.Hour
	}
	return s.repository.CompensateOpenSessions(ctx, mictimedomain.CompensationOptions{
		NowMs:                     time.Now().UnixMilli(),
		BatchSize:                 options.BatchSize,
		PendingPublishMaxAgeMs:    options.PendingPublishMaxAge.Milliseconds(),
		PublishingSessionMaxAgeMs: options.PublishingSessionMaxAge.Milliseconds(),
	})
}

func (s *Service) processBatch(ctx context.Context, cursor *mictimedomain.RoomOutboxCursor, batchSize int) (int, error) {
	events, err := s.reader.ListRoomMicEventsAfter(ctx, *cursor, batchSize)
	if err != nil {
		return 0, err
	}
	for _, event := range events {
		if _, err := s.repository.ProcessRoomMicEvent(ctx, event); err != nil {
			return 0, err
		}
		cursor.CreatedAtMs = event.OutboxCreatedAtMs
		cursor.EventID = event.EventID
	}
	return len(events), nil
}
