package use_cases

import (
	"context"
	"fmt"

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

// DelegateUserShareInfo contains user's share for delegate party
type DelegateUserShareInfo struct {
	DelegatePartyID string
	EncryptedShare  []byte
	PartyIndex      int32
}

// MessageRouterClient interface for publishing session events
type MessageRouterClient interface {
	PublishSessionCreated(
		ctx context.Context,
		sessionID uuid.UUID,
		thresholdN int32,
		thresholdT int32,
		selectedParties []string,
		joinTokens map[string]string,
		messageHash []byte,
		createdAt int64,
		expiresAt int64,
		delegateUserShare *DelegateUserShareInfo,
	) error
}

// CreateSessionUseCase implements the create session use case
type CreateSessionUseCase struct {
	sessionRepo         repositories.SessionRepository
	tokenGen            jwt.TokenGenerator
	eventPublisher      output.MessageBrokerPort
	partyPool           output.PartyPoolPort
	messageRouterClient MessageRouterClient
	notificationService output.NotificationServicePort // TODO: Wire up notification service
	coordinatorSvc      *services.SessionCoordinatorService
}

// NewCreateSessionUseCase creates a new create session use case
func NewCreateSessionUseCase(
	sessionRepo repositories.SessionRepository,
	tokenGen jwt.TokenGenerator,
	eventPublisher output.MessageBrokerPort,
	partyPool output.PartyPoolPort,
	messageRouterClient MessageRouterClient,
) *CreateSessionUseCase {
	return &CreateSessionUseCase{
		sessionRepo:         sessionRepo,
		tokenGen:            tokenGen,
		eventPublisher:      eventPublisher,
		partyPool:           partyPool,
		messageRouterClient: messageRouterClient,
		notificationService: nil, // TODO: Inject notification service
		coordinatorSvc:      services.NewSessionCoordinatorService(),
	}
}

// NewCreateSessionUseCaseWithNotification creates a new create session use case with notification support
// TODO: Use this constructor when notification service is implemented
func NewCreateSessionUseCaseWithNotification(
	sessionRepo repositories.SessionRepository,
	tokenGen jwt.TokenGenerator,
	eventPublisher output.MessageBrokerPort,
	partyPool output.PartyPoolPort,
	messageRouterClient MessageRouterClient,
	notificationService output.NotificationServicePort,
) *CreateSessionUseCase {
	return &CreateSessionUseCase{
		sessionRepo:         sessionRepo,
		tokenGen:            tokenGen,
		eventPublisher:      eventPublisher,
		partyPool:           partyPool,
		messageRouterClient: messageRouterClient,
		notificationService: notificationService,
		coordinatorSvc:      services.NewSessionCoordinatorService(),
	}
}

// Execute executes the create session use case
func (uc *CreateSessionUseCase) Execute(
	ctx context.Context,
	req input.CreateSessionInput,
) (*input.CreateSessionOutput, error) {
	// 1. Create threshold value object
	threshold, err := value_objects.NewThreshold(req.ThresholdT, req.ThresholdN)
	if err != nil {
		return nil, err
	}

	// 2. Validate input
	sessionType := entities.SessionType(req.SessionType)
	if err := uc.coordinatorSvc.ValidateSessionCreation(
		sessionType,
		threshold,
		len(req.Participants),
		req.MessageHash,
	); err != nil {
		return nil, err
	}

	// 3. Calculate expiration
	expiresIn := req.ExpiresIn
	if expiresIn == 0 {
		expiresIn = uc.coordinatorSvc.CalculateSessionTimeout(sessionType)
	}

	// 4. Create session entity
	session, err := entities.NewMPCSession(
		sessionType,
		threshold,
		req.InitiatorID,
		expiresIn,
		req.MessageHash,
	)
	if err != nil {
		return nil, err
	}

	// 4.1 Set keygen_session_id for sign sessions
	if req.KeygenSessionID != uuid.Nil {
		session.KeygenSessionID = req.KeygenSessionID
		logger.Info("Sign session created with keygen_session_id",
			zap.String("session_id", session.ID.String()),
			zap.String("keygen_session_id", req.KeygenSessionID.String()))
	}

	// 4.2 Set wallet name and invite code for co_managed_keygen sessions
	if req.WalletName != "" {
		session.WalletName = req.WalletName
	}
	if req.InviteCode != "" {
		session.InviteCode = req.InviteCode
		logger.Info("Co-managed keygen session created with invite code",
			zap.String("session_id", session.ID.String()),
			zap.String("wallet_name", req.WalletName),
			zap.String("invite_code", req.InviteCode))
	}

	// 5. Add participants and generate join tokens
	tokens := make(map[string]string)
	if len(req.Participants) == 0 {
		// No participants provided - use party pool for automatic selection
		if uc.partyPool != nil {
			var selectedParties []output.PartyEndpoint
			var err error

			// Check if party composition is specified
			if req.PartyComposition != nil {
				// For co_managed_keygen, TemporaryCount represents external participants
				// who will join dynamically via invite code - don't select from pool
				if sessionType == entities.SessionTypeCoManagedKeygen {
					selectedParties, err = uc.selectPartiesByCompositionForCoManaged(req.PartyComposition)
				} else {
					// Select parties based on composition requirements
					selectedParties, err = uc.selectPartiesByComposition(req.PartyComposition)
				}
				if err != nil {
					logger.Warn("failed to select parties by composition, falling back to simple selection",
						zap.Error(err))
					// Try simple selection as fallback
					selectedParties, err = uc.partyPool.SelectParties(threshold.N())
				}
			} else {
				// Default behavior: MUST use persistent parties only
				// No fallback - fail if insufficient persistent parties
				selectedParties, err = uc.partyPool.SelectPartiesWithFilter(output.PartySelectionFilter{
					Count: threshold.N(),
					Role:  output.PartyRolePersistent,
				})
				if err != nil {
					// Return error immediately - insufficient persistent parties
					return nil, fmt.Errorf("insufficient persistent parties: need %d persistent parties but not enough available. Use PartyComposition to specify custom party requirements: %w", threshold.N(), err)
				}
				logger.Info("selected persistent parties by default",
					zap.String("session_id", session.ID.String()),
					zap.Int("party_count", len(selectedParties)))
			}

			if err != nil {
				logger.Warn("failed to select parties from pool, falling back to dynamic join",
					zap.Error(err),
					zap.Int("required_parties", threshold.N()))

				// Fallback: generate universal join token for dynamic joining
				universalToken, err := uc.tokenGen.GenerateJoinToken(session.ID.UUID(), "*", expiresIn)
				if err != nil {
					return nil, err
				}
				tokens["*"] = universalToken
			} else {
				// Track parties by mode for notification/push routing
				var realTimeParties []output.PartyEndpoint
				var offlineParties []output.PartyEndpoint

				// Add selected parties as participants
				for i, party := range selectedParties {
					partyID, err := value_objects.NewPartyID(party.PartyID)
					if err != nil {
						return nil, err
					}

					// Create participant with empty DeviceInfo (server parties don't have device info)
					participant, err := entities.NewParticipant(partyID, i, entities.DeviceInfo{})
					if err != nil {
						return nil, err
					}

					if err := session.AddParticipant(participant); err != nil {
						return nil, err
					}

					// Track delegate party for user share retrieval
					if party.Role == output.PartyRoleDelegate {
						session.DelegatePartyID = party.PartyID
					}

					// Generate join token for this party
					token, err := uc.tokenGen.GenerateJoinToken(session.ID.UUID(), party.PartyID, expiresIn)
					if err != nil {
						return nil, err
					}
					tokens[party.PartyID] = token

					// Categorize party by mode based on notification channel presence
					// Real-time mode: No notification channel - use Message Router push
					// Offline mode: Has notification channel - send notification, 24h to complete
					if party.IsRealTimeMode() {
						realTimeParties = append(realTimeParties, party)
					} else {
						offlineParties = append(offlineParties, party)
					}
				}

				logger.Info("selected parties from pool",
					zap.String("session_id", session.ID.String()),
					zap.Int("party_count", len(selectedParties)),
					zap.Int("realtime_parties", len(realTimeParties)),
					zap.Int("offline_parties", len(offlineParties)),
					zap.String("delegate_party", session.DelegatePartyID))

				// TODO: Send notifications to offline parties
				// Offline parties have notification channels and can complete within 24 hours
				if len(offlineParties) > 0 && uc.notificationService != nil {
					uc.sendNotificationsToOfflineParties(ctx, session, offlineParties, tokens)
				}

				// For co_managed_keygen sessions, also generate a wildcard token
				// for external participants who will join dynamically via invite code
				if sessionType == entities.SessionTypeCoManagedKeygen && req.PartyComposition != nil && req.PartyComposition.TemporaryCount > 0 {
					wildcardToken, err := uc.tokenGen.GenerateJoinToken(session.ID.UUID(), "*", expiresIn)
					if err != nil {
						return nil, err
					}
					tokens["*"] = wildcardToken
					logger.Info("generated wildcard token for external participants",
						zap.String("session_id", session.ID.String()),
						zap.Int("external_count", req.PartyComposition.TemporaryCount))
				}
			}
		} else {
			// No party pool configured - fallback to dynamic join
			universalToken, err := uc.tokenGen.GenerateJoinToken(session.ID.UUID(), "*", expiresIn)
			if err != nil {
				return nil, err
			}
			tokens["*"] = universalToken
		}
	} else {
		// For pre-registered participants (e.g., signing sessions with specified parties)
		// Use PartyIndex from input - this is the original index from keygen which must be preserved
		for _, pInfo := range req.Participants {
			partyID, err := value_objects.NewPartyID(pInfo.PartyID)
			if err != nil {
				return nil, err
			}

			// Use the original PartyIndex from keygen (critical for TSS signing)
			participant, err := entities.NewParticipant(partyID, pInfo.PartyIndex, pInfo.DeviceInfo)
			if err != nil {
				return nil, err
			}

			if err := session.AddParticipant(participant); err != nil {
				return nil, err
			}

			// Generate secure join token (JWT)
			token, err := uc.tokenGen.GenerateJoinToken(session.ID.UUID(), pInfo.PartyID, expiresIn)
			if err != nil {
				return nil, err
			}
			tokens[pInfo.PartyID] = token
		}
	}

	// 6. Save session
	if err := uc.sessionRepo.Save(ctx, session); err != nil {
		return nil, err
	}

	// 7. Publish session created event to RabbitMQ (legacy)
	event := output.SessionCreatedEvent{
		SessionID:    session.ID.String(),
		SessionType:  string(session.SessionType),
		ThresholdN:   session.Threshold.N(),
		ThresholdT:   session.Threshold.T(),
		Participants: session.GetPartyIDs(),
		CreatedBy:    session.CreatedBy,
		CreatedAt:    session.CreatedAt.UnixMilli(),
		ExpiresAt:    session.ExpiresAt.UnixMilli(),
	}

	if err := uc.eventPublisher.PublishEvent(ctx, output.TopicSessionCreated, event); err != nil {
		// Log error but don't fail the operation
		logger.Error("failed to publish session created event",
			zap.String("session_id", session.ID.String()),
			zap.Error(err))
	}

	// 8. Publish SessionEvent to Message Router (party-driven architecture)
	// Only publish if parties were selected from pool
	if len(session.GetPartyIDs()) > 0 && uc.messageRouterClient != nil {
		selectedParties := session.GetPartyIDs()

		// Convert delegate user share if provided (for sign sessions with delegate)
		var delegateUserShare *DelegateUserShareInfo
		if req.DelegateUserShare != nil {
			delegateUserShare = &DelegateUserShareInfo{
				DelegatePartyID: req.DelegateUserShare.DelegatePartyID,
				EncryptedShare:  req.DelegateUserShare.EncryptedShare,
				PartyIndex:      req.DelegateUserShare.PartyIndex,
			}
		}

		err := uc.messageRouterClient.PublishSessionCreated(
			ctx,
			session.ID.UUID(),
			int32(session.Threshold.N()),
			int32(session.Threshold.T()),
			selectedParties,
			tokens,
			session.MessageHash,
			session.CreatedAt.UnixMilli(),
			session.ExpiresAt.UnixMilli(),
			delegateUserShare,
		)
		if err != nil {
			// Log error but don't fail the operation
			logger.Error("failed to publish session event to message router",
				zap.String("session_id", session.ID.String()),
				zap.Error(err))
		} else {
			logger.Info("published session event to message router",
				zap.String("session_id", session.ID.String()),
				zap.Int("party_count", len(selectedParties)))
		}
	}

	// 9. Return output
	return &input.CreateSessionOutput{
		SessionID:  session.ID.UUID(),
		JoinTokens: tokens,
		ExpiresAt:  session.ExpiresAt,
	}, nil
}

// selectPartiesByCompositionForCoManaged selects parties for co_managed_keygen sessions
// For co_managed_keygen, TemporaryCount represents external participants who will join
// dynamically via invite code - we don't select them from pool, only select persistent parties
// IMPORTANT: Uses PartyRoleCoManagedPersistent to select parties that use two-phase event handling
func (uc *CreateSessionUseCase) selectPartiesByCompositionForCoManaged(composition *input.PartyComposition) ([]output.PartyEndpoint, error) {
	if uc.partyPool == nil {
		return nil, fmt.Errorf("party pool not configured")
	}

	var allSelected []output.PartyEndpoint

	// Select co-managed persistent parties (server-side parties with two-phase event handling)
	// These are different from normal persistent parties to avoid mixing with regular keygen/sign
	if composition.PersistentCount > 0 {
		persistent, err := uc.partyPool.SelectPartiesWithFilter(output.PartySelectionFilter{
			Count: composition.PersistentCount,
			Role:  output.PartyRoleCoManagedPersistent,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to select co-managed persistent parties: %w", err)
		}
		allSelected = append(allSelected, persistent...)
	}

	// Select delegate parties if any
	if composition.DelegateCount > 0 {
		delegate, err := uc.partyPool.SelectPartiesWithFilter(output.PartySelectionFilter{
			Count: composition.DelegateCount,
			Role:  output.PartyRoleDelegate,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to select delegate parties: %w", err)
		}
		allSelected = append(allSelected, delegate...)
	}

	// NOTE: TemporaryCount is intentionally NOT selected from pool for co_managed_keygen
	// External participants will join dynamically via invite code with wildcard token
	if composition.TemporaryCount > 0 {
		logger.Info("co_managed_keygen: TemporaryCount represents external participants, not selecting from pool",
			zap.Int("temporary_count", composition.TemporaryCount),
			zap.Int("persistent_selected", len(allSelected)))
	}

	// Apply custom filters if provided
	for _, filter := range composition.CustomFilters {
		customParties, err := uc.partyPool.SelectPartiesWithFilter(filter)
		if err != nil {
			return nil, fmt.Errorf("failed to select parties with custom filter: %w", err)
		}
		allSelected = append(allSelected, customParties...)
	}

	// For co_managed_keygen, it's OK to have no selected parties if all are external
	// The session will use wildcard tokens for all participants
	return allSelected, nil
}

// selectPartiesByComposition selects parties based on composition requirements
func (uc *CreateSessionUseCase) selectPartiesByComposition(composition *input.PartyComposition) ([]output.PartyEndpoint, error) {
	if uc.partyPool == nil {
		return nil, fmt.Errorf("party pool not configured")
	}

	var allSelected []output.PartyEndpoint

	// Select persistent parties
	if composition.PersistentCount > 0 {
		persistent, err := uc.partyPool.SelectPartiesWithFilter(output.PartySelectionFilter{
			Count: composition.PersistentCount,
			Role:  output.PartyRolePersistent,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to select persistent parties: %w", err)
		}
		allSelected = append(allSelected, persistent...)
	}

	// Select delegate parties
	if composition.DelegateCount > 0 {
		delegate, err := uc.partyPool.SelectPartiesWithFilter(output.PartySelectionFilter{
			Count: composition.DelegateCount,
			Role:  output.PartyRoleDelegate,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to select delegate parties: %w", err)
		}
		allSelected = append(allSelected, delegate...)
	}

	// Select temporary parties
	if composition.TemporaryCount > 0 {
		temporary, err := uc.partyPool.SelectPartiesWithFilter(output.PartySelectionFilter{
			Count: composition.TemporaryCount,
			Role:  output.PartyRoleTemporary,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to select temporary parties: %w", err)
		}
		allSelected = append(allSelected, temporary...)
	}

	// Apply custom filters if provided
	for _, filter := range composition.CustomFilters {
		customParties, err := uc.partyPool.SelectPartiesWithFilter(filter)
		if err != nil {
			return nil, fmt.Errorf("failed to select parties with custom filter: %w", err)
		}
		allSelected = append(allSelected, customParties...)
	}

	// If no parties were selected (all counts are 0 and no custom filters), return error
	// This prevents falling back to unfiltered selection
	if len(allSelected) == 0 {
		return nil, fmt.Errorf("PartyComposition specified but no parties selected: all counts are zero and no custom filters provided")
	}

	return allSelected, nil
}

// ExtractPartyIDs extracts party IDs from participant info
func extractPartyIDs(participants []input.ParticipantInfo) []string {
	ids := make([]string, len(participants))
	for i, p := range participants {
		ids[i] = p.PartyID
	}
	return ids
}

// sendNotificationsToOfflineParties sends session invitation notifications to offline parties
// Offline parties have notification channels configured and can complete within 24 hours
// TODO: Implement actual notification sending when NotificationService is ready
func (uc *CreateSessionUseCase) sendNotificationsToOfflineParties(
	ctx context.Context,
	session *entities.MPCSession,
	offlineParties []output.PartyEndpoint,
	tokens map[string]string,
) {
	for _, party := range offlineParties {
		joinToken, exists := tokens[party.PartyID]
		if !exists {
			logger.Warn("no join token found for offline party",
				zap.String("session_id", session.ID.String()),
				zap.String("party_id", party.PartyID))
			continue
		}

		// Build notification input
		notificationInput := output.SessionInvitationInput{
			PartyID:     party.PartyID,
			SessionID:   session.ID.String(),
			SessionType: string(session.SessionType),
			JoinToken:   joinToken,
			ExpiresAt:   session.ExpiresAt.UnixMilli(),
			InitiatorID: session.CreatedBy,
		}

		// Add notification channels from party
		if party.Notification != nil {
			notificationInput.Email = party.Notification.Email
			notificationInput.Phone = party.Notification.Phone
			notificationInput.PushToken = party.Notification.PushToken
		}

		// Send notification (async to not block session creation)
		go func(input output.SessionInvitationInput) {
			if err := uc.notificationService.SendSessionInvitation(ctx, input); err != nil {
				logger.Error("failed to send session invitation notification",
					zap.String("session_id", input.SessionID),
					zap.String("party_id", input.PartyID),
					zap.Error(err))
			} else {
				logger.Info("session invitation notification sent",
					zap.String("session_id", input.SessionID),
					zap.String("party_id", input.PartyID),
					zap.Bool("has_email", input.Email != ""),
					zap.Bool("has_phone", input.Phone != ""),
					zap.Bool("has_push", input.PushToken != ""))
			}
		}(notificationInput)
	}
}
