package discord

import (
	"net"
	"net/http"
	"strconv"
	"time"

	"github.com/pkg/errors"
)

// RateLimitState tracks rate limit information for a specific bucket
type RateLimitState struct {
	Remaining int       // Requests remaining in current window
	Limit     int       // Total requests allowed in window
	Reset     time.Time // When the rate limit resets
	Bucket    string    // Discord's bucket identifier
}

// Do executes an HTTP request with automatic rate limit handling
// It will wait if rate limits are about to be exceeded and retry once if a 429 is received
func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
	if req == nil {
		return nil, errors.New("request cannot be nil")
	}

	// Step 1: Check if we need to wait before making request
	bucket := c.getBucketFromRequest(req)
	if err := c.waitIfNeeded(bucket); err != nil {
		return nil, err
	}

	// Step 2: Execute request
	resp, err := c.client.Do(req)
	if err != nil {
		// Check if it's a network timeout
		if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
			return nil, errors.Wrap(err, "request timed out")
		}
		return nil, errors.Wrap(err, "http request failed")
	}

	// Step 3: Update rate limit state from response headers
	c.updateRateLimit(resp.Header)

	// Step 4: Handle 429 (rate limited)
	if resp.StatusCode == http.StatusTooManyRequests {
		resp.Body.Close() // Close original response

		retryAfter := c.parseRetryAfter(resp.Header)

		// No Retry-After header, can't retry safely
		if retryAfter == 0 {
			c.logger.Warn().
				Str("bucket", bucket).
				Str("method", req.Method).
				Str("path", req.URL.Path).
				Msg("Rate limited but no Retry-After header provided")
			return nil, errors.New("discord API rate limited but no Retry-After header provided")
		}

		// Retry-After exceeds 30 second cap
		if retryAfter > 30*time.Second {
			c.logger.Warn().
				Str("bucket", bucket).
				Str("method", req.Method).
				Str("path", req.URL.Path).
				Dur("retry_after", retryAfter).
				Msg("Rate limited with Retry-After exceeding 30s cap, not retrying")
			return nil, errors.Errorf(
				"discord API rate limited (retry after %s exceeds 30s cap)",
				retryAfter,
			)
		}

		// Wait and retry
		c.logger.Warn().
			Str("bucket", bucket).
			Str("method", req.Method).
			Str("path", req.URL.Path).
			Dur("retry_after", retryAfter).
			Msg("Rate limited, waiting before retry")

		time.Sleep(retryAfter)

		// Retry the request
		resp, err = c.client.Do(req)
		if err != nil {
			if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
				return nil, errors.Wrap(err, "retry request timed out")
			}
			return nil, errors.Wrap(err, "retry request failed")
		}

		// Update rate limit again after retry
		c.updateRateLimit(resp.Header)

		// If STILL rate limited after retry, return error
		if resp.StatusCode == http.StatusTooManyRequests {
			resp.Body.Close()
			c.logger.Error().
				Str("bucket", bucket).
				Str("method", req.Method).
				Str("path", req.URL.Path).
				Msg("Still rate limited after retry, Discord may be experiencing issues")
			return nil, errors.Errorf(
				"discord API still rate limited after retry (waited %s), Discord may be experiencing issues",
				retryAfter,
			)
		}
	}

	return resp, nil
}

// getBucketFromRequest extracts or generates bucket ID from request
// For Discord, the bucket is typically METHOD:path until we get the actual bucket from headers
func (c *APIClient) getBucketFromRequest(req *http.Request) string {
	return req.Method + ":" + req.URL.Path
}

// waitIfNeeded checks if we need to delay before request to avoid hitting rate limits
func (c *APIClient) waitIfNeeded(bucket string) error {
	c.mu.RLock()
	state, exists := c.buckets[bucket]
	c.mu.RUnlock()

	if !exists {
		return nil // No state yet, proceed
	}

	now := time.Now()

	// If we have no remaining requests and reset hasn't occurred, wait
	if state.Remaining == 0 && now.Before(state.Reset) {
		waitDuration := time.Until(state.Reset)
		// Add small buffer (100ms) to ensure reset has occurred
		waitDuration += 100 * time.Millisecond

		if waitDuration > 0 {
			c.logger.Debug().
				Str("bucket", bucket).
				Dur("wait_duration", waitDuration).
				Msg("Proactively waiting for rate limit reset")
			time.Sleep(waitDuration)
		}
	}

	return nil
}

// updateRateLimit parses response headers and updates bucket state
func (c *APIClient) updateRateLimit(headers http.Header) {
	bucket := headers.Get("X-RateLimit-Bucket")
	if bucket == "" {
		return // No bucket info, can't track
	}

	// Parse headers
	limit := c.parseInt(headers.Get("X-RateLimit-Limit"))
	remaining := c.parseInt(headers.Get("X-RateLimit-Remaining"))
	resetAfter := c.parseFloat(headers.Get("X-RateLimit-Reset-After"))

	state := &RateLimitState{
		Bucket:    bucket,
		Limit:     limit,
		Remaining: remaining,
		Reset:     time.Now().Add(time.Duration(resetAfter * float64(time.Second))),
	}

	c.mu.Lock()
	c.buckets[bucket] = state
	c.mu.Unlock()

	// Log rate limit state for debugging
	c.logger.Debug().
		Str("bucket", bucket).
		Int("remaining", remaining).
		Int("limit", limit).
		Dur("reset_in", time.Until(state.Reset)).
		Msg("Rate limit state updated")
}

// parseRetryAfter extracts retry delay from Retry-After header
func (c *APIClient) parseRetryAfter(headers http.Header) time.Duration {
	retryAfter := headers.Get("Retry-After")
	if retryAfter == "" {
		return 0
	}

	// Discord returns seconds as float
	seconds := c.parseFloat(retryAfter)
	if seconds <= 0 {
		return 0
	}

	return time.Duration(seconds * float64(time.Second))
}

// parseInt parses an integer from a header value, returns 0 on error
func (c *APIClient) parseInt(s string) int {
	if s == "" {
		return 0
	}
	i, _ := strconv.Atoi(s)
	return i
}

// parseFloat parses a float from a header value, returns 0 on error
func (c *APIClient) parseFloat(s string) float64 {
	if s == "" {
		return 0
	}
	f, _ := strconv.ParseFloat(s, 64)
	return f
}
