// Package baseline provides learned baseline metrics for anomaly detection.
// It learns what "normal" looks like for each resource by analyzing historical
// metrics and can then flag current values that deviate significantly from the baseline.
package baseline

import (
	"encoding/json"
	"math"
	"os"
	"path/filepath"
	"sort"
	"sync"
	"time"

	"github.com/rs/zerolog/log"
)

// MetricBaseline represents learned "normal" behavior for a single metric
type MetricBaseline struct {
	Mean        float64            `json:"mean"`         // Average value
	StdDev      float64            `json:"stddev"`       // Standard deviation
	Percentiles map[int]float64    `json:"percentiles"`  // 5, 25, 50, 75, 95
	SampleCount int                `json:"sample_count"` // Number of samples used
	
	// Time-of-day patterns (future enhancement)
	HourlyMeans [24]float64 `json:"hourly_means,omitempty"`
}

// ResourceBaseline contains baselines for all metrics of a resource
type ResourceBaseline struct {
	ResourceID   string                     `json:"resource_id"`
	ResourceType string                     `json:"resource_type"` // node, vm, container, storage
	LastUpdated  time.Time                  `json:"last_updated"`
	Metrics      map[string]*MetricBaseline `json:"metrics"` // cpu, memory, disk
}

// Store manages baseline storage and learning
type Store struct {
	mu        sync.RWMutex
	baselines map[string]*ResourceBaseline // resourceID -> baseline
	
	// Configuration
	learningWindow  time.Duration // How far back to learn from (default: 7 days)
	minSamples      int           // Minimum samples needed (default: 50)
	updateInterval  time.Duration // How often to recompute (default: 1 hour)
	
	// Persistence
	dataDir     string
	persistence Persistence
}

// Persistence interface for saving/loading baselines
type Persistence interface {
	Save(baselines map[string]*ResourceBaseline) error
	Load() (map[string]*ResourceBaseline, error)
}

// StoreConfig configures the baseline store
type StoreConfig struct {
	LearningWindow  time.Duration
	MinSamples      int
	UpdateInterval  time.Duration
	DataDir         string
}

// DefaultConfig returns sensible defaults
func DefaultConfig() StoreConfig {
	return StoreConfig{
		LearningWindow:  7 * 24 * time.Hour, // 7 days
		MinSamples:      50,
		UpdateInterval:  1 * time.Hour,
	}
}

// NewStore creates a new baseline store
func NewStore(cfg StoreConfig) *Store {
	if cfg.LearningWindow == 0 {
		cfg.LearningWindow = 7 * 24 * time.Hour
	}
	if cfg.MinSamples == 0 {
		cfg.MinSamples = 50
	}
	if cfg.UpdateInterval == 0 {
		cfg.UpdateInterval = 1 * time.Hour
	}
	
	s := &Store{
		baselines:      make(map[string]*ResourceBaseline),
		learningWindow: cfg.LearningWindow,
		minSamples:     cfg.MinSamples,
		updateInterval: cfg.UpdateInterval,
		dataDir:        cfg.DataDir,
	}
	
	// Try to load existing baselines from disk
	if cfg.DataDir != "" {
		if err := s.loadFromDisk(); err != nil {
			log.Warn().Err(err).Msg("Failed to load baselines from disk, starting fresh")
		} else {
			log.Info().Int("count", len(s.baselines)).Msg("Loaded baselines from disk")
		}
	}
	
	return s
}

// MetricPoint represents a single metric value at a point in time
type MetricPoint struct {
	Value     float64
	Timestamp time.Time
}

// Learn computes baseline from historical data points
func (s *Store) Learn(resourceID, resourceType, metric string, points []MetricPoint) error {
	if len(points) < s.minSamples {
		log.Debug().
			Str("resource", resourceID).
			Str("metric", metric).
			Int("samples", len(points)).
			Int("required", s.minSamples).
			Msg("Insufficient data for baseline learning")
		return nil // Not an error, just not enough data yet
	}
	
	// Extract values
	values := make([]float64, len(points))
	for i, p := range points {
		values[i] = p.Value
	}
	
	// Compute statistics
	baseline := &MetricBaseline{
		Mean:        computeMean(values),
		StdDev:      computeStdDev(values),
		Percentiles: computePercentiles(values),
		SampleCount: len(values),
	}
	
	s.mu.Lock()
	defer s.mu.Unlock()
	
	// Get or create resource baseline
	rb, exists := s.baselines[resourceID]
	if !exists {
		rb = &ResourceBaseline{
			ResourceID:   resourceID,
			ResourceType: resourceType,
			Metrics:      make(map[string]*MetricBaseline),
		}
		s.baselines[resourceID] = rb
	}
	
	rb.Metrics[metric] = baseline
	rb.LastUpdated = time.Now()
	
	log.Debug().
		Str("resource", resourceID).
		Str("metric", metric).
		Float64("mean", baseline.Mean).
		Float64("stddev", baseline.StdDev).
		Int("samples", baseline.SampleCount).
		Msg("Baseline learned")
	
	return nil
}

// GetBaseline returns the baseline for a resource/metric
func (s *Store) GetBaseline(resourceID, metric string) (*MetricBaseline, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	
	rb, exists := s.baselines[resourceID]
	if !exists {
		return nil, false
	}
	
	mb, exists := rb.Metrics[metric]
	return mb, exists
}

// GetResourceBaseline returns all baselines for a resource
func (s *Store) GetResourceBaseline(resourceID string) (*ResourceBaseline, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	
	rb, exists := s.baselines[resourceID]
	if !exists {
		return nil, false
	}
	
	// Return a copy to prevent mutation
	copy := &ResourceBaseline{
		ResourceID:   rb.ResourceID,
		ResourceType: rb.ResourceType,
		LastUpdated:  rb.LastUpdated,
		Metrics:      make(map[string]*MetricBaseline),
	}
	for k, v := range rb.Metrics {
		copy.Metrics[k] = v
	}
	return copy, true
}

// IsAnomaly checks if a value is anomalous for the given resource/metric
// Returns: isAnomaly, zScore (number of standard deviations from mean)
func (s *Store) IsAnomaly(resourceID, metric string, value float64) (bool, float64) {
	baseline, ok := s.GetBaseline(resourceID, metric)
	if !ok || baseline.SampleCount < s.minSamples {
		return false, 0 // Not enough data to determine
	}
	
	if baseline.StdDev == 0 {
		// No variance - any different value is anomalous
		if value != baseline.Mean {
			return true, math.Inf(1)
		}
		return false, 0
	}
	
	zScore := (value - baseline.Mean) / baseline.StdDev
	
	// Consider anything > 2 standard deviations as anomalous
	// (covers ~95% of normal distribution)
	isAnomaly := math.Abs(zScore) > 2.0
	
	return isAnomaly, zScore
}

// AnomalySeverity categorizes how severe an anomaly is
type AnomalySeverity string

const (
	AnomalyNone     AnomalySeverity = ""
	AnomalyLow      AnomalySeverity = "low"      // 2-2.5 std devs
	AnomalyMedium   AnomalySeverity = "medium"   // 2.5-3 std devs
	AnomalyHigh     AnomalySeverity = "high"     // 3-4 std devs
	AnomalyCritical AnomalySeverity = "critical" // > 4 std devs
)

// CheckAnomaly performs a detailed anomaly check with severity classification
func (s *Store) CheckAnomaly(resourceID, metric string, value float64) (AnomalySeverity, float64, *MetricBaseline) {
	baseline, ok := s.GetBaseline(resourceID, metric)
	if !ok || baseline.SampleCount < s.minSamples {
		return AnomalyNone, 0, nil
	}
	
	if baseline.StdDev == 0 {
		if value != baseline.Mean {
			return AnomalyCritical, math.Inf(1), baseline
		}
		return AnomalyNone, 0, baseline
	}
	
	zScore := (value - baseline.Mean) / baseline.StdDev
	absZ := math.Abs(zScore)
	
	var severity AnomalySeverity
	switch {
	case absZ < 2.0:
		severity = AnomalyNone
	case absZ < 2.5:
		severity = AnomalyLow
	case absZ < 3.0:
		severity = AnomalyMedium
	case absZ < 4.0:
		severity = AnomalyHigh
	default:
		severity = AnomalyCritical
	}
	
	return severity, zScore, baseline
}

// ResourceCount returns the number of resources with baselines
func (s *Store) ResourceCount() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return len(s.baselines)
}

// FlatBaseline is a flattened representation of a single metric baseline for API responses
type FlatBaseline struct {
	ResourceID string    `json:"resource_id"`
	Metric     string    `json:"metric"`
	Mean       float64   `json:"mean"`
	StdDev     float64   `json:"std_dev"`
	Min        float64   `json:"min"`
	Max        float64   `json:"max"`
	Samples    int       `json:"samples"`
	LastUpdate time.Time `json:"last_update"`
}

// GetAllBaselines returns all baselines as a flat map for API access
func (s *Store) GetAllBaselines() map[string]*FlatBaseline {
	s.mu.RLock()
	defer s.mu.RUnlock()
	
	result := make(map[string]*FlatBaseline)
	for resourceID, rb := range s.baselines {
		for metric, mb := range rb.Metrics {
			key := resourceID + ":" + metric
			fb := &FlatBaseline{
				ResourceID: resourceID,
				Metric:     metric,
				Mean:       mb.Mean,
				StdDev:     mb.StdDev,
				Samples:    mb.SampleCount,
				LastUpdate: rb.LastUpdated,
			}
			// Set min/max from percentiles if available
			if mb.Percentiles != nil {
				if p5, ok := mb.Percentiles[5]; ok {
					fb.Min = p5
				}
				if p95, ok := mb.Percentiles[95]; ok {
					fb.Max = p95
				}
			}
			result[key] = fb
		}
	}
	return result
}

// Save persists baselines to disk
func (s *Store) Save() error {
	if s.dataDir == "" {
		return nil
	}
	
	s.mu.RLock()
	defer s.mu.RUnlock()
	
	return s.saveToDisk()
}

// saveToDisk writes baselines to JSON file
func (s *Store) saveToDisk() error {
	if s.dataDir == "" {
		return nil
	}
	
	path := filepath.Join(s.dataDir, "baselines.json")
	
	data, err := json.MarshalIndent(s.baselines, "", "  ")
	if err != nil {
		return err
	}
	
	// Write to temp file first, then rename for atomicity
	tmpPath := path + ".tmp"
	if err := os.WriteFile(tmpPath, data, 0600); err != nil {
		return err
	}
	
	return os.Rename(tmpPath, path)
}

// loadFromDisk reads baselines from JSON file
func (s *Store) loadFromDisk() error {
	path := filepath.Join(s.dataDir, "baselines.json")
	
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // No saved baselines yet
		}
		return err
	}
	
	return json.Unmarshal(data, &s.baselines)
}

// Helper functions for statistics

func computeMean(values []float64) float64 {
	if len(values) == 0 {
		return 0
	}
	sum := 0.0
	for _, v := range values {
		sum += v
	}
	return sum / float64(len(values))
}

func computeStdDev(values []float64) float64 {
	if len(values) < 2 {
		return 0
	}
	mean := computeMean(values)
	sumSqDiff := 0.0
	for _, v := range values {
		diff := v - mean
		sumSqDiff += diff * diff
	}
	variance := sumSqDiff / float64(len(values)-1) // Sample standard deviation
	return math.Sqrt(variance)
}

func computePercentiles(values []float64) map[int]float64 {
	if len(values) == 0 {
		return nil
	}
	
	// Sort a copy
	sorted := make([]float64, len(values))
	copy(sorted, values)
	sort.Float64s(sorted)
	
	percentiles := map[int]float64{
		5:  percentile(sorted, 5),
		25: percentile(sorted, 25),
		50: percentile(sorted, 50),
		75: percentile(sorted, 75),
		95: percentile(sorted, 95),
	}
	
	return percentiles
}

func percentile(sorted []float64, p int) float64 {
	if len(sorted) == 0 {
		return 0
	}
	
	// Use linear interpolation
	rank := float64(p) / 100.0 * float64(len(sorted)-1)
	lower := int(rank)
	upper := lower + 1
	
	if upper >= len(sorted) {
		return sorted[len(sorted)-1]
	}
	
	// Interpolate
	weight := rank - float64(lower)
	return sorted[lower]*(1-weight) + sorted[upper]*weight
}
