package scheduler

import (
	"sync"
	"time"
)

// RefreshStatusStore is a concurrent-safe in-memory store for scheduler runtime
// refresh status. The worker records a status after each tick; the socket
// handler reads all or a named schedule's status on demand.
type RefreshStatusStore struct {
	mu      sync.RWMutex
	entries map[string]RefreshStatus
	config  Config
}

// NewRefreshStatusStore returns an empty, ready-to-use store.
func NewRefreshStatusStore() *RefreshStatusStore {
	return &RefreshStatusStore{
		entries: make(map[string]RefreshStatus),
	}
}

// SetScheduleConfig stores the latest schedule config inside the store so the
// socket handler can compute next_run values without importing the scheduler
// package directly.
func (s *RefreshStatusStore) SetScheduleConfig(cfg Config) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.config = cfg
}

// ScheduleConfig returns the stored config (zero value if not set).
func (s *RefreshStatusStore) ScheduleConfig() Config {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.config
}

// Record upserts the refresh status for the given schedule name.
func (s *RefreshStatusStore) Record(name string, status RefreshStatus) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.entries[name] = status
}

// All returns a snapshot of all recorded statuses, excluding internal entries.
func (s *RefreshStatusStore) All() []RefreshStatus {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]RefreshStatus, 0, len(s.entries))
	for k, v := range s.entries {
		if k == "_config" {
			continue
		}
		out = append(out, v)
	}
	return out
}

// Get returns the status for a named schedule. The second return value is false
// when no status has been recorded for that name yet.
func (s *RefreshStatusStore) Get(name string) (RefreshStatus, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	v, ok := s.entries[name]
	return v, ok
}

// RecordTick updates the store from a completed TickResult. Each item whose
// status is "succeeded" and has non-nil ImportSummary is aggregated into the
// status entry. Items with status "succeeded" but nil ImportSummary are treated
// as "ran but no evidence" — they count towards success classification but do
// NOT contribute bar counts, preventing fabricated S04 evidence.
//
// RecordTick only writes when at least one item belongs to the schedule.
// Zero-item runs are skipped so that no-item success, zero imported bar count,
// and next_run 누락 같은 false evidence is not created.
//
// Important: skipped/duplicate-window items are NOT treated as success. If any
// items exist but none succeeded (all skipped or all failed), the entry is
// marked "stale" (no error) or "error" (failed with message) rather than
// "success". A mixed result (some succeeded, some skipped/failed) is still
// "success" only when all succeeded AND no skipped/failed items exist; otherwise
// it is classified by the worst outcome. This prevents fabricating S04 evidence.
func (s *RefreshStatusStore) RecordTick(scheduleName string, items []ItemResult, now time.Time) {
	if scheduleName == "" {
		return
	}

	// Aggregate item outcomes for this schedule.
	var lastErr string
	totalImportedBars := 0
	totalMissingBars := 0
	totalGapBars := 0
	totalDuplicateBars := 0
	totalProviderDelayDays := 0
	hasEvidence := false
	succeededCount := 0
	skippedCount := 0
	ranNoEvidenceCount := 0 // succeeded but no ImportSummary
	failedCount := 0
	hasItems := false

	for _, item := range items {
		if item.Schedule != scheduleName {
			continue
		}
		hasItems = true
		switch item.Status {
		case "succeeded":
			succeededCount++
			if item.ImportSummary != nil {
				hasEvidence = true
				totalImportedBars += item.ImportSummary.ImportedBars
				totalMissingBars += item.ImportSummary.MissingBars
				totalGapBars += item.ImportSummary.GapBars
				totalDuplicateBars += item.ImportSummary.DuplicateBars
				if item.ImportSummary.ProviderDelayDays > totalProviderDelayDays {
					totalProviderDelayDays = item.ImportSummary.ProviderDelayDays
				}
			} else {
				// Item succeeded but no import evidence — counts for
				// classification but not for bar counts.
				ranNoEvidenceCount++
			}
		case "skipped":
			skippedCount++
		case "failed":
			failedCount++
			if item.Error != "" {
				lastErr = item.Error
			}
		}
	}

	// Skip recording when no items were found for this schedule.
	// This prevents false evidence (no-item success, zero imported bar count, etc.).
	if !hasItems {
		return
	}

	// Determine status based on outcomes:
	// - If any failed: status is "error"
	// - If only skipped/duplicate (no succeeded, no failed): status is "stale"
	// - If all succeeded but no evidence: status is "stale" (no-evidence success)
	// - If all succeeded with at least one evidence: status is "success"
	var statusStr string
	if failedCount > 0 {
		statusStr = "error"
	} else if skippedCount > 0 || succeededCount == 0 {
		// If there are skipped items or no succeeded items (but hasItems), don't claim success
		statusStr = "stale"
	} else if !hasEvidence {
		// All items succeeded but no evidence — classify as "stale" rather than
		// "success", preventing the silent "ran but unknown" → "success" escalation.
		statusStr = "stale"
	} else {
		// All items succeeded AND at least one carried actual import evidence
		statusStr = "success"
	}

	existing, hasExisting := s.Get(scheduleName)

	var lastSuccess time.Time
	// Only update last_success when we have actual evidence (ImportSummary).
	// No-evidence success ("succeeded" but ImportSummary == nil) must NOT
	// fabricate last_success evidence, preventing the "ran but unknown how
	// much data" → "success" silent escalation.
	if statusStr == "success" && hasEvidence {
		lastSuccess = now
	} else if hasExisting {
		lastSuccess = existing.LastSuccess
	}

	// Accumulate counts only from actual evidence.
	// For stale/error, preserve previous counts (don't add from partial/no-evidence runs).
	var finalImportedBarCount, finalMissingCount, finalGapCount, finalDuplicateCount int
	var finalProviderDelayDays int
	if hasExisting {
		finalImportedBarCount = existing.ImportedBarCount
		finalMissingCount = existing.MissingCount
		finalGapCount = existing.GapCount
		finalDuplicateCount = existing.DuplicateCount
		finalProviderDelayDays = existing.ProviderDelay
	}
	if statusStr == "success" && hasEvidence {
		finalImportedBarCount += totalImportedBars
		finalMissingCount += totalMissingBars
		finalGapCount += totalGapBars
		finalDuplicateCount += totalDuplicateBars
		if totalProviderDelayDays > finalProviderDelayDays {
			finalProviderDelayDays = totalProviderDelayDays
		}
	}

	s.Record(scheduleName, RefreshStatus{
		Schedule:         scheduleName,
		Status:           statusStr,
		LastSuccess:      lastSuccess,
		LastError:        lastErr,
		ImportedBarCount: finalImportedBarCount,
		MissingCount:     finalMissingCount,
		GapCount:         finalGapCount,
		DuplicateCount:   finalDuplicateCount,
		ProviderDelay:    finalProviderDelayDays,
	})
}
