package use_cases

import (
	"context"
	"time"

	"github.com/rwadurian/mpc-system/pkg/logger"
	"github.com/rwadurian/mpc-system/services/session-coordinator/application/ports/output"
	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/repositories"
	"go.uber.org/zap"
)

// CheckPartyTimeoutsUseCase handles checking for timed-out parties in active sessions
type CheckPartyTimeoutsUseCase struct {
	sessionRepo      repositories.SessionRepository
	eventPublisher   output.MessageBrokerPort
	inactivityTimeout time.Duration
}

// CheckPartyTimeoutsResult contains the result of the timeout check
type CheckPartyTimeoutsResult struct {
	SessionsChecked    int
	PartiesTimedOut    int
	SessionsFailedDueToTimeout int
}

// NewCheckPartyTimeoutsUseCase creates a new check party timeouts use case
func NewCheckPartyTimeoutsUseCase(
	sessionRepo repositories.SessionRepository,
	eventPublisher output.MessageBrokerPort,
	inactivityTimeout time.Duration,
) *CheckPartyTimeoutsUseCase {
	// Default to 2 minutes if not specified
	if inactivityTimeout <= 0 {
		inactivityTimeout = 2 * time.Minute
	}
	return &CheckPartyTimeoutsUseCase{
		sessionRepo:       sessionRepo,
		eventPublisher:    eventPublisher,
		inactivityTimeout: inactivityTimeout,
	}
}

// Execute checks all active sessions for timed-out parties
func (uc *CheckPartyTimeoutsUseCase) Execute(ctx context.Context) (*CheckPartyTimeoutsResult, error) {
	result := &CheckPartyTimeoutsResult{}

	// 1. Find all active sessions
	sessions, err := uc.sessionRepo.FindActive(ctx)
	if err != nil {
		return nil, err
	}

	result.SessionsChecked = len(sessions)

	for _, session := range sessions {
		// 2. Check for timed-out parties
		timedOutParties := session.MarkTimedOutPartiesAsFailed(uc.inactivityTimeout)

		if len(timedOutParties) == 0 {
			continue
		}

		result.PartiesTimedOut += len(timedOutParties)

		// 3. Publish timeout events for each timed-out party
		for _, party := range timedOutParties {
			event := output.ParticipantTimedOutEvent{
				SessionID:         session.ID.String(),
				PartyID:           party.PartyID.String(),
				InactivitySeconds: int64(party.TimeSinceLastActivity().Seconds()),
				TimedOutAt:        time.Now().UnixMilli(),
			}
			if err := uc.eventPublisher.PublishEvent(ctx, output.TopicParticipantTimedOut, event); err != nil {
				logger.Error("failed to publish participant timeout event",
					zap.String("session_id", session.ID.String()),
					zap.String("party_id", party.PartyID.String()),
					zap.Error(err))
			}

			logger.Warn("Party timed out due to inactivity",
				zap.String("session_id", session.ID.String()),
				zap.String("party_id", party.PartyID.String()),
				zap.Duration("inactivity", party.TimeSinceLastActivity()),
				zap.Duration("timeout_threshold", uc.inactivityTimeout))
		}

		// 4. If any party timed out in an in_progress session, fail the entire session
		// MPC requires all parties to participate; if one fails, the session fails
		if session.Status.IsActive() && len(timedOutParties) > 0 {
			if err := session.Fail(); err != nil {
				logger.Error("failed to mark session as failed after party timeout",
					zap.String("session_id", session.ID.String()),
					zap.Error(err))
			} else {
				result.SessionsFailedDueToTimeout++

				// Publish session failed event
				failedEvent := output.SessionFailedEvent{
					SessionID: session.ID.String(),
					Reason:    "party timed out due to inactivity",
					FailedAt:  time.Now().UnixMilli(),
				}
				if err := uc.eventPublisher.PublishEvent(ctx, output.TopicSessionFailed, failedEvent); err != nil {
					logger.Error("failed to publish session failed event",
						zap.String("session_id", session.ID.String()),
						zap.Error(err))
				}
			}
		}

		// 5. Save updated session
		if err := uc.sessionRepo.Update(ctx, session); err != nil {
			logger.Error("failed to update session after party timeout",
				zap.String("session_id", session.ID.String()),
				zap.Error(err))
		}
	}

	return result, nil
}

// GetInactivityTimeout returns the configured inactivity timeout
func (uc *CheckPartyTimeoutsUseCase) GetInactivityTimeout() time.Duration {
	return uc.inactivityTimeout
}
