package entities

import (
	"errors"
	"time"

	"github.com/google/uuid"
	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects"
)

var (
	ErrSessionNotFound         = errors.New("session not found")
	ErrSessionFull             = errors.New("session is full")
	ErrSessionExpired          = errors.New("session expired")
	ErrSessionNotInProgress    = errors.New("session not in progress")
	ErrParticipantNotFound     = errors.New("participant not found")
	ErrPartyNotInvited         = errors.New("party not invited to this session")
	ErrInvalidSessionType      = errors.New("invalid session type")
	ErrInvalidStatusTransition = errors.New("invalid status transition")
	ErrParticipantTimedOut     = errors.New("participant timed out")
	ErrOptimisticLockConflict  = errors.New("optimistic lock conflict: session was modified by another transaction")
)

// SessionType represents the type of MPC session
type SessionType string

const (
	SessionTypeKeygen          SessionType = "keygen"
	SessionTypeSign            SessionType = "sign"
	SessionTypeCoManagedKeygen SessionType = "co_managed_keygen" // 共管钱包密钥生成
)

// IsValid checks if the session type is valid
func (t SessionType) IsValid() bool {
	return t == SessionTypeKeygen || t == SessionTypeSign || t == SessionTypeCoManagedKeygen
}

// IsKeygen checks if the session type is a keygen type (includes co_managed_keygen)
func (t SessionType) IsKeygen() bool {
	return t == SessionTypeKeygen || t == SessionTypeCoManagedKeygen
}

// MPCSession represents an MPC session
// Coordinator only manages session metadata, does not participate in MPC computation
type MPCSession struct {
	ID              value_objects.SessionID
	SessionType     SessionType
	Threshold       value_objects.Threshold
	Participants    []*Participant
	Status          value_objects.SessionStatus
	MessageHash     []byte    // Used for Sign sessions
	PublicKey       []byte    // Group public key after Keygen completion
	DelegatePartyID string    // The delegate party ID (returns share to user instead of storing)
	KeygenSessionID uuid.UUID // For Sign sessions: which keygen session's shares to use
	CreatedBy       string
	CreatedAt       time.Time
	UpdatedAt       time.Time
	ExpiresAt       time.Time
	CompletedAt     *time.Time
	Version         int64 // Optimistic locking version number

	// Co-managed wallet specific fields (共管钱包特有字段)
	WalletName string // 钱包名称 (for co_managed_keygen)
	InviteCode string // 邀请码 (for co_managed_keygen)
}

// NewMPCSession creates a new MPC session
func NewMPCSession(
	sessionType SessionType,
	threshold value_objects.Threshold,
	createdBy string,
	expiresIn time.Duration,
	messageHash []byte, // Only for Sign sessions
) (*MPCSession, error) {
	if !sessionType.IsValid() {
		return nil, ErrInvalidSessionType
	}

	if sessionType == SessionTypeSign && len(messageHash) == 0 {
		return nil, errors.New("message hash required for sign session")
	}

	now := time.Now().UTC()
	return &MPCSession{
		ID:           value_objects.NewSessionID(),
		SessionType:  sessionType,
		Threshold:    threshold,
		Participants: make([]*Participant, 0, threshold.N()),
		Status:       value_objects.SessionStatusCreated,
		MessageHash:  messageHash,
		CreatedBy:    createdBy,
		CreatedAt:    now,
		UpdatedAt:    now,
		ExpiresAt:    now.Add(expiresIn),
		Version:      1,
	}, nil
}

// AddParticipant adds a participant to the session
func (s *MPCSession) AddParticipant(p *Participant) error {
	// For sign sessions, the max participant check is handled at the API level
	// (co-managed uses T, persistent uses T+1)
	// Here we just prevent exceeding N which is the absolute maximum
	if len(s.Participants) >= s.Threshold.N() {
		return ErrSessionFull
	}
	s.Participants = append(s.Participants, p)
	s.UpdatedAt = time.Now().UTC()
	return nil
}

// GetParticipant gets a participant by party ID
func (s *MPCSession) GetParticipant(partyID value_objects.PartyID) (*Participant, error) {
	for _, p := range s.Participants {
		if p.PartyID.Equals(partyID) {
			return p, nil
		}
	}
	return nil, ErrParticipantNotFound
}

// UpdateParticipantStatus updates a participant's status
func (s *MPCSession) UpdateParticipantStatus(partyID value_objects.PartyID, status value_objects.ParticipantStatus) error {
	for _, p := range s.Participants {
		if p.PartyID.Equals(partyID) {
			switch status {
			case value_objects.ParticipantStatusJoined:
				return p.Join()
			case value_objects.ParticipantStatusReady:
				return p.MarkReady()
			case value_objects.ParticipantStatusCompleted:
				return p.MarkCompleted()
			case value_objects.ParticipantStatusFailed:
				p.MarkFailed()
				return nil
			default:
				return errors.New("invalid status")
			}
		}
	}
	return ErrParticipantNotFound
}

// CanStart checks if all participants have joined and the session can start
func (s *MPCSession) CanStart() bool {
	// For keygen sessions (including co-managed keygen): must have exactly N participants
	if s.SessionType.IsKeygen() {
		if len(s.Participants) != s.Threshold.N() {
			return false
		}
		readyCount := 0
		for _, p := range s.Participants {
			if p.IsJoined() || p.IsReady() {
				readyCount++
			}
		}
		return readyCount == s.Threshold.N()
	}

	// For sign sessions: check all registered participants have joined
	// The number of participants was determined at session creation time (T or T+1)
	if len(s.Participants) == 0 {
		return false
	}
	for _, p := range s.Participants {
		if !p.IsJoined() && !p.IsReady() {
			return false
		}
	}
	return true
}

// Start transitions the session to in_progress
func (s *MPCSession) Start() error {
	// If already in progress, just return success (idempotent)
	if s.Status == value_objects.SessionStatusInProgress {
		return nil
	}
	if !s.Status.CanTransitionTo(value_objects.SessionStatusInProgress) {
		return ErrInvalidStatusTransition
	}
	if !s.CanStart() {
		return errors.New("not all participants have joined")
	}
	s.Status = value_objects.SessionStatusInProgress
	s.UpdatedAt = time.Now().UTC()
	return nil
}

// Complete marks the session as completed
func (s *MPCSession) Complete(publicKey []byte) error {
	if !s.Status.CanTransitionTo(value_objects.SessionStatusCompleted) {
		return ErrInvalidStatusTransition
	}
	s.Status = value_objects.SessionStatusCompleted
	s.PublicKey = publicKey
	now := time.Now().UTC()
	s.CompletedAt = &now
	s.UpdatedAt = now
	return nil
}

// Fail marks the session as failed
func (s *MPCSession) Fail() error {
	if !s.Status.CanTransitionTo(value_objects.SessionStatusFailed) {
		return ErrInvalidStatusTransition
	}
	s.Status = value_objects.SessionStatusFailed
	s.UpdatedAt = time.Now().UTC()
	return nil
}

// Expire marks the session as expired
func (s *MPCSession) Expire() error {
	if !s.Status.CanTransitionTo(value_objects.SessionStatusExpired) {
		return ErrInvalidStatusTransition
	}
	s.Status = value_objects.SessionStatusExpired
	s.UpdatedAt = time.Now().UTC()
	return nil
}

// IsExpired checks if the session has expired
func (s *MPCSession) IsExpired() bool {
	return time.Now().UTC().After(s.ExpiresAt)
}

// IsActive checks if the session is active
func (s *MPCSession) IsActive() bool {
	return s.Status.IsActive() && !s.IsExpired()
}

// IsParticipant checks if a party is a participant
func (s *MPCSession) IsParticipant(partyID value_objects.PartyID) bool {
	for _, p := range s.Participants {
		if p.PartyID.Equals(partyID) {
			return true
		}
	}
	return false
}

// AllCompleted checks if all participants have completed
func (s *MPCSession) AllCompleted() bool {
	for _, p := range s.Participants {
		if !p.IsCompleted() {
			return false
		}
	}
	return true
}

// CompletedCount returns the number of completed participants
func (s *MPCSession) CompletedCount() int {
	count := 0
	for _, p := range s.Participants {
		if p.IsCompleted() {
			count++
		}
	}
	return count
}

// MarkPartyReady marks a participant as ready by party ID string
func (s *MPCSession) MarkPartyReady(partyID string) error {
	for _, p := range s.Participants {
		if p.PartyID.String() == partyID {
			return p.MarkReady()
		}
	}
	return ErrParticipantNotFound
}

// AllPartiesReady checks if all participants are ready
func (s *MPCSession) AllPartiesReady() bool {
	// For keygen sessions: must have exactly N participants
	if s.SessionType.IsKeygen() {
		if len(s.Participants) != s.Threshold.N() {
			return false
		}
		for _, p := range s.Participants {
			if !p.IsReady() && !p.IsCompleted() {
				return false
			}
		}
		return true
	}

	// For sign sessions: check all registered participants are ready
	if len(s.Participants) == 0 {
		return false
	}
	for _, p := range s.Participants {
		if !p.IsReady() && !p.IsCompleted() {
			return false
		}
	}
	return true
}

// JoinedCount returns the number of joined participants
func (s *MPCSession) JoinedCount() int {
	count := 0
	for _, p := range s.Participants {
		if p.IsJoined() {
			count++
		}
	}
	return count
}

// GetPartyIDs returns all party IDs
func (s *MPCSession) GetPartyIDs() []string {
	ids := make([]string, len(s.Participants))
	for i, p := range s.Participants {
		ids[i] = p.PartyID.String()
	}
	return ids
}

// GetOtherParties returns participants except the specified party
func (s *MPCSession) GetOtherParties(excludePartyID value_objects.PartyID) []*Participant {
	others := make([]*Participant, 0, len(s.Participants)-1)
	for _, p := range s.Participants {
		if !p.PartyID.Equals(excludePartyID) {
			others = append(others, p)
		}
	}
	return others
}

// UpdatePartyActivity updates the activity timestamp for a party
func (s *MPCSession) UpdatePartyActivity(partyID value_objects.PartyID) error {
	for _, p := range s.Participants {
		if p.PartyID.Equals(partyID) {
			p.UpdateActivity()
			s.UpdatedAt = time.Now().UTC()
			return nil
		}
	}
	return ErrParticipantNotFound
}

// GetTimedOutParties returns participants that have exceeded the inactivity timeout
// Only checks active participants (joined or ready, not yet completed/failed)
func (s *MPCSession) GetTimedOutParties(timeout time.Duration) []*Participant {
	timedOut := make([]*Participant, 0)
	for _, p := range s.Participants {
		// Only check active participants
		if p.IsJoined() && !p.IsCompleted() && !p.IsFailed() {
			if p.IsTimedOut(timeout) {
				timedOut = append(timedOut, p)
			}
		}
	}
	return timedOut
}

// HasTimedOutParties checks if any active participant has timed out
func (s *MPCSession) HasTimedOutParties(timeout time.Duration) bool {
	return len(s.GetTimedOutParties(timeout)) > 0
}

// MarkTimedOutPartiesAsFailed marks all timed-out parties as failed
func (s *MPCSession) MarkTimedOutPartiesAsFailed(timeout time.Duration) []*Participant {
	timedOut := s.GetTimedOutParties(timeout)
	for _, p := range timedOut {
		p.MarkFailed()
	}
	if len(timedOut) > 0 {
		s.UpdatedAt = time.Now().UTC()
	}
	return timedOut
}

// ToDTO converts to a DTO for API responses
func (s *MPCSession) ToDTO() SessionDTO {
	participants := make([]ParticipantDTO, len(s.Participants))
	for i, p := range s.Participants {
		participants[i] = ParticipantDTO{
			PartyID:    p.PartyID.String(),
			PartyIndex: p.PartyIndex,
			Status:     p.Status.String(),
			DeviceType: string(p.DeviceInfo.DeviceType),
		}
	}

	return SessionDTO{
		ID:           s.ID.String(),
		SessionType:  string(s.SessionType),
		ThresholdN:   s.Threshold.N(),
		ThresholdT:   s.Threshold.T(),
		Participants: participants,
		Status:       s.Status.String(),
		CreatedAt:    s.CreatedAt,
		ExpiresAt:    s.ExpiresAt,
		WalletName:   s.WalletName,
		InviteCode:   s.InviteCode,
	}
}

// SessionDTO is a data transfer object for sessions
type SessionDTO struct {
	ID           string           `json:"id"`
	SessionType  string           `json:"session_type"`
	ThresholdN   int              `json:"threshold_n"`
	ThresholdT   int              `json:"threshold_t"`
	Participants []ParticipantDTO `json:"participants"`
	Status       string           `json:"status"`
	CreatedAt    time.Time        `json:"created_at"`
	ExpiresAt    time.Time        `json:"expires_at"`
	WalletName   string           `json:"wallet_name,omitempty"`   // 钱包名称 (for co_managed_keygen)
	InviteCode   string           `json:"invite_code,omitempty"`   // 邀请码 (for co_managed_keygen)
}

// ParticipantDTO is a data transfer object for participants
type ParticipantDTO struct {
	PartyID    string `json:"party_id"`
	PartyIndex int    `json:"party_index"`
	Status     string `json:"status"`
	DeviceType string `json:"device_type"`
}

// ReconstructSessionOptions contains optional fields for session reconstruction
type ReconstructSessionOptions struct {
	WalletName string // 钱包名称 (for co_managed_keygen)
	InviteCode string // 邀请码 (for co_managed_keygen)
}

// ReconstructSession reconstructs an MPCSession from database
// This is the original signature - kept for backward compatibility
func ReconstructSession(
	id uuid.UUID,
	sessionType string,
	thresholdT, thresholdN int,
	status string,
	messageHash, publicKey []byte,
	delegatePartyID string,
	keygenSessionID uuid.UUID,
	createdBy string,
	createdAt, updatedAt, expiresAt time.Time,
	completedAt *time.Time,
	participants []*Participant,
	version int64,
) (*MPCSession, error) {
	return ReconstructSessionWithOptions(
		id, sessionType, thresholdT, thresholdN, status,
		messageHash, publicKey, delegatePartyID, keygenSessionID,
		createdBy, createdAt, updatedAt, expiresAt, completedAt,
		participants, version, nil,
	)
}

// ReconstructSessionWithOptions reconstructs an MPCSession with optional co-managed wallet fields
func ReconstructSessionWithOptions(
	id uuid.UUID,
	sessionType string,
	thresholdT, thresholdN int,
	status string,
	messageHash, publicKey []byte,
	delegatePartyID string,
	keygenSessionID uuid.UUID,
	createdBy string,
	createdAt, updatedAt, expiresAt time.Time,
	completedAt *time.Time,
	participants []*Participant,
	version int64,
	opts *ReconstructSessionOptions,
) (*MPCSession, error) {
	sessionStatus, err := value_objects.NewSessionStatus(status)
	if err != nil {
		return nil, err
	}

	threshold, err := value_objects.NewThreshold(thresholdT, thresholdN)
	if err != nil {
		return nil, err
	}

	session := &MPCSession{
		ID:              value_objects.SessionIDFromUUID(id),
		SessionType:     SessionType(sessionType),
		Threshold:       threshold,
		Participants:    participants,
		Status:          sessionStatus,
		MessageHash:     messageHash,
		PublicKey:       publicKey,
		DelegatePartyID: delegatePartyID,
		KeygenSessionID: keygenSessionID,
		CreatedBy:       createdBy,
		CreatedAt:       createdAt,
		UpdatedAt:       updatedAt,
		ExpiresAt:       expiresAt,
		CompletedAt:     completedAt,
		Version:         version,
	}

	// Apply optional fields if provided
	if opts != nil {
		session.WalletName = opts.WalletName
		session.InviteCode = opts.InviteCode
	}

	return session, nil
}
