package redis

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"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/value_objects"
)

const (
	sessionKeyPrefix      = "mpc:session:"
	sessionLockKeyPrefix  = "mpc:lock:session:"
	partyOnlineKeyPrefix  = "mpc:party:online:"
)

// SessionCacheAdapter implements SessionCachePort using Redis
type SessionCacheAdapter struct {
	client *redis.Client
}

// NewSessionCacheAdapter creates a new Redis session cache adapter
func NewSessionCacheAdapter(client *redis.Client) *SessionCacheAdapter {
	return &SessionCacheAdapter{client: client}
}

// CacheSession caches a session in Redis
func (a *SessionCacheAdapter) CacheSession(ctx context.Context, session *entities.MPCSession, ttl time.Duration) error {
	key := sessionKey(session.ID.UUID())

	data, err := json.Marshal(sessionToCacheEntry(session))
	if err != nil {
		return err
	}

	return a.client.Set(ctx, key, data, ttl).Err()
}

// GetCachedSession retrieves a session from Redis cache
func (a *SessionCacheAdapter) GetCachedSession(ctx context.Context, id uuid.UUID) (*entities.MPCSession, error) {
	key := sessionKey(id)

	data, err := a.client.Get(ctx, key).Bytes()
	if err != nil {
		if err == redis.Nil {
			return nil, nil // Cache miss
		}
		return nil, err
	}

	var entry sessionCacheEntry
	if err := json.Unmarshal(data, &entry); err != nil {
		return nil, err
	}

	return cacheEntryToSession(entry)
}

// InvalidateSession removes a session from cache
func (a *SessionCacheAdapter) InvalidateSession(ctx context.Context, id uuid.UUID) error {
	key := sessionKey(id)
	return a.client.Del(ctx, key).Err()
}

// AcquireLock attempts to acquire a distributed lock for a session
func (a *SessionCacheAdapter) AcquireLock(ctx context.Context, sessionID uuid.UUID, ttl time.Duration) (bool, error) {
	key := sessionLockKey(sessionID)

	// Use SET NX (only set if not exists)
	result, err := a.client.SetNX(ctx, key, "locked", ttl).Result()
	if err != nil {
		return false, err
	}
	return result, nil
}

// ReleaseLock releases a distributed lock for a session
func (a *SessionCacheAdapter) ReleaseLock(ctx context.Context, sessionID uuid.UUID) error {
	key := sessionLockKey(sessionID)
	return a.client.Del(ctx, key).Err()
}

// SetPartyOnline marks a party as online
func (a *SessionCacheAdapter) SetPartyOnline(ctx context.Context, sessionID uuid.UUID, partyID string, ttl time.Duration) error {
	key := partyOnlineKey(sessionID, partyID)
	return a.client.Set(ctx, key, "online", ttl).Err()
}

// IsPartyOnline checks if a party is online
func (a *SessionCacheAdapter) IsPartyOnline(ctx context.Context, sessionID uuid.UUID, partyID string) (bool, error) {
	key := partyOnlineKey(sessionID, partyID)
	exists, err := a.client.Exists(ctx, key).Result()
	if err != nil {
		return false, err
	}
	return exists > 0, nil
}

// GetOnlineParties returns all online parties for a session
func (a *SessionCacheAdapter) GetOnlineParties(ctx context.Context, sessionID uuid.UUID) ([]string, error) {
	pattern := fmt.Sprintf("%s%s:*", partyOnlineKeyPrefix, sessionID.String())

	var cursor uint64
	var parties []string

	for {
		keys, nextCursor, err := a.client.Scan(ctx, cursor, pattern, 100).Result()
		if err != nil {
			return nil, err
		}

		for _, key := range keys {
			// Extract party ID from key
			partyID := key[len(partyOnlineKeyPrefix)+len(sessionID.String())+1:]
			parties = append(parties, partyID)
		}

		cursor = nextCursor
		if cursor == 0 {
			break
		}
	}

	return parties, nil
}

// Helper functions

func sessionKey(id uuid.UUID) string {
	return sessionKeyPrefix + id.String()
}

func sessionLockKey(id uuid.UUID) string {
	return sessionLockKeyPrefix + id.String()
}

func partyOnlineKey(sessionID uuid.UUID, partyID string) string {
	return fmt.Sprintf("%s%s:%s", partyOnlineKeyPrefix, sessionID.String(), partyID)
}

// Cache entry structures

type sessionCacheEntry struct {
	ID           string                   `json:"id"`
	SessionType  string                   `json:"session_type"`
	ThresholdN   int                      `json:"threshold_n"`
	ThresholdT   int                      `json:"threshold_t"`
	Status       string                   `json:"status"`
	MessageHash  []byte                   `json:"message_hash,omitempty"`
	PublicKey    []byte                   `json:"public_key,omitempty"`
	CreatedBy    string                   `json:"created_by"`
	CreatedAt    int64                    `json:"created_at"`
	UpdatedAt    int64                    `json:"updated_at"`
	ExpiresAt    int64                    `json:"expires_at"`
	CompletedAt  *int64                   `json:"completed_at,omitempty"`
	Participants []participantCacheEntry  `json:"participants"`
	WalletName   string                   `json:"wallet_name,omitempty"`   // 钱包名称 (for co_managed_keygen)
	InviteCode   string                   `json:"invite_code,omitempty"`   // 邀请码 (for co_managed_keygen)
}

type participantCacheEntry struct {
	PartyID     string `json:"party_id"`
	PartyIndex  int    `json:"party_index"`
	Status      string `json:"status"`
	DeviceType  string `json:"device_type"`
	DeviceID    string `json:"device_id"`
	Platform    string `json:"platform"`
	AppVersion  string `json:"app_version"`
	JoinedAt    int64  `json:"joined_at"`
	CompletedAt *int64 `json:"completed_at,omitempty"`
}

func sessionToCacheEntry(s *entities.MPCSession) sessionCacheEntry {
	participants := make([]participantCacheEntry, len(s.Participants))
	for i, p := range s.Participants {
		var completedAt *int64
		if p.CompletedAt != nil {
			t := p.CompletedAt.UnixMilli()
			completedAt = &t
		}
		participants[i] = participantCacheEntry{
			PartyID:     p.PartyID.String(),
			PartyIndex:  p.PartyIndex,
			Status:      p.Status.String(),
			DeviceType:  string(p.DeviceInfo.DeviceType),
			DeviceID:    p.DeviceInfo.DeviceID,
			Platform:    p.DeviceInfo.Platform,
			AppVersion:  p.DeviceInfo.AppVersion,
			JoinedAt:    p.JoinedAt.UnixMilli(),
			CompletedAt: completedAt,
		}
	}

	var completedAt *int64
	if s.CompletedAt != nil {
		t := s.CompletedAt.UnixMilli()
		completedAt = &t
	}

	return sessionCacheEntry{
		ID:           s.ID.String(),
		SessionType:  string(s.SessionType),
		ThresholdN:   s.Threshold.N(),
		ThresholdT:   s.Threshold.T(),
		Status:       s.Status.String(),
		MessageHash:  s.MessageHash,
		PublicKey:    s.PublicKey,
		CreatedBy:    s.CreatedBy,
		CreatedAt:    s.CreatedAt.UnixMilli(),
		UpdatedAt:    s.UpdatedAt.UnixMilli(),
		ExpiresAt:    s.ExpiresAt.UnixMilli(),
		CompletedAt:  completedAt,
		Participants: participants,
		WalletName:   s.WalletName,
		InviteCode:   s.InviteCode,
	}
}

func cacheEntryToSession(entry sessionCacheEntry) (*entities.MPCSession, error) {
	id, err := uuid.Parse(entry.ID)
	if err != nil {
		return nil, err
	}

	participants := make([]*entities.Participant, len(entry.Participants))
	for i, p := range entry.Participants {
		partyID, err := value_objects.NewPartyID(p.PartyID)
		if err != nil {
			return nil, err
		}

		var completedAt *time.Time
		if p.CompletedAt != nil {
			t := time.UnixMilli(*p.CompletedAt)
			completedAt = &t
		}

		participants[i] = &entities.Participant{
			PartyID:    partyID,
			PartyIndex: p.PartyIndex,
			Status:     value_objects.ParticipantStatus(p.Status),
			DeviceInfo: entities.DeviceInfo{
				DeviceType: entities.DeviceType(p.DeviceType),
				DeviceID:   p.DeviceID,
				Platform:   p.Platform,
				AppVersion: p.AppVersion,
			},
			JoinedAt:    time.UnixMilli(p.JoinedAt),
			CompletedAt: completedAt,
		}
	}

	var completedAt *time.Time
	if entry.CompletedAt != nil {
		t := time.UnixMilli(*entry.CompletedAt)
		completedAt = &t
	}

	return entities.ReconstructSessionWithOptions(
		id,
		entry.SessionType,
		entry.ThresholdT,
		entry.ThresholdN,
		entry.Status,
		entry.MessageHash,
		entry.PublicKey,
		"",       // delegatePartyID - not cached
		uuid.Nil, // keygenSessionID - not cached
		entry.CreatedBy,
		time.UnixMilli(entry.CreatedAt),
		time.UnixMilli(entry.UpdatedAt),
		time.UnixMilli(entry.ExpiresAt),
		completedAt,
		participants,
		1, // version - default to 1 for cached sessions (not used in cache)
		&entities.ReconstructSessionOptions{
			WalletName: entry.WalletName,
			InviteCode: entry.InviteCode,
		},
	)
}

// Ensure interface compliance
var _ output.SessionCachePort = (*SessionCacheAdapter)(nil)
