package backbeat

import (
	"context"
	"fmt"
	"math"
	"sync"
	"time"

	"github.com/rs/zerolog"
)

// DegradationManager implements BACKBEAT-REQ-003 (Degrade Local)
// Manages local tempo derivation when leader is lost and reconciliation
type DegradationManager struct {
	mu     sync.RWMutex
	logger zerolog.Logger

	// State tracking
	inDegradationMode bool
	leaderLostAt      time.Time
	lastLeaderSync    time.Time
	localTempo        int
	originalTempo     int

	// Timing state for BACKBEAT-PER-003 compliance
	referenceTime     time.Time
	referenceBeat     int64
	expectedBeatTime  time.Time
	actualBeatTime    time.Time
	driftAccumulation time.Duration

	// Configuration
	maxDriftPercent   float64 // BACKBEAT-PER-003: 1% max drift
	syncTimeout       time.Duration
	degradationWindow time.Duration

	// Metrics
	metrics *Metrics
}

// DegradationConfig configures the degradation manager
type DegradationConfig struct {
	Logger            zerolog.Logger
	Metrics           *Metrics
	MaxDriftPercent   float64       // Default: 0.01 (1%)
	SyncTimeout       time.Duration // Default: 30s
	DegradationWindow time.Duration // Default: 5m
}

// NewDegradationManager creates a new degradation manager
func NewDegradationManager(config DegradationConfig) *DegradationManager {
	// Set defaults
	if config.MaxDriftPercent == 0 {
		config.MaxDriftPercent = 0.01 // 1% as per BACKBEAT-PER-003
	}
	if config.SyncTimeout == 0 {
		config.SyncTimeout = 30 * time.Second
	}
	if config.DegradationWindow == 0 {
		config.DegradationWindow = 5 * time.Minute
	}

	return &DegradationManager{
		logger:            config.Logger.With().Str("component", "degradation").Logger(),
		metrics:           config.Metrics,
		maxDriftPercent:   config.MaxDriftPercent,
		syncTimeout:       config.SyncTimeout,
		degradationWindow: config.DegradationWindow,
		referenceTime:     time.Now(),
		lastLeaderSync:    time.Now(),
	}
}

// OnLeaderLost is called when leadership is lost, initiating degradation mode
func (d *DegradationManager) OnLeaderLost(currentTempo int, beatIndex int64) {
	d.mu.Lock()
	defer d.mu.Unlock()

	now := time.Now()
	d.inDegradationMode = true
	d.leaderLostAt = now
	d.localTempo = currentTempo
	d.originalTempo = currentTempo
	d.referenceTime = now
	d.referenceBeat = beatIndex
	d.driftAccumulation = 0

	d.logger.Warn().
		Int("tempo_bpm", currentTempo).
		Int64("beat_index", beatIndex).
		Msg("entered degradation mode - deriving local tempo")

	if d.metrics != nil {
		d.metrics.UpdateDegradationMode(true)
	}
}

// OnLeaderRecovered is called when leadership is restored
func (d *DegradationManager) OnLeaderRecovered(leaderTempo int, leaderBeatIndex int64, hlc string) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	if !d.inDegradationMode {
		return nil // Already recovered
	}

	now := time.Now()
	degradationDuration := now.Sub(d.leaderLostAt)

	d.logger.Info().
		Dur("degradation_duration", degradationDuration).
		Int("local_tempo", d.localTempo).
		Int("leader_tempo", leaderTempo).
		Int64("local_beat", d.referenceBeat).
		Int64("leader_beat", leaderBeatIndex).
		Str("leader_hlc", hlc).
		Msg("reconciling with leader after degradation")

	// Calculate drift during degradation period
	drift := d.calculateDrift(now)

	// Reset degradation state
	d.inDegradationMode = false
	d.lastLeaderSync = now
	d.referenceTime = now
	d.referenceBeat = leaderBeatIndex
	d.driftAccumulation = 0

	d.logger.Info().
		Float64("drift_percent", drift*100).
		Msg("recovered from degradation mode")

	if d.metrics != nil {
		d.metrics.UpdateDegradationMode(false)
		d.metrics.UpdateDriftMetrics(drift, 0) // Reset HLC drift
	}

	return nil
}

// UpdateBeatTiming updates timing information for drift calculation
func (d *DegradationManager) UpdateBeatTiming(expectedTime, actualTime time.Time, beatIndex int64) {
	d.mu.Lock()
	defer d.mu.Unlock()

	d.expectedBeatTime = expectedTime
	d.actualBeatTime = actualTime

	// Accumulate drift if in degradation mode
	if d.inDegradationMode {
		beatDrift := actualTime.Sub(expectedTime)
		d.driftAccumulation += beatDrift.Abs()

		// Update metrics
		if d.metrics != nil {
			drift := d.calculateDrift(actualTime)
			d.metrics.UpdateDriftMetrics(drift, 0)
		}
	}
}

// GetTimerDrift returns the current timer drift ratio for BACKBEAT-PER-003
func (d *DegradationManager) GetTimerDrift() float64 {
	d.mu.RLock()
	defer d.mu.RUnlock()

	if !d.inDegradationMode {
		return 0.0 // No drift when synchronized with leader
	}

	return d.calculateDrift(time.Now())
}

// calculateDrift calculates the current drift ratio (internal method, must be called with lock)
func (d *DegradationManager) calculateDrift(now time.Time) float64 {
	if d.referenceTime.IsZero() {
		return 0.0
	}

	elapsed := now.Sub(d.referenceTime)
	if elapsed <= 0 {
		return 0.0
	}

	// Calculate expected vs actual timing
	expectedDuration := elapsed
	actualDuration := elapsed + d.driftAccumulation

	if expectedDuration <= 0 {
		return 0.0
	}

	drift := float64(actualDuration-expectedDuration) / float64(expectedDuration)
	return math.Abs(drift)
}

// IsInDegradationMode returns true if currently in degradation mode
func (d *DegradationManager) IsInDegradationMode() bool {
	d.mu.RLock()
	defer d.mu.RUnlock()
	return d.inDegradationMode
}

// GetDegradationDuration returns how long we've been in degradation mode
func (d *DegradationManager) GetDegradationDuration() time.Duration {
	d.mu.RLock()
	defer d.mu.RUnlock()

	if !d.inDegradationMode {
		return 0
	}

	return time.Since(d.leaderLostAt)
}

// IsWithinDriftLimits checks if current drift is within BACKBEAT-PER-003 limits
func (d *DegradationManager) IsWithinDriftLimits() bool {
	drift := d.GetTimerDrift()
	return drift <= d.maxDriftPercent
}

// GetLocalTempo returns the current local tempo when in degradation mode
func (d *DegradationManager) GetLocalTempo() int {
	d.mu.RLock()
	defer d.mu.RUnlock()

	if !d.inDegradationMode {
		return 0 // Not applicable when not in degradation
	}

	return d.localTempo
}

// AdjustLocalTempo allows fine-tuning local tempo to minimize drift
func (d *DegradationManager) AdjustLocalTempo(newTempo int) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	if !d.inDegradationMode {
		return fmt.Errorf("cannot adjust local tempo when not in degradation mode")
	}

	// Validate tempo adjustment (max 5% change from original)
	maxChange := float64(d.originalTempo) * 0.05
	change := math.Abs(float64(newTempo - d.originalTempo))

	if change > maxChange {
		return fmt.Errorf("tempo adjustment too large: %.1f BPM (max %.1f BPM)",
			change, maxChange)
	}

	oldTempo := d.localTempo
	d.localTempo = newTempo

	d.logger.Info().
		Int("old_tempo", oldTempo).
		Int("new_tempo", newTempo).
		Float64("drift_percent", d.calculateDrift(time.Now())*100).
		Msg("adjusted local tempo to minimize drift")

	return nil
}

// GetState returns the current degradation manager state for debugging
func (d *DegradationManager) GetState() map[string]interface{} {
	d.mu.RLock()
	defer d.mu.RUnlock()

	state := map[string]interface{}{
		"in_degradation_mode":   d.inDegradationMode,
		"local_tempo":           d.localTempo,
		"original_tempo":        d.originalTempo,
		"drift_percent":         d.calculateDrift(time.Now()) * 100,
		"within_limits":         d.IsWithinDriftLimits(),
		"max_drift_percent":     d.maxDriftPercent * 100,
		"reference_time":        d.referenceTime,
		"reference_beat":        d.referenceBeat,
		"drift_accumulation_ms": d.driftAccumulation.Milliseconds(),
	}

	if d.inDegradationMode {
		state["degradation_duration"] = time.Since(d.leaderLostAt)
		state["leader_lost_at"] = d.leaderLostAt
	}

	return state
}

// MonitorDrift runs a background goroutine to monitor drift and alert on violations
func (d *DegradationManager) MonitorDrift(ctx context.Context) {
	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			d.checkDriftLimits()
		}
	}
}

// checkDriftLimits monitors drift and logs warnings when limits are exceeded
func (d *DegradationManager) checkDriftLimits() {
	d.mu.RLock()
	inDegradation := d.inDegradationMode
	drift := d.calculateDrift(time.Now())
	d.mu.RUnlock()

	if !inDegradation {
		return // No drift monitoring when synchronized
	}

	driftPercent := drift * 100

	if drift > d.maxDriftPercent {
		d.logger.Warn().
			Float64("drift_percent", driftPercent).
			Float64("limit_percent", d.maxDriftPercent*100).
			Msg("BACKBEAT-PER-003 violation: timer drift exceeds 1% limit")
	} else if drift > d.maxDriftPercent*0.8 {
		// Warning at 80% of limit
		d.logger.Warn().
			Float64("drift_percent", driftPercent).
			Float64("limit_percent", d.maxDriftPercent*100).
			Msg("approaching drift limit")
	}
}
