package jackbox

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

	"github.com/gorilla/websocket"
	"github.com/sirupsen/logrus"
)

// WebSocketClient handles WebSocket connection to Jackbox API
type WebSocketClient struct {
	apiURL              string
	token               string
	conn                *websocket.Conn
	messageCallback     func(string)
	apiClient           *Client // Reference to API client for vote tracking
	log                 *logrus.Entry
	mu                  sync.Mutex
	reconnectDelay      time.Duration
	maxReconnect        time.Duration
	stopChan            chan struct{}
	connected           bool
	authenticated       bool
	subscribedSession      int
	enableRoomCodeImage    bool          // Whether to upload room code images to Kosmi
	roomCodeImageDelay     time.Duration // Delay before sending image announcement
	roomCodePlaintextDelay time.Duration // Delay before sending plaintext room code
}

// WebSocket message types
type WSMessage struct {
	Type      string          `json:"type"`
	Token     string          `json:"token,omitempty"`
	SessionID int             `json:"sessionId,omitempty"`
	Message   string          `json:"message,omitempty"`
	Timestamp string          `json:"timestamp,omitempty"`
	Data      json.RawMessage `json:"data,omitempty"`
}

// SessionStartedData represents the session.started event data
type SessionStartedData struct {
	Session struct {
		ID        int    `json:"id"`
		IsActive  int    `json:"is_active"`
		CreatedAt string `json:"created_at"`
		Notes     string `json:"notes"`
	} `json:"session"`
}

// GameAddedData represents the game.added event data
type GameAddedData struct {
	Session struct {
		ID          int  `json:"id"`
		IsActive    bool `json:"is_active"`
		GamesPlayed int  `json:"games_played"`
	} `json:"session"`
	Game struct {
		ID            int    `json:"id"`
		Title         string `json:"title"`
		PackName      string `json:"pack_name"`
		MinPlayers    int    `json:"min_players"`
		MaxPlayers    int    `json:"max_players"`
		ManuallyAdded bool   `json:"manually_added"`
		RoomCode      string `json:"room_code"`
	} `json:"game"`
}

// NewWebSocketClient creates a new WebSocket client
func NewWebSocketClient(apiURL, token string, messageCallback func(string), apiClient *Client, enableRoomCodeImage bool, roomCodeImageDelay, roomCodePlaintextDelay time.Duration, log *logrus.Entry) *WebSocketClient {
	return &WebSocketClient{
		apiURL:                 apiURL,
		token:                  token,
		messageCallback:        messageCallback,
		apiClient:              apiClient,
		enableRoomCodeImage:    enableRoomCodeImage,
		roomCodeImageDelay:     roomCodeImageDelay,
		roomCodePlaintextDelay: roomCodePlaintextDelay,
		log:                    log,
		reconnectDelay:         1 * time.Second,
		maxReconnect:           30 * time.Second,
		stopChan:               make(chan struct{}),
	}
}

// Connect establishes WebSocket connection
func (c *WebSocketClient) Connect() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Convert http(s):// to ws(s)://
	wsURL := c.apiURL
	if len(wsURL) > 7 && wsURL[:7] == "http://" {
		wsURL = "ws://" + wsURL[7:]
	} else if len(wsURL) > 8 && wsURL[:8] == "https://" {
		wsURL = "wss://" + wsURL[8:]
	}

	wsURL += "/api/sessions/live"

	c.log.Infof("Connecting to WebSocket: %s", wsURL)

	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		return fmt.Errorf("failed to connect: %w", err)
	}

	c.conn = conn
	c.connected = true
	c.log.Info("WebSocket connected")

	// Start message listener
	go c.listen()

	// Authenticate
	return c.authenticate()
}

// authenticate sends authentication message
func (c *WebSocketClient) authenticate() error {
	msg := WSMessage{
		Type:  "auth",
		Token: c.token,
	}

	if err := c.sendMessage(msg); err != nil {
		return fmt.Errorf("failed to send auth: %w", err)
	}

	c.log.Debug("Authentication message sent")
	return nil
}

// Subscribe subscribes to a session's events
func (c *WebSocketClient) Subscribe(sessionID int) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	if !c.authenticated {
		return fmt.Errorf("not authenticated")
	}

	msg := WSMessage{
		Type:      "subscribe",
		SessionID: sessionID,
	}

	if err := c.sendMessage(msg); err != nil {
		return fmt.Errorf("failed to subscribe: %w", err)
	}

	c.subscribedSession = sessionID
	c.log.Infof("Subscribed to session %d", sessionID)
	return nil
}

// Unsubscribe unsubscribes from a session's events
func (c *WebSocketClient) Unsubscribe(sessionID int) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	msg := WSMessage{
		Type:      "unsubscribe",
		SessionID: sessionID,
	}

	if err := c.sendMessage(msg); err != nil {
		return fmt.Errorf("failed to unsubscribe: %w", err)
	}

	if c.subscribedSession == sessionID {
		c.subscribedSession = 0
	}

	c.log.Infof("Unsubscribed from session %d", sessionID)
	return nil
}

// listen handles incoming WebSocket messages
func (c *WebSocketClient) listen() {
	defer c.handleDisconnect()

	// Start heartbeat
	go c.startHeartbeat()

	for {
		select {
		case <-c.stopChan:
			return
		default:
			_, message, err := c.conn.ReadMessage()
			if err != nil {
				c.log.Errorf("Error reading message: %v", err)
				return
			}

			c.handleMessage(message)
		}
	}
}

// handleMessage processes incoming messages
func (c *WebSocketClient) handleMessage(data []byte) {
	var msg WSMessage
	if err := json.Unmarshal(data, &msg); err != nil {
		c.log.Errorf("Failed to parse message: %v", err)
		return
	}

	switch msg.Type {
	case "auth_success":
		c.mu.Lock()
		c.authenticated = true
		c.mu.Unlock()
		c.log.Info("Authentication successful")
		// session.started events are automatically broadcast to all authenticated clients
		// No need to subscribe - just wait for session.started events

	case "auth_error":
		c.log.Errorf("Authentication failed: %s", msg.Message)
		c.authenticated = false

	case "subscribed":
		c.log.Infof("Subscription confirmed: %s", msg.Message)

	case "unsubscribed":
		c.log.Infof("Unsubscription confirmed: %s", msg.Message)

	case "session.started":
		c.handleSessionStarted(msg.Data)

	case "game.added":
		c.handleGameAdded(msg.Data)

	case "session.ended":
		c.handleSessionEnded(msg.Data)

	case "pong":
		c.log.Debug("Heartbeat pong received")

	case "error":
		c.log.Errorf("Server error: %s", msg.Message)

	default:
		c.log.Debugf("Unknown message type: %s", msg.Type)
	}
}

// handleSessionStarted processes session.started events
func (c *WebSocketClient) handleSessionStarted(data json.RawMessage) {
	var sessionData SessionStartedData
	if err := json.Unmarshal(data, &sessionData); err != nil {
		c.log.Errorf("Failed to parse session.started data: %v", err)
		return
	}

	sessionID := sessionData.Session.ID
	c.log.Infof("Session started: ID=%d", sessionID)

	// Subscribe to the new session
	if err := c.Subscribe(sessionID); err != nil {
		c.log.Errorf("Failed to subscribe to new session %d: %v", sessionID, err)
		return
	}

	// Set the active session on the client for vote tracking
	if c.apiClient != nil {
		c.apiClient.SetActiveSession(sessionID)
	}

	// Announce the new session
	message := fmt.Sprintf("🎮 Game Night is starting! Session #%d", sessionID)
	if c.messageCallback != nil {
		c.messageCallback(message)
	}
}

// handleGameAdded processes game.added events
func (c *WebSocketClient) handleGameAdded(data json.RawMessage) {
	var gameData GameAddedData
	if err := json.Unmarshal(data, &gameData); err != nil {
		c.log.Errorf("Failed to parse game.added data: %v", err)
		return
	}

	c.log.Infof("Game added: %s from %s (Room Code: %s)", gameData.Game.Title, gameData.Game.PackName, gameData.Game.RoomCode)

	// Get and clear the last vote response for the previous game
	var message string
	if c.apiClient != nil {
		lastVote := c.apiClient.GetAndClearLastVoteResponse()
		if lastVote != nil {
			// Include vote results from the previous game
			message = fmt.Sprintf("🗳️ Final votes for %s: %d👍 %d👎 (Score: %d)\n🎮 Coming up next: %s",
				lastVote.Game.Title,
				lastVote.Game.Upvotes, lastVote.Game.Downvotes, lastVote.Game.PopularityScore,
				gameData.Game.Title)
		} else {
			// No votes for previous game (or first game)
			message = fmt.Sprintf("🎮 Coming up next: %s", gameData.Game.Title)
		}
	} else {
		// Fallback if no API client
		message = fmt.Sprintf("🎮 Coming up next: %s", gameData.Game.Title)
	}

	// Handle room code display based on configuration
	if gameData.Game.RoomCode != "" {
		if c.enableRoomCodeImage {
			// Try to upload room code image (for Kosmi) - image contains all info
			c.broadcastWithRoomCodeImage(message, gameData.Game.Title, gameData.Game.RoomCode)
		} else {
			// Use IRC text formatting (fallback)
			roomCodeText := fmt.Sprintf(" - Room Code \x02\x11%s\x0F", gameData.Game.RoomCode)
			if c.messageCallback != nil {
				c.messageCallback(message + roomCodeText)
			}
		}
	} else {
		// No room code, just send the message
		if c.messageCallback != nil {
			c.messageCallback(message)
		}
	}
}

// broadcastWithRoomCodeImage generates, uploads, and broadcasts a room code image
// The image contains all the information (game title, room code, etc.)
// The message parameter should contain the full game announcement including any vote results
func (c *WebSocketClient) broadcastWithRoomCodeImage(message, gameTitle, roomCode string) {
	c.log.Infof("🎨 Starting room code image generation and upload for: %s - %s", gameTitle, roomCode)

	// Generate room code image (animated GIF) with game title embedded
	c.log.Infof("📝 Step 1: Generating image...")
	imageData, err := GenerateRoomCodeImage(roomCode, gameTitle)
	if err != nil {
		c.log.Errorf("❌ Failed to generate room code image: %v", err)
		// Fallback to plain text (no IRC formatting codes for Kosmi)
		fallbackMessage := fmt.Sprintf("%s - Room Code %s", message, roomCode)
		if c.messageCallback != nil {
			c.messageCallback(fallbackMessage)
		}
		return
	}

	c.log.Infof("✅ Step 1 complete: Generated %d bytes of GIF data", len(imageData))

	// Upload animated GIF to Kosmi CDN (MUST complete before announcing)
	c.log.Infof("📤 Step 2: Uploading to Kosmi CDN...")
	filename := fmt.Sprintf("roomcode_%s.gif", roomCode)
	imageURL, err := UploadImageToKosmi(imageData, filename)
	if err != nil {
		c.log.Errorf("❌ Failed to upload room code image: %v", err)
		// Fallback to plain text (no IRC formatting codes for Kosmi)
		fallbackMessage := fmt.Sprintf("%s - Room Code %s", message, roomCode)
		if c.messageCallback != nil {
			c.messageCallback(fallbackMessage)
		}
		return
	}

	c.log.Infof("✅ Step 2 complete: Uploaded to %s", imageURL)

	// Now that upload succeeded, send the full announcement with the message and URL
	if c.roomCodeImageDelay > 0 {
		c.log.Infof("⏳ Step 3: Waiting %v before broadcasting game announcement...", c.roomCodeImageDelay)
		time.Sleep(c.roomCodeImageDelay)
	}
	c.log.Infof("📢 Step 3: Broadcasting game announcement with URL...")
	fullMessage := fmt.Sprintf("%s %s", message, imageURL)
	if c.messageCallback != nil {
		c.messageCallback(fullMessage)
		c.log.Infof("✅ Step 3 complete: Game announcement sent with URL")
	} else {
		c.log.Error("❌ Step 3 failed: messageCallback is nil")
	}

	// Send the plaintext room code after configured delay (to sync with animation completion)
	// Capture callback and logger in closure
	callback := c.messageCallback
	logger := c.log
	plainRoomCode := roomCode // Capture room code for plain text message
	plaintextDelay := c.roomCodePlaintextDelay

	c.log.Infof("⏰ Step 4: Starting %v timer goroutine for plaintext room code...", plaintextDelay)
	go func() {
		logger.Infof("⏰ [Goroutine started] Waiting %v before sending plaintext room code: %s", plaintextDelay, plainRoomCode)
		time.Sleep(plaintextDelay)
		logger.Infof("⏰ [%v elapsed] Now sending plaintext room code...", plaintextDelay)

		if callback != nil {
			// Send just the room code in plaintext (for easy copy/paste)
			plaintextMessage := fmt.Sprintf("Room Code: %s", plainRoomCode)
			logger.Infof("📤 Sending plaintext: %s", plaintextMessage)
			callback(plaintextMessage)
			logger.Infof("✅ Successfully sent plaintext room code")
		} else {
			logger.Error("❌ Message callback is nil when trying to send delayed room code")
		}
	}()

	c.log.Infof("✅ Step 4 complete: Goroutine launched, will fire in %v", plaintextDelay)
}

// handleSessionEnded processes session.ended events
func (c *WebSocketClient) handleSessionEnded(data json.RawMessage) {
	c.log.Info("Session ended event received")
	c.AnnounceSessionEnd()
}

// AnnounceSessionEnd announces the final votes and says goodnight
func (c *WebSocketClient) AnnounceSessionEnd() {
	// Get and clear the last vote response for the final game
	var message string
	if c.apiClient != nil {
		lastVote := c.apiClient.GetAndClearLastVoteResponse()
		if lastVote != nil {
			// Include final vote results
			message = fmt.Sprintf("🗳️ Final votes for %s: %d👍 %d👎 (Score: %d)\n🌙 Game Night has ended! Thanks for playing!",
				lastVote.Game.Title,
				lastVote.Game.Upvotes, lastVote.Game.Downvotes, lastVote.Game.PopularityScore)
		} else {
			// No votes for final game
			message = "🌙 Game Night has ended! Thanks for playing!"
		}

		// Clear the active session
		c.apiClient.SetActiveSession(0)
	} else {
		message = "🌙 Game Night has ended! Thanks for playing!"
	}

	// Broadcast to chats via callback
	if c.messageCallback != nil {
		c.messageCallback(message)
	}
}

// startHeartbeat sends ping messages periodically
func (c *WebSocketClient) startHeartbeat() {
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-c.stopChan:
			return
		case <-ticker.C:
			c.mu.Lock()
			if c.connected && c.conn != nil {
				msg := WSMessage{Type: "ping"}
				if err := c.sendMessage(msg); err != nil {
					c.log.Errorf("Failed to send ping: %v", err)
				}
			}
			c.mu.Unlock()
		}
	}
}

// sendMessage sends a message to the WebSocket server
func (c *WebSocketClient) sendMessage(msg WSMessage) error {
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}

	if c.conn == nil {
		return fmt.Errorf("not connected")
	}

	return c.conn.WriteMessage(websocket.TextMessage, data)
}

// handleDisconnect handles connection loss and attempts reconnection
func (c *WebSocketClient) handleDisconnect() {
	c.mu.Lock()
	c.connected = false
	c.authenticated = false
	if c.conn != nil {
		c.conn.Close()
		c.conn = nil
	}
	c.mu.Unlock()

	c.log.Warn("WebSocket disconnected, attempting to reconnect...")

	// Exponential backoff reconnection
	delay := c.reconnectDelay
	for {
		select {
		case <-c.stopChan:
			return
		case <-time.After(delay):
			c.log.Infof("Reconnecting... (delay: %v)", delay)

			if err := c.Connect(); err != nil {
				c.log.Errorf("Reconnection failed: %v", err)

				// Increase delay with exponential backoff
				delay *= 2
				if delay > c.maxReconnect {
					delay = c.maxReconnect
				}
				continue
			}

			// Reconnected successfully
			c.log.Info("Reconnected successfully")

			// Re-subscribe if we were subscribed before
			if c.subscribedSession > 0 {
				if err := c.Subscribe(c.subscribedSession); err != nil {
					c.log.Errorf("Failed to re-subscribe: %v", err)
				}
			}

			return
		}
	}
}

// Close closes the WebSocket connection
func (c *WebSocketClient) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	close(c.stopChan)

	if c.conn != nil {
		c.log.Info("Closing WebSocket connection")
		err := c.conn.Close()
		c.conn = nil
		c.connected = false
		c.authenticated = false
		return err
	}

	return nil
}

// IsConnected returns whether the client is connected
func (c *WebSocketClient) IsConnected() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.connected && c.authenticated
}

// IsSubscribed returns whether the client is subscribed to a session
func (c *WebSocketClient) IsSubscribed() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.subscribedSession > 0
}
