// Package quota monitors Claude Code sessions for quota exhaustion and triggers
// account switches when thresholds are crossed.
package quota

import (
	"context"
	"log"
	"regexp"
	"strings"
	"time"

	"forge.secuaas.ovh/olivier/claude-failover/internal/config"
	"forge.secuaas.ovh/olivier/claude-failover/internal/state"
	"forge.secuaas.ovh/olivier/claude-failover/internal/tmux"
)

// SwitchRequest is emitted when quota exhaustion is detected, requesting
// the AccountSwitcher to activate a different account.
type SwitchRequest struct {
	From      string // current active account name
	To        string // desired account name (empty = auto-select)
	ResetTime string // human-readable reset time extracted from the pane
}

// quotaPatterns are substrings that indicate quota exhaustion in a pane.
var quotaPatterns = []string{
	"you've hit your limit",
	"rate limit",
	"quota exceeded",
	"usage limit reached",
	"claude pro usage",
	"too many requests",
}

// resetTimeRe extracts reset times like "resets 8pm", "resets at 11:30pm",
// "resets in 45 minutes".
var resetTimeRe = regexp.MustCompile(
	`(?i)resets?\s+(?:at\s+)?([0-9]+(?::[0-9]+)?\s*[ap]m|in\s+[0-9]+\s+(?:minute|hour)s?)`,
)

// Monitor polls tmux panes for quota exhaustion messages.
type Monitor struct {
	tmux     tmux.Client
	state    *state.State
	config   *config.Config
	switchCh chan SwitchRequest
	interval time.Duration
	logger   *log.Logger
}

// New creates a Monitor with defaults from cfg.
func New(tc tmux.Client, s *state.State, cfg *config.Config) *Monitor {
	interval := cfg.Quota.PollInterval.Duration
	if interval == 0 {
		interval = 30 * time.Second
	}
	return &Monitor{
		tmux:     tc,
		state:    s,
		config:   cfg,
		switchCh: make(chan SwitchRequest, 1),
		interval: interval,
		logger:   log.Default(),
	}
}

// SwitchChan returns the channel on which SwitchRequests are sent.
func (m *Monitor) SwitchChan() <-chan SwitchRequest {
	return m.switchCh
}

// Run starts the quota monitor loop until ctx is cancelled.
func (m *Monitor) Run(ctx context.Context) {
	ticker := time.NewTicker(m.interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			m.poll()
		}
	}
}

// poll checks all sessions for quota exhaustion once.
func (m *Monitor) poll() {
	if m.state.ActiveAccount() != "" && m.isQuotaPaused() {
		return
	}

	blockedPool := 0
	blockedInteractive := 0
	var resetTime string

	prefix := m.config.Pool.Autonomous.Prefix
	if prefix == "" {
		prefix = "ccl-auto-"
	}
	for i := 0; i < m.config.Pool.Autonomous.Max; i++ {
		name := sessionName(prefix, i)
		if !m.tmux.HasSession(name) {
			continue
		}
		// Only capture 3 lines — avoids false positives on stale history.
		tail, err := m.tmux.CapturePaneTail(name, 3)
		if err != nil {
			continue
		}
		if isQuotaExhausted(tail) {
			blockedPool++
			if rt := extractResetTime(tail); rt != "" {
				resetTime = rt
			}
		}
	}

	for _, ds := range m.config.Pool.Dedicated {
		if !m.tmux.HasSession(ds.Name) {
			continue
		}
		tail, err := m.tmux.CapturePaneTail(ds.Name, 3)
		if err != nil {
			continue
		}
		if isQuotaExhausted(tail) {
			blockedInteractive++
			if rt := extractResetTime(tail); rt != "" {
				resetTime = rt
			}
		}
	}

	if blockedPool >= 2 || blockedInteractive >= 1 {
		req := SwitchRequest{
			From:      m.state.ActiveAccount(),
			ResetTime: resetTime,
		}
		select {
		case m.switchCh <- req:
			m.logger.Printf("[quota] SwapRequested: from=%s pool=%d interactive=%d reset=%q",
				req.From, blockedPool, blockedInteractive, resetTime)
		default:
			// Swap already pending — do not queue another.
		}
	}
}

// isQuotaPaused checks whether a swap is already in progress.
func (m *Monitor) isQuotaPaused() bool {
	// Lightweight proxy: if switch channel is full, a swap is pending.
	return len(m.switchCh) > 0
}

// isQuotaExhausted returns true if the pane content indicates quota exhaustion.
func isQuotaExhausted(paneContent string) bool {
	lower := strings.ToLower(paneContent)
	for _, p := range quotaPatterns {
		if strings.Contains(lower, p) {
			return true
		}
	}
	return false
}

// extractResetTime parses a reset time string from pane content.
// Returns "" if none found.
func extractResetTime(content string) string {
	m := resetTimeRe.FindStringSubmatch(content)
	if len(m) >= 2 {
		return strings.TrimSpace(m[1])
	}
	return ""
}

func sessionName(prefix string, i int) string {
	return prefix + itoa(i)
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	b := make([]byte, 0, 10)
	for n > 0 {
		b = append([]byte{byte('0' + n%10)}, b...)
		n /= 10
	}
	return string(b)
}
