// Package learning implements the Oikos learning engine (Phase 3).
// Hourly pattern extraction: reads feedback past the watermark, groups by
// (applies_type, action), updates pattern counters with Wilson confidence,
// detects anomalies, and refines skills.
package learning

import (
	"context"
	"log/slog"
	"math"
	"time"

	"github.com/dtoro/oikos/internal/config"
	"github.com/dtoro/oikos/internal/adapters/postgres"
	"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
	"github.com/google/uuid"
)

// Run starts the learning loop. Blocks until ctx is cancelled.
func Run(ctx context.Context, pool *db.Pool, cfg config.Config) {
	slog.Info("learning: starting", "interval", cfg.LearningInterval)
	interval := cfg.LearningInterval
	if interval <= 0 {
		interval = 1 * time.Hour
	}

	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	watermark := time.Now().Add(-24 * time.Hour) // start from 24h ago

	for {
		select {
		case <-ctx.Done():
			slog.Info("learning: shutting down")
			return
		case <-ticker.C:
			watermark = extractPatterns(ctx, pool, watermark)
		}
	}
}

// extractPatterns reads feedback past the watermark, groups by (type, action),
// updates pattern counters, and returns the new watermark.
func extractPatterns(ctx context.Context, pool *db.Pool, watermark time.Time) time.Time {
	q := sqlcgen.New(pool)

	feedback, err := q.GetFeedbackAfterWatermark(ctx, watermark)
	if err != nil {
		slog.Error("learning: get feedback", "error", err)
		return watermark
	}
	if len(feedback) == 0 {
		// Advance watermark to now so we don't re-scan
		return time.Now()
	}

	// Group by (applies_type, action)
	type groupKey struct {
		Type   string
		Action string
	}
	groups := make(map[groupKey][]sqlcgen.GetFeedbackAfterWatermarkRow)
	for _, f := range feedback {
		key := groupKey{Type: f.AppliesType, Action: f.Action}
		groups[key] = append(groups[key], f)
	}

	for key, items := range groups {
		processGroup(ctx, pool, q, key.Type, key.Action, items)
	}

	// Update watermark to the latest feedback timestamp
	newWatermark := watermark
	for _, f := range feedback {
		if f.CreatedAt.After(newWatermark) {
			newWatermark = f.CreatedAt
		}
	}
	return newWatermark
}

func processGroup(ctx context.Context, pool *db.Pool, q *sqlcgen.Queries,
	appliesType, action string, items []sqlcgen.GetFeedbackAfterWatermarkRow) {

	successCount, failureCount := countOutcomes(items)
	total := successCount + failureCount
	if total == 0 {
		return
	}

	confidence := computeConfidence(successCount, failureCount)

	// Get or create pattern — first look up existing entity, then upsert.
	existing, err := q.GetPattern(ctx, sqlcgen.GetPatternParams{
		AppliesType: appliesType,
		Action:      action,
	})

	var patternID uuid.UUID
	if err == nil && existing.EntityID != uuid.Nil {
		patternID = existing.EntityID
	} else {
		id, idErr := uuid.NewV7()
		if idErr != nil {
			slog.Error("learning: gen pattern uuid", "error", idErr)
			return
		}
		patternID = id
	}
	patternSummary := action + " on " + appliesType

	err = q.UpsertPattern(ctx, sqlcgen.UpsertPatternParams{
		EntityID:      patternID,
		AppliesType:   appliesType,
		Action:        action,
		Pattern:       patternSummary,
		Confidence:    float32(confidence),
		EvidenceCount: int32(total),
		SuccessCount:  int32(successCount),
		FailureCount:  int32(failureCount),
	})
	if err != nil {
		slog.Error("learning: upsert pattern", "error", err)
		return
	}

	// Update pattern status based on confidence
	pat, err := q.GetPattern(ctx, sqlcgen.GetPatternParams{
		AppliesType: appliesType,
		Action:      action,
	})
	if err != nil {
		return
	}

	if shouldValidate(int(pat.EvidenceCount), float64(pat.Confidence), pat.Quarantined) {
		_ = q.UpdatePatternStatus(ctx, sqlcgen.UpdatePatternStatusParams{
			EntityID: pat.EntityID,
			Status:   "validated",
		})
		slog.Info("learning: pattern validated",
			"type", appliesType, "action", action,
			"confidence", confidence, "samples", total)
	}

	if shouldQuarantine(total) {
		_ = q.UpdatePatternQuarantine(ctx, sqlcgen.UpdatePatternQuarantineParams{
			EntityID:    pat.EntityID,
			Quarantined: true,
		})
		slog.Warn("learning: pattern quarantined (anomaly burst)",
			"type", appliesType, "action", action)
	}
}

// countOutcomes tallies feedback items into success and failure counts.
// "partial" counts as a half-success (increments success).
func countOutcomes(items []sqlcgen.GetFeedbackAfterWatermarkRow) (success, failure int) {
	for _, f := range items {
		switch f.Outcome {
		case "success":
			success++
		case "failure", "unexpected":
			failure++
		case "partial":
			success++
		}
	}
	return success, failure
}

// computeConfidence calculates the Wilson score lower bound, capped by
// sample size (nothing looks confident before 5 samples).
func computeConfidence(success, failure int) float64 {
	total := success + failure
	if total == 0 {
		return 0
	}
	confidence := wilsonLowerBound(float64(success), float64(total), 0.95)
	return math.Min(confidence, float64(total)/5.0)
}

// shouldValidate returns true when a pattern has enough evidence and
// confidence to be promoted from "hypothesized" to "validated".
func shouldValidate(evidenceCount int, confidence float64, quarantined bool) bool {
	return evidenceCount >= 5 && confidence >= 0.7 && !quarantined
}

// shouldQuarantine returns true when an anomaly burst is detected
// (>10 identical outcomes, indicating a runaway loop rather than organic feedback).
func shouldQuarantine(total int) bool {
	return total > 10
}

// wilsonLowerBound computes the Wilson score interval lower bound.
// Conservative estimate of success rate for small sample sizes.
func wilsonLowerBound(success, total, z float64) float64 {
	if total == 0 {
		return 0
	}
	p := success / total
	z2 := z * z
	denom := 1 + z2/total
	center := (p + z2/(2*total)) / denom
	sp := math.Sqrt((p*(1-p) + z2/(4*total)) / total) / denom
	return math.Max(0, center-z*sp)
}