package heuristics

import (
	"math"
	"sync"
	"time"

	"github.com/breakpilot/pca-platform/heuristic-service/internal/config"
)

// SessionMetrics holds behavioral metrics for a session
type SessionMetrics struct {
	SessionID        string    `json:"session_id"`
	StartTime        time.Time `json:"start_time"`
	VisibleTime      float64   `json:"visible_time"`      // Seconds visible
	LastVisibleTS    time.Time `json:"last_visible_ts"`   // Last visibility timestamp
	MaxScrollPercent float64   `json:"max_scroll_percent"` // 0-1 scroll depth
	ClickCount       int       `json:"click_count"`
	MouseMoves       int       `json:"mouse_moves"`
	KeyStrokes       int       `json:"key_strokes"`
	TouchEvents      int       `json:"touch_events"`

	// Advanced metrics
	MouseVelocities  []float64 `json:"mouse_velocities,omitempty"`   // For variance calculation
	ScrollVelocities []float64 `json:"scroll_velocities,omitempty"` // Scroll speed patterns
	ClickIntervals   []float64 `json:"click_intervals,omitempty"`   // Time between clicks

	// Computed score
	LastScore        float64   `json:"last_score"`
	LastScoreTime    time.Time `json:"last_score_time"`
}

// Scorer calculates human-likelihood scores based on behavioral heuristics
type Scorer struct {
	config  *config.Config
	mu      sync.RWMutex
	sessions map[string]*SessionMetrics
}

// NewScorer creates a new heuristic scorer
func NewScorer(cfg *config.Config) *Scorer {
	return &Scorer{
		config:   cfg,
		sessions: make(map[string]*SessionMetrics),
	}
}

// GetOrCreateSession retrieves or creates a session
func (s *Scorer) GetOrCreateSession(sessionID string) *SessionMetrics {
	s.mu.Lock()
	defer s.mu.Unlock()

	if session, exists := s.sessions[sessionID]; exists {
		return session
	}

	session := &SessionMetrics{
		SessionID:     sessionID,
		StartTime:     time.Now(),
		LastVisibleTS: time.Now(),
	}
	s.sessions[sessionID] = session
	return session
}

// UpdateMetrics updates session metrics from a tick
func (s *Scorer) UpdateMetrics(sessionID string, metrics *SessionMetrics) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if existing, exists := s.sessions[sessionID]; exists {
		// Merge metrics
		existing.VisibleTime = metrics.VisibleTime
		existing.MaxScrollPercent = metrics.MaxScrollPercent
		existing.ClickCount = metrics.ClickCount
		existing.MouseMoves = metrics.MouseMoves
		existing.KeyStrokes = metrics.KeyStrokes
		existing.TouchEvents = metrics.TouchEvents

		if len(metrics.MouseVelocities) > 0 {
			existing.MouseVelocities = append(existing.MouseVelocities, metrics.MouseVelocities...)
		}
		if len(metrics.ScrollVelocities) > 0 {
			existing.ScrollVelocities = append(existing.ScrollVelocities, metrics.ScrollVelocities...)
		}
		if len(metrics.ClickIntervals) > 0 {
			existing.ClickIntervals = append(existing.ClickIntervals, metrics.ClickIntervals...)
		}
	} else {
		s.sessions[sessionID] = metrics
	}
}

// CalculateScore computes the human-likelihood score for a session
func (s *Scorer) CalculateScore(sessionID string) float64 {
	s.mu.RLock()
	session, exists := s.sessions[sessionID]
	if !exists {
		s.mu.RUnlock()
		return 0.0
	}
	s.mu.RUnlock()

	weights := s.config.Weights

	// Calculate individual heuristic scores (0-1)
	dwellScore := s.calculateDwellScore(session)
	scrollScore := s.calculateScrollScore(session)
	pointerScore := s.calculatePointerScore(session)
	clickScore := s.calculateClickScore(session)

	// Weighted sum
	totalScore := dwellScore*weights.DwellRatio +
		scrollScore*weights.ScrollScore +
		pointerScore*weights.PointerVariance +
		clickScore*weights.ClickRate

	// Clamp to [0, 1]
	if totalScore > 1.0 {
		totalScore = 1.0
	}
	if totalScore < 0.0 {
		totalScore = 0.0
	}

	// Update session with score
	s.mu.Lock()
	session.LastScore = totalScore
	session.LastScoreTime = time.Now()
	s.mu.Unlock()

	return totalScore
}

// calculateDwellScore: visible time / total time ratio
func (s *Scorer) calculateDwellScore(session *SessionMetrics) float64 {
	totalTime := time.Since(session.StartTime).Seconds()
	if totalTime <= 0 {
		return 0.0
	}

	// Calculate visible time including current period if visible
	visibleTime := session.VisibleTime

	ratio := visibleTime / totalTime
	if ratio > 1.0 {
		ratio = 1.0
	}

	// Apply sigmoid to reward longer dwell times
	// A 30+ second dwell with high visibility is very human-like
	return sigmoid(ratio, 0.5, 10)
}

// calculateScrollScore: scroll depth and natural patterns
func (s *Scorer) calculateScrollScore(session *SessionMetrics) float64 {
	// Base score from scroll depth
	baseScore := session.MaxScrollPercent
	if baseScore > 1.0 {
		baseScore = 1.0
	}

	// Bonus for natural scroll velocity patterns (humans have variable scroll speeds)
	if len(session.ScrollVelocities) > 2 {
		variance := calculateVariance(session.ScrollVelocities)
		// Too uniform = bot, some variance = human
		if variance > 0.01 && variance < 10.0 {
			baseScore *= 1.2 // Boost for natural variance
		}
	}

	if baseScore > 1.0 {
		baseScore = 1.0
	}

	return baseScore
}

// calculatePointerScore: mouse movement patterns
func (s *Scorer) calculatePointerScore(session *SessionMetrics) float64 {
	// Binary: has mouse activity at all
	if session.MouseMoves == 0 && session.TouchEvents == 0 {
		return 0.0
	}

	baseScore := 0.5 // Some activity

	// Humans have variable mouse velocities
	if len(session.MouseVelocities) > 5 {
		variance := calculateVariance(session.MouseVelocities)
		// Bots often have either very uniform or very erratic movement
		if variance > 0.1 && variance < 100.0 {
			baseScore = 0.9 // Natural variance pattern
		} else if variance <= 0.1 {
			baseScore = 0.3 // Too uniform - suspicious
		} else {
			baseScore = 0.4 // Too erratic - also suspicious
		}
	}

	// Boost for touch events (mobile users)
	if session.TouchEvents > 0 {
		baseScore += 0.2
	}

	if baseScore > 1.0 {
		baseScore = 1.0
	}

	return baseScore
}

// calculateClickScore: click patterns
func (s *Scorer) calculateClickScore(session *SessionMetrics) float64 {
	if session.ClickCount == 0 {
		return 0.0
	}

	totalTime := time.Since(session.StartTime).Seconds()
	if totalTime <= 0 {
		return 0.0
	}

	// Clicks per second
	clickRate := float64(session.ClickCount) / totalTime

	// Natural click rate is 0.1-2 clicks per second
	// Too fast = bot, none = no interaction
	var baseScore float64
	if clickRate > 0.05 && clickRate < 3.0 {
		baseScore = 0.8
	} else if clickRate >= 3.0 {
		baseScore = 0.2 // Suspiciously fast clicking
	} else {
		baseScore = 0.4
	}

	// Check for natural intervals between clicks
	if len(session.ClickIntervals) > 2 {
		variance := calculateVariance(session.ClickIntervals)
		// Natural human timing has variance
		if variance > 0.01 {
			baseScore += 0.2
		}
	}

	if baseScore > 1.0 {
		baseScore = 1.0
	}

	return baseScore
}

// EvaluateRequest determines action based on score
func (s *Scorer) EvaluateRequest(sessionID string, path string, pathConfigs map[string]config.PathConfig) *EvaluationResult {
	score := s.CalculateScore(sessionID)

	// Get path-specific config or use defaults
	minScore := s.config.Thresholds.ScoreChallenge
	var stepUpMethod *string

	if cfg, exists := pathConfigs[path]; exists {
		minScore = cfg.MinScore
		stepUpMethod = cfg.StepUpMethod
	}

	result := &EvaluationResult{
		SessionID:  sessionID,
		Score:      score,
		MinScore:   minScore,
		Action:     "allow",
	}

	if score >= s.config.Thresholds.ScorePass {
		result.Action = "allow"
	} else if score >= minScore {
		result.Action = "allow" // In gray zone but above minimum
	} else {
		result.Action = "challenge"
		if stepUpMethod != nil {
			result.StepUpMethod = *stepUpMethod
		} else {
			result.StepUpMethod = s.config.StepUp.Primary
		}
	}

	return result
}

// EvaluationResult contains the score evaluation outcome
type EvaluationResult struct {
	SessionID    string  `json:"session_id"`
	Score        float64 `json:"score"`
	MinScore     float64 `json:"min_score"`
	Action       string  `json:"action"` // "allow", "challenge", "block"
	StepUpMethod string  `json:"step_up_method,omitempty"`
}

// CleanupOldSessions removes sessions older than maxAge
func (s *Scorer) CleanupOldSessions(maxAge time.Duration) {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now()
	for id, session := range s.sessions {
		if now.Sub(session.StartTime) > maxAge {
			delete(s.sessions, id)
		}
	}
}

// Helper functions

func calculateVariance(values []float64) float64 {
	if len(values) < 2 {
		return 0.0
	}

	// Calculate mean
	var sum float64
	for _, v := range values {
		sum += v
	}
	mean := sum / float64(len(values))

	// Calculate variance
	var variance float64
	for _, v := range values {
		diff := v - mean
		variance += diff * diff
	}
	variance /= float64(len(values) - 1)

	return variance
}

// sigmoid applies a sigmoid transformation for smoother score curves
func sigmoid(x, midpoint, steepness float64) float64 {
	return 1.0 / (1.0 + math.Exp(-steepness*(x-midpoint)))
}
