package jackbox

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"

	"github.com/sirupsen/logrus"
)

// Client handles communication with the Jackbox Game Picker API
type Client struct {
	apiURL          string
	adminPassword   string
	token           string
	tokenExpiry     time.Time
	mu              sync.RWMutex
	log             *logrus.Entry
	httpClient      *http.Client
	messageCallback func(string)

	// Vote tracking
	activeSessionID   int
	lastVoteResponse  *VoteResponse
	voteDebounceTimer *time.Timer
	voteDebounceDelay time.Duration
}

// AuthResponse represents the authentication response from the API
type AuthResponse struct {
	Token string `json:"token"`
}

// VoteRequest represents a vote submission to the API
type VoteRequest struct {
	Username  string `json:"username"`
	Vote      string `json:"vote"` // "up" or "down"
	Timestamp string `json:"timestamp"`
}

// VoteResponse represents the API response to a vote submission
type VoteResponse struct {
	Success bool   `json:"success"`
	Message string `json:"message"`
	Game    struct {
		Title           string `json:"title"`
		Upvotes         int    `json:"upvotes"`
		Downvotes       int    `json:"downvotes"`
		PopularityScore int    `json:"popularity_score"`
	} `json:"game"`
}

// Session represents an active Jackbox session
type Session struct {
	ID          int    `json:"id"`
	IsActive    int    `json:"is_active"` // API returns 1 for active, 0 for inactive
	GamesPlayed int    `json:"games_played"`
	CreatedAt   string `json:"created_at"`
}

// SessionResponse represents the API response for session queries
type SessionResponse struct {
	Session *Session `json:"session"`
}

// NewClient creates a new Jackbox API client
func NewClient(apiURL, adminPassword string, log *logrus.Entry) *Client {
	return &Client{
		apiURL:        apiURL,
		adminPassword: adminPassword,
		log:           log,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
		voteDebounceDelay: 3 * time.Second, // Wait 3 seconds after last vote before broadcasting
	}
}

// SetMessageCallback sets the callback function for broadcasting messages
func (c *Client) SetMessageCallback(callback func(string)) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.messageCallback = callback
}

// SetActiveSession sets the active session ID for vote tracking
func (c *Client) SetActiveSession(sessionID int) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.activeSessionID = sessionID
	c.log.Infof("Active session set to %d", sessionID)
}

// GetAndClearLastVoteResponse returns the last vote response and clears it
func (c *Client) GetAndClearLastVoteResponse() *VoteResponse {
	c.mu.Lock()
	defer c.mu.Unlock()

	resp := c.lastVoteResponse
	c.lastVoteResponse = nil

	// Stop any pending debounce timer
	if c.voteDebounceTimer != nil {
		c.voteDebounceTimer.Stop()
		c.voteDebounceTimer = nil
	}

	return resp
}

// broadcastMessage sends a message via the callback if set
func (c *Client) broadcastMessage(message string) {
	c.mu.RLock()
	callback := c.messageCallback
	c.mu.RUnlock()

	if callback != nil {
		callback(message)
	}
}

// Authenticate obtains a JWT token from the API using admin password
func (c *Client) Authenticate() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.log.Debug("Authenticating with Jackbox API...")

	// Prepare authentication request
	authReq := map[string]string{
		"key": c.adminPassword,
	}

	jsonBody, err := json.Marshal(authReq)
	if err != nil {
		return fmt.Errorf("failed to marshal auth request: %w", err)
	}

	// Send authentication request
	req, err := http.NewRequest("POST", c.apiURL+"/api/auth/login", bytes.NewReader(jsonBody))
	if err != nil {
		return fmt.Errorf("failed to create auth request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to send auth request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("authentication failed with status %d: %s", resp.StatusCode, string(body))
	}

	// Parse response
	var authResp AuthResponse
	if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
		return fmt.Errorf("failed to parse auth response: %w", err)
	}

	c.token = authResp.Token
	// Assume token is valid for 24 hours (adjust based on actual API behavior)
	c.tokenExpiry = time.Now().Add(24 * time.Hour)

	c.log.Info("Successfully authenticated with Jackbox API")
	return nil
}

// ensureAuthenticated checks if we have a valid token and authenticates if needed
func (c *Client) ensureAuthenticated() error {
	c.mu.RLock()
	hasValidToken := c.token != "" && time.Now().Before(c.tokenExpiry)
	c.mu.RUnlock()

	if hasValidToken {
		return nil
	}

	return c.Authenticate()
}

// SendVote sends a vote to the Jackbox API
func (c *Client) SendVote(username, voteType string, timestamp time.Time) error {
	// Ensure we're authenticated
	if err := c.ensureAuthenticated(); err != nil {
		return fmt.Errorf("authentication failed: %w", err)
	}

	c.mu.RLock()
	token := c.token
	c.mu.RUnlock()

	// Prepare vote request
	voteReq := VoteRequest{
		Username:  username,
		Vote:      voteType,
		Timestamp: timestamp.Format(time.RFC3339),
	}

	jsonBody, err := json.Marshal(voteReq)
	if err != nil {
		return fmt.Errorf("failed to marshal vote request: %w", err)
	}

	// Send vote request
	req, err := http.NewRequest("POST", c.apiURL+"/api/votes/live", bytes.NewReader(jsonBody))
	if err != nil {
		return fmt.Errorf("failed to create vote request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to send vote request: %w", err)
	}
	defer resp.Body.Close()

	// Read response body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read vote response: %w", err)
	}

	// Check response status
	if resp.StatusCode == http.StatusUnauthorized {
		// Token expired, try to re-authenticate
		c.log.Warn("Token expired, re-authenticating...")
		if err := c.Authenticate(); err != nil {
			return fmt.Errorf("re-authentication failed: %w", err)
		}
		// Retry the vote
		return c.SendVote(username, voteType, timestamp)
	}

	if resp.StatusCode == http.StatusConflict {
		// Duplicate vote - this is expected, just log it
		c.log.Debugf("Duplicate vote from %s (within 1 second)", username)
		return nil
	}

	if resp.StatusCode == http.StatusNotFound {
		// No active session or timestamp doesn't match any game
		c.log.Debug("Vote rejected: no active session or timestamp doesn't match any game")
		return nil
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("vote failed with status %d: %s", resp.StatusCode, string(body))
	}

	// Parse successful response
	var voteResp VoteResponse
	if err := json.Unmarshal(body, &voteResp); err != nil {
		c.log.Warnf("Failed to parse vote response: %v", err)
		return nil // Don't fail if we can't parse the response
	}

	c.log.Debugf("Vote recorded for %s: %s - %d👍 %d👎",
		voteResp.Game.Title, username, voteResp.Game.Upvotes, voteResp.Game.Downvotes)

	// Debounce vote broadcasts - wait for activity to settle
	c.debouncedVoteBroadcast(&voteResp)

	return nil
}

// GetToken returns the current JWT token
func (c *Client) GetToken() string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.token
}

// GetActiveSession retrieves the currently active session from the API
func (c *Client) GetActiveSession() (*Session, error) {
	// Ensure we're authenticated
	if err := c.ensureAuthenticated(); err != nil {
		return nil, fmt.Errorf("authentication failed: %w", err)
	}

	c.mu.RLock()
	token := c.token
	c.mu.RUnlock()

	// Create request to get active session
	req, err := http.NewRequest("GET", c.apiURL+"/api/sessions/active", nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	// Read body for debugging
	body, readErr := io.ReadAll(resp.Body)
	if readErr != nil {
		return nil, fmt.Errorf("failed to read response body: %w", readErr)
	}

	c.log.Infof("GetActiveSession: GET %s/api/sessions/active returned %d", c.apiURL, resp.StatusCode)
	c.log.Infof("GetActiveSession response body: %s", string(body))

	// Handle 404 - no active session
	if resp.StatusCode == http.StatusNotFound {
		c.log.Info("API returned 404 - endpoint may not exist or no active session")
		return nil, nil
	}

	// Handle 401 - token expired
	if resp.StatusCode == http.StatusUnauthorized {
		c.log.Warn("Token expired, re-authenticating...")
		if err := c.Authenticate(); err != nil {
			return nil, fmt.Errorf("re-authentication failed: %w", err)
		}
		// Retry the request
		return c.GetActiveSession()
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
	}

	// Try to parse as direct session object first
	var session Session
	if err := json.Unmarshal(body, &session); err != nil {
		c.log.Errorf("Failed to parse session response: %v, body: %s", err, string(body))
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	// Check if we got a valid session (ID > 0 means it's valid)
	if session.ID == 0 {
		c.log.Info("No active session (ID is 0)")
		return nil, nil
	}

	c.log.Infof("Parsed session: ID=%d, IsActive=%v, GamesPlayed=%d", session.ID, session.IsActive, session.GamesPlayed)
	return &session, nil
}

// debouncedVoteBroadcast implements debouncing for vote broadcasts
// When there's an active session, it stores votes to be announced with the next game
// When there's no active session, it uses time-based debouncing (3 seconds)
func (c *Client) debouncedVoteBroadcast(voteResp *VoteResponse) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Store the latest vote response
	c.lastVoteResponse = voteResp

	// If there's an active session, just accumulate votes silently
	// They'll be announced when the next game is picked
	if c.activeSessionID > 0 {
		c.log.Debugf("Vote accumulated for %s (session active, will announce with next game)", voteResp.Game.Title)
		// Cancel any existing timer since we're in session mode
		if c.voteDebounceTimer != nil {
			c.voteDebounceTimer.Stop()
			c.voteDebounceTimer = nil
		}
		return
	}

	// No active session - use time-based debouncing
	// If there's an existing timer, stop it
	if c.voteDebounceTimer != nil {
		c.voteDebounceTimer.Stop()
	}

	// Create a new timer that will fire after the debounce delay
	c.voteDebounceTimer = time.AfterFunc(c.voteDebounceDelay, func() {
		c.mu.Lock()
		lastResp := c.lastVoteResponse
		c.lastVoteResponse = nil
		c.mu.Unlock()

		if lastResp != nil {
			// Broadcast the final vote result
			message := fmt.Sprintf("🗳️ Voting complete for %s • %d👍 %d👎 (Score: %d)",
				lastResp.Game.Title,
				lastResp.Game.Upvotes, lastResp.Game.Downvotes, lastResp.Game.PopularityScore)
			c.broadcastMessage(message)
			c.log.Infof("Broadcast final vote result: %s - %d👍 %d👎",
				lastResp.Game.Title, lastResp.Game.Upvotes, lastResp.Game.Downvotes)
		}
	})
}
