package stats

import (
	"context"
	"database/sql"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/telemetry"
)

const (
	maxNegativeMileageJitterKM  = 1.0
	defaultCacheRetention       = 72 * time.Hour
	defaultCacheCleanupInterval = 10 * time.Minute
	defaultBaselineMissTTL      = time.Minute
	defaultMaxCacheEntries      = 1000000
)

type Execer interface {
	ExecContext(context.Context, string, ...any) (sql.Result, error)
}

type Writer struct {
	exec                   Execer
	query                  Queryer
	loc                    *time.Location
	sourceTouchInterval    time.Duration
	projectionInterval     time.Duration
	cacheRetention         time.Duration
	cacheCleanupInterval   time.Duration
	baselineMissTTL        time.Duration
	maxCacheEntries        int
	lastCacheCleanup       time.Time
	lastCacheCleanupStats  cacheCleanupStats
	cacheEvictions         cacheCleanupStats
	mu                     sync.Mutex
	lastTotalMileage       map[string]float64
	lastSourceSeen         map[string]time.Time
	lastProjection         map[string]projectionCacheEntry
	baselineCache          map[string]sourceBaselineCacheEntry
	mileageKeysByPrefix    map[string]map[string]struct{}
	projectionKeysByPrefix map[string]map[string]struct{}
	baselineKeysByPrefix   map[string]map[string]struct{}
}

type MetricSample struct {
	VIN            string
	Protocol       envelope.Protocol
	StatDate       string
	TotalMileageKM float64
	EventTime      time.Time
	SourceKey      string
	Phone          string
	DeviceID       string
	SourceEndpoint string
	PlatformName   string
}

type AppendResult struct {
	SamplesFound                     int
	SamplesWritten                   int
	SamplesSkippedMissingFields      int
	SamplesSkippedMissingVIN         int
	SamplesSkippedMissingMileage     int
	SamplesSkippedNonMileageFrame    int
	SamplesSkippedNonPositiveMileage int
	SamplesSkippedMissingTime        int
	SamplesSkippedSameMileage        int
	SamplesSkippedMissingSource      int
	SamplesAdjustedFutureEventTime   int
	SourceTouchesAttempted           int
	SourceTouchesWritten             int
	SourceTouchesSkippedThrottled    int
	SourceTouchesSkippedMissing      int
	SourceTouchesSkippedUnmanaged    int
	ProjectionsAttempted             int
	ProjectionsWritten               int
	ProjectionsSkippedThrottled      int
}

type CacheStats struct {
	LastTotalMileageEntries int
	LastSourceSeenEntries   int
	LastProjectionEntries   int
	BaselineEntries         int
	MaxEntries              int
	LastCleanupAt           time.Time
	LastCleanupTotalMileage int
	LastCleanupSourceSeen   int
	LastCleanupProjection   int
	LastCleanupBaseline     int
	TotalMileageEvictions   int
	SourceSeenEvictions     int
	ProjectionEvictions     int
	BaselineEvictions       int
}

type cacheCleanupStats struct {
	totalMileage int
	sourceSeen   int
	projection   int
	baseline     int
}

func NewWriter(exec Execer, loc *time.Location) *Writer {
	if exec == nil {
		panic("stats execer must not be nil")
	}
	if loc == nil {
		loc = time.FixedZone("Asia/Shanghai", 8*3600)
	}
	writer := &Writer{
		exec:                   exec,
		loc:                    loc,
		sourceTouchInterval:    time.Minute,
		projectionInterval:     15 * time.Second,
		cacheRetention:         defaultCacheRetention,
		cacheCleanupInterval:   defaultCacheCleanupInterval,
		baselineMissTTL:        defaultBaselineMissTTL,
		maxCacheEntries:        defaultMaxCacheEntries,
		lastTotalMileage:       map[string]float64{},
		lastSourceSeen:         map[string]time.Time{},
		lastProjection:         map[string]projectionCacheEntry{},
		baselineCache:          map[string]sourceBaselineCacheEntry{},
		mileageKeysByPrefix:    map[string]map[string]struct{}{},
		projectionKeysByPrefix: map[string]map[string]struct{}{},
		baselineKeysByPrefix:   map[string]map[string]struct{}{},
	}
	if query, ok := exec.(Queryer); ok {
		writer.query = query
	}
	return writer
}

func (w *Writer) SetSourceTouchInterval(interval time.Duration) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if interval < 0 {
		interval = 0
	}
	w.sourceTouchInterval = interval
}

func (w *Writer) SetProjectionInterval(interval time.Duration) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if interval < 0 {
		interval = 0
	}
	w.projectionInterval = interval
}

func (w *Writer) SetCacheRetention(retention time.Duration) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if retention < 0 {
		retention = 0
	}
	w.cacheRetention = retention
}

func (w *Writer) SetCacheCleanupInterval(interval time.Duration) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if interval < 0 {
		interval = 0
	}
	w.cacheCleanupInterval = interval
}

func (w *Writer) SetBaselineMissTTL(ttl time.Duration) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if ttl < 0 {
		ttl = 0
	}
	w.baselineMissTTL = ttl
}

func (w *Writer) SetMaxCacheEntries(maxEntries int) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if maxEntries < 0 {
		maxEntries = 0
	}
	w.maxCacheEntries = maxEntries
	w.enforceCacheLimitsLocked()
}

func (w *Writer) CacheStats() CacheStats {
	w.mu.Lock()
	defer w.mu.Unlock()
	return CacheStats{
		LastTotalMileageEntries: len(w.lastTotalMileage),
		LastSourceSeenEntries:   len(w.lastSourceSeen),
		LastProjectionEntries:   len(w.lastProjection),
		BaselineEntries:         len(w.baselineCache),
		MaxEntries:              w.maxCacheEntries,
		LastCleanupAt:           w.lastCacheCleanup,
		LastCleanupTotalMileage: w.lastCacheCleanupStats.totalMileage,
		LastCleanupSourceSeen:   w.lastCacheCleanupStats.sourceSeen,
		LastCleanupProjection:   w.lastCacheCleanupStats.projection,
		LastCleanupBaseline:     w.lastCacheCleanupStats.baseline,
		TotalMileageEvictions:   w.cacheEvictions.totalMileage,
		SourceSeenEvictions:     w.cacheEvictions.sourceSeen,
		ProjectionEvictions:     w.cacheEvictions.projection,
		BaselineEvictions:       w.cacheEvictions.baseline,
	}
}

func (w *Writer) EnsureSchema(ctx context.Context) error {
	for _, statement := range []string{
		DataSourceTableSQL,
		DailyMileageSourceTableSQL,
		DailyMileageTableSQL,
	} {
		if _, err := w.exec.ExecContext(ctx, statement); err != nil {
			return err
		}
	}
	for _, statement := range DailyMileageAlterSQL {
		if _, err := w.exec.ExecContext(ctx, statement); err != nil && !isIgnorableSchemaChangeError(err) {
			return err
		}
	}
	return nil
}

func (w *Writer) Append(ctx context.Context, env envelope.FrameEnvelope) error {
	_, err := w.AppendWithResult(ctx, env)
	return err
}

func (w *Writer) AppendWithResult(ctx context.Context, env envelope.FrameEnvelope) (AppendResult, error) {
	var result AppendResult
	if len(env.Fields) == 0 {
		result.SamplesSkippedMissingFields = 1
		return result, nil
	}
	seenAt := w.sourceSeenAt(env)
	w.maybeCleanupCaches(seenAt)
	identity, hasSource := NewSourceIdentityFromEnvelope(env)
	var sourceResult AppendResult
	if hasSource && ShouldManageDataSource(identity) {
		if w.shouldTouchSource(identity, seenAt) {
			sourceResult.SourceTouchesAttempted = 1
			if err := UpsertDataSource(ctx, w.exec, identity, seenAt); err != nil {
				return sourceResult, err
			}
			w.markSourceTouched(identity, seenAt)
			sourceResult.SourceTouchesWritten = 1
		} else {
			sourceResult.SourceTouchesSkippedThrottled = 1
		}
	} else if hasSource {
		sourceResult.SourceTouchesSkippedUnmanaged = 1
	}
	samples, extractionResult, err := samplesFromEnvelopeWithResult(env, w.loc)
	result = extractionResult
	result.SourceTouchesAttempted += sourceResult.SourceTouchesAttempted
	result.SourceTouchesWritten += sourceResult.SourceTouchesWritten
	result.SourceTouchesSkippedThrottled += sourceResult.SourceTouchesSkippedThrottled
	result.SourceTouchesSkippedMissing += sourceResult.SourceTouchesSkippedMissing
	result.SourceTouchesSkippedUnmanaged += sourceResult.SourceTouchesSkippedUnmanaged
	if err != nil {
		return result, err
	}
	for _, sample := range samples {
		if !hasSource {
			result.SamplesSkippedMissingSource++
			result.SourceTouchesSkippedMissing++
			continue
		}
		if w.seenSameMileage(sample) {
			result.SamplesSkippedSameMileage++
			continue
		}
		candidate := SourceMileageSampleFromMetric(sample, identity)
		if err := w.applyRealtimeBaseline(ctx, &candidate); err != nil {
			return result, err
		}
		projectDaily := w.shouldProjectDailyMileage(sample)
		if projectDaily {
			result.ProjectionsAttempted++
		} else {
			result.ProjectionsSkippedThrottled++
		}
		if err := w.writeMileageSample(ctx, sample, candidate, projectDaily); err != nil {
			return result, err
		}
		w.markBaselineWritten(sample, candidate)
		if projectDaily {
			w.markProjected(sample)
			result.ProjectionsWritten++
		}
		w.markMileageWritten(sample)
		result.SamplesWritten++
	}
	return result, nil
}

func (w *Writer) writeMileageSample(ctx context.Context, sample MetricSample, candidate SourceMileageSample, projectDaily bool) error {
	if projectDaily {
		if beginner, ok := w.exec.(txBeginner); ok {
			tx, err := beginner.BeginTx(ctx, nil)
			if err != nil {
				return err
			}
			if err := UpsertSourceMileage(ctx, tx, candidate); err != nil {
				_ = tx.Rollback()
				return err
			}
			if ShouldNormalizePlatformSourceMileage(candidate) {
				if err := NormalizePlatformSourceMileage(ctx, tx, sample.VIN, sample.StatDate, sample.Protocol); err != nil {
					_ = tx.Rollback()
					return err
				}
			}
			if err := projectDailyMileageWithExec(ctx, tx, sample.VIN, sample.StatDate, sample.Protocol); err != nil {
				_ = tx.Rollback()
				return err
			}
			return tx.Commit()
		}
	}
	if err := UpsertSourceMileage(ctx, w.exec, candidate); err != nil {
		return err
	}
	if projectDaily {
		if ShouldNormalizePlatformSourceMileage(candidate) {
			if err := NormalizePlatformSourceMileage(ctx, w.exec, sample.VIN, sample.StatDate, sample.Protocol); err != nil {
				return err
			}
		}
		return ProjectDailyMileage(ctx, w.exec, sample.VIN, sample.StatDate, sample.Protocol)
	}
	return nil
}

func (w *Writer) sourceSeenAt(env envelope.FrameEnvelope) time.Time {
	if env.ReceivedAtMS > 0 {
		return time.UnixMilli(env.ReceivedAtMS).In(w.loc)
	}
	if eventMS, ok := statEventTimeMS(env); ok {
		return time.UnixMilli(eventMS).In(w.loc)
	}
	return time.Now().In(w.loc)
}

func (w *Writer) shouldTouchSource(identity SourceIdentity, seenAt time.Time) bool {
	key := string(identity.Protocol) + "|" + identity.SourceIP
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.sourceTouchInterval == 0 {
		return true
	}
	last, ok := w.lastSourceSeen[key]
	if ok && !seenAt.After(last.Add(w.sourceTouchInterval)) {
		return false
	}
	return true
}

func (w *Writer) markSourceTouched(identity SourceIdentity, seenAt time.Time) {
	key := string(identity.Protocol) + "|" + identity.SourceIP
	w.mu.Lock()
	w.lastSourceSeen[key] = seenAt
	w.enforceCacheLimitsLocked()
	w.mu.Unlock()
}

func (w *Writer) shouldProjectDailyMileage(sample MetricSample) bool {
	key := projectionCacheKey(sample)
	w.mu.Lock()
	defer w.mu.Unlock()
	interval := w.projectionInterval
	if interval == 0 {
		return true
	}
	entry, ok := w.lastProjection[key]
	if !ok {
		return true
	}
	if _, ok := entry.sourceKeys[sample.SourceKey]; !ok {
		return true
	}
	return sample.EventTime.After(entry.projectedAt.Add(interval))
}

func (w *Writer) markProjected(sample MetricSample) {
	key := projectionCacheKey(sample)
	prefix := projectionCachePrefix(sample)
	w.mu.Lock()
	defer w.mu.Unlock()
	w.deleteProjectionPrefixExceptLocked(prefix, key)
	entry := w.lastProjection[key]
	if entry.sourceKeys == nil {
		entry.sourceKeys = map[string]struct{}{}
	}
	entry.sourceKeys[sample.SourceKey] = struct{}{}
	entry.projectedAt = sample.EventTime
	w.lastProjection[key] = entry
	w.addCacheKeyLocked(w.projectionKeysByPrefix, prefix, key)
	w.enforceCacheLimitsLocked()
}

func (w *Writer) seenSameMileage(sample MetricSample) bool {
	key := mileageCacheKey(sample)
	w.mu.Lock()
	defer w.mu.Unlock()
	if last, ok := w.lastTotalMileage[key]; ok && last == sample.TotalMileageKM {
		return true
	}
	return false
}

func (w *Writer) markMileageWritten(sample MetricSample) {
	prefix := mileageCachePrefix(sample)
	key := mileageCacheKey(sample)
	w.mu.Lock()
	defer w.mu.Unlock()
	w.deleteMileagePrefixExceptLocked(prefix, key)
	w.deleteBaselinePrefixExceptLocked(prefix, key)
	w.lastTotalMileage[key] = sample.TotalMileageKM
	w.addCacheKeyLocked(w.mileageKeysByPrefix, prefix, key)
	w.enforceCacheLimitsLocked()
}

func (w *Writer) maybeCleanupCaches(now time.Time) {
	if now.IsZero() {
		now = time.Now().In(w.loc)
	}
	w.mu.Lock()
	defer w.mu.Unlock()
	retention := w.cacheRetention
	if retention <= 0 {
		w.enforceCacheLimitsLocked()
		return
	}
	interval := w.cacheCleanupInterval
	if interval > 0 && !w.lastCacheCleanup.IsZero() && !now.After(w.lastCacheCleanup.Add(interval)) {
		w.enforceCacheLimitsLocked()
		return
	}
	w.lastCacheCleanup = now
	cleanupStats := cacheCleanupStats{}
	cutoff := now.Add(-retention)
	cutoffDate := cutoff.In(w.loc).Format("2006-01-02")
	for key, seenAt := range w.lastSourceSeen {
		if !seenAt.IsZero() && seenAt.Before(cutoff) {
			delete(w.lastSourceSeen, key)
			cleanupStats.sourceSeen++
		}
	}
	for key, entry := range w.lastProjection {
		if entry.projectedAt.IsZero() || entry.projectedAt.Before(cutoff) || cacheKeyDateBefore(key, cutoffDate) {
			w.deleteProjectionKeyLocked(key)
			cleanupStats.projection++
		}
	}
	for key := range w.lastTotalMileage {
		if cacheKeyDateBefore(key, cutoffDate) {
			w.deleteMileageKeyLocked(key)
			cleanupStats.totalMileage++
		}
	}
	for key := range w.baselineCache {
		if cacheKeyDateBefore(key, cutoffDate) {
			w.deleteBaselineKeyLocked(key)
			cleanupStats.baseline++
		}
	}
	w.lastCacheCleanupStats = cleanupStats
	w.enforceCacheLimitsLocked()
}

func (w *Writer) applyRealtimeBaseline(ctx context.Context, candidate *SourceMileageSample) error {
	if candidate == nil {
		return nil
	}
	baseline, found, err := w.previousBaseline(ctx, *candidate)
	if err != nil {
		return err
	}
	if !found {
		// If no earlier odometer exists at all, use the first current-day sample.
		// Later samples retain that boundary through the in-memory baseline cache.
		candidate.DailyKM = 0
		candidate.QualityStatus = QualityOK
		candidate.QualityReason = QualityReasonCurrentDayFirst
		return nil
	}
	candidate.FirstTotalKM = baseline.LatestTotalKM
	candidate.FirstEventTime = baseline.LatestEventTime
	candidate.DailyKM = DailyMileageFromDayBoundary(baseline.LatestTotalKM, candidate.LatestTotalKM)
	candidate.QualityStatus = QualityOK
	candidate.QualityReason = baseline.QualityReason
	if candidate.QualityReason == "" {
		candidate.QualityReason = QualityReasonHistorical
	}
	ApplyMileageQualityRules(candidate)
	return nil
}

func (w *Writer) previousBaseline(ctx context.Context, candidate SourceMileageSample) (sourceBaseline, bool, error) {
	cacheKey := mileageCacheKey(MetricSample{
		VIN:       candidate.VIN,
		Protocol:  candidate.Protocol,
		StatDate:  candidate.StatDate,
		SourceKey: candidate.SourceKey,
	})
	now := time.Now()
	w.mu.Lock()
	if cached, ok := w.baselineCache[cacheKey]; ok {
		missExpired := !cached.found && (w.baselineMissTTL == 0 || cached.cachedAt.IsZero() || now.Sub(cached.cachedAt) >= w.baselineMissTTL)
		if !missExpired {
			w.mu.Unlock()
			return cached.baseline, cached.found, nil
		}
		w.deleteBaselineKeyLocked(cacheKey)
	}
	w.mu.Unlock()

	baseline, found, err := lookupPreviousSourceBaseline(ctx, w.query, candidate.VIN, candidate.StatDate, candidate.Protocol, candidate.SourceKey)
	if err != nil {
		return sourceBaseline{}, false, err
	}

	if found {
		w.cacheBaseline(candidate, cacheKey, sourceBaselineCacheEntry{baseline: baseline, found: true})
	} else {
		w.cacheBaseline(candidate, cacheKey, sourceBaselineCacheEntry{found: false})
	}
	return baseline, found, nil
}

func (w *Writer) markBaselineWritten(sample MetricSample, candidate SourceMileageSample) {
	if candidate.QualityStatus != QualityOK {
		return
	}
	baselineTotal := candidate.FirstTotalKM
	if baselineTotal <= 0 {
		baselineTotal = candidate.LatestTotalKM
	}
	baselineTime := candidate.FirstEventTime
	if baselineTime.IsZero() {
		baselineTime = candidate.LatestEventTime
	}
	if baselineTotal <= 0 || baselineTime.IsZero() {
		return
	}
	cacheKey := mileageCacheKey(sample)
	w.cacheBaseline(candidate, cacheKey, sourceBaselineCacheEntry{
		baseline: sourceBaseline{
			LatestTotalKM:   baselineTotal,
			LatestEventTime: baselineTime,
			QualityReason:   candidate.QualityReason,
		},
		found: true,
	})
}

func (w *Writer) cacheBaseline(candidate SourceMileageSample, cacheKey string, entry sourceBaselineCacheEntry) {
	if cacheKey == "" {
		return
	}
	w.mu.Lock()
	if entry.cachedAt.IsZero() {
		entry.cachedAt = time.Now()
	}
	w.baselineCache[cacheKey] = entry
	w.addCacheKeyLocked(w.baselineKeysByPrefix, mileageCachePrefix(MetricSample{
		VIN:       candidate.VIN,
		Protocol:  candidate.Protocol,
		SourceKey: candidate.SourceKey,
	}), cacheKey)
	w.enforceCacheLimitsLocked()
	w.mu.Unlock()
}

func (w *Writer) addCacheKeyLocked(index map[string]map[string]struct{}, prefix string, key string) {
	if prefix == "" || key == "" {
		return
	}
	keys := index[prefix]
	if keys == nil {
		keys = map[string]struct{}{}
		index[prefix] = keys
	}
	keys[key] = struct{}{}
}

func (w *Writer) deleteMileagePrefixExceptLocked(prefix string, keep string) {
	for key := range w.mileageKeysByPrefix[prefix] {
		if key == keep {
			continue
		}
		w.deleteMileageKeyLocked(key)
	}
}

func (w *Writer) deleteBaselinePrefixExceptLocked(prefix string, keep string) {
	for key := range w.baselineKeysByPrefix[prefix] {
		if key == keep {
			continue
		}
		w.deleteBaselineKeyLocked(key)
	}
}

func (w *Writer) deleteProjectionPrefixExceptLocked(prefix string, keep string) {
	for key := range w.projectionKeysByPrefix[prefix] {
		if key == keep {
			continue
		}
		w.deleteProjectionKeyLocked(key)
	}
}

func (w *Writer) deleteMileageKeyLocked(key string) {
	delete(w.lastTotalMileage, key)
	w.deleteIndexedCacheKeyLocked(w.mileageKeysByPrefix, cacheKeyPrefix(key), key)
}

func (w *Writer) deleteBaselineKeyLocked(key string) {
	delete(w.baselineCache, key)
	w.deleteIndexedCacheKeyLocked(w.baselineKeysByPrefix, cacheKeyPrefix(key), key)
}

func (w *Writer) deleteProjectionKeyLocked(key string) {
	delete(w.lastProjection, key)
	w.deleteIndexedCacheKeyLocked(w.projectionKeysByPrefix, cacheKeyPrefix(key), key)
}

func (w *Writer) deleteIndexedCacheKeyLocked(index map[string]map[string]struct{}, prefix string, key string) {
	if prefix == "" || key == "" {
		return
	}
	keys := index[prefix]
	if len(keys) == 0 {
		return
	}
	delete(keys, key)
	if len(keys) == 0 {
		delete(index, prefix)
	}
}

func (w *Writer) enforceCacheLimitsLocked() {
	if w.maxCacheEntries <= 0 {
		return
	}
	for len(w.lastTotalMileage) > w.maxCacheEntries {
		victim := oldestCacheKeyByDate(w.lastTotalMileage)
		if victim == "" {
			break
		}
		w.deleteMileageKeyLocked(victim)
		w.cacheEvictions.totalMileage++
	}
	for len(w.baselineCache) > w.maxCacheEntries {
		victim := oldestCacheKeyByDate(w.baselineCache)
		if victim == "" {
			break
		}
		w.deleteBaselineKeyLocked(victim)
		w.cacheEvictions.baseline++
	}
	for len(w.lastProjection) > w.maxCacheEntries {
		victim := oldestProjectionCacheKey(w.lastProjection)
		if victim == "" {
			break
		}
		w.deleteProjectionKeyLocked(victim)
		w.cacheEvictions.projection++
	}
	for len(w.lastSourceSeen) > w.maxCacheEntries {
		victim := oldestSourceSeenKey(w.lastSourceSeen)
		if victim == "" {
			break
		}
		delete(w.lastSourceSeen, victim)
		w.cacheEvictions.sourceSeen++
	}
}

func mileageCachePrefix(sample MetricSample) string {
	return fmt.Sprintf("%s|%s|%s|", sample.VIN, sample.Protocol, sample.SourceKey)
}

func mileageCacheKey(sample MetricSample) string {
	return mileageCachePrefix(sample) + sample.StatDate
}

func projectionCacheKey(sample MetricSample) string {
	return projectionCachePrefix(sample) + sample.StatDate
}

func projectionCachePrefix(sample MetricSample) string {
	return fmt.Sprintf("%s|%s|", sample.VIN, sample.Protocol)
}

func cacheKeyDateBefore(key string, cutoffDate string) bool {
	date := cacheKeyDate(key)
	if len(date) != len("2006-01-02") {
		return false
	}
	return date < cutoffDate
}

func cacheKeyPrefix(key string) string {
	index := strings.LastIndex(key, "|")
	if index < 0 {
		return ""
	}
	return key[:index+1]
}

func cacheKeyDate(key string) string {
	index := strings.LastIndex(key, "|")
	if index < 0 || index == len(key)-1 {
		return ""
	}
	return key[index+1:]
}

func oldestCacheKeyByDate[T any](items map[string]T) string {
	victim := ""
	victimDate := ""
	for key := range items {
		date := cacheKeyDate(key)
		if date == "" {
			date = "9999-99-99"
		}
		if victim == "" || date < victimDate || (date == victimDate && key < victim) {
			victim = key
			victimDate = date
		}
	}
	return victim
}

func oldestProjectionCacheKey(items map[string]projectionCacheEntry) string {
	victim := ""
	var victimTime time.Time
	for key, entry := range items {
		if victim == "" ||
			(!entry.projectedAt.IsZero() && (victimTime.IsZero() || entry.projectedAt.Before(victimTime))) ||
			(entry.projectedAt.Equal(victimTime) && key < victim) {
			victim = key
			victimTime = entry.projectedAt
		}
	}
	if victim != "" {
		return victim
	}
	return oldestCacheKeyByDate(items)
}

func oldestSourceSeenKey(items map[string]time.Time) string {
	victim := ""
	var victimTime time.Time
	for key, seenAt := range items {
		if victim == "" ||
			(!seenAt.IsZero() && (victimTime.IsZero() || seenAt.Before(victimTime))) ||
			(seenAt.Equal(victimTime) && key < victim) {
			victim = key
			victimTime = seenAt
		}
	}
	return victim
}

type projectionCacheEntry struct {
	projectedAt time.Time
	sourceKeys  map[string]struct{}
}

type sourceBaselineCacheEntry struct {
	baseline sourceBaseline
	found    bool
	cachedAt time.Time
}

func SamplesFromEnvelope(env envelope.FrameEnvelope, loc *time.Location) ([]MetricSample, error) {
	samples, _, err := samplesFromEnvelopeWithResult(env, loc)
	return samples, err
}

func samplesFromEnvelopeWithResult(env envelope.FrameEnvelope, loc *time.Location) ([]MetricSample, AppendResult, error) {
	var result AppendResult
	if len(env.Fields) == 0 {
		result.SamplesSkippedMissingFields = 1
		return nil, result, nil
	}
	vin := strings.TrimSpace(env.VIN)
	if vin == "" {
		result.SamplesSkippedMissingVIN = 1
		return nil, result, nil
	}
	totalMileage, ok := totalMileageKMFromEnvelope(env)
	if !ok {
		if isMileageCandidateEnvelope(env) {
			result.SamplesSkippedMissingMileage = 1
		} else {
			result.SamplesSkippedNonMileageFrame = 1
		}
		return nil, result, nil
	}
	if totalMileage <= 0 {
		result.SamplesSkippedNonPositiveMileage = 1
		return nil, result, nil
	}
	if loc == nil {
		loc = time.FixedZone("Asia/Shanghai", 8*3600)
	}
	eventMS, reason, ok := envelope.NormalizedEventTimeMSWithReason(env)
	if !ok {
		result.SamplesSkippedMissingTime = 1
		return nil, result, nil
	}
	if reason == envelope.EventTimeReasonReceivedFutureEvent {
		result.SamplesAdjustedFutureEventTime = 1
	}
	statDate := time.UnixMilli(eventMS).In(loc).Format("2006-01-02")
	result.SamplesFound = 1
	return []MetricSample{{
		VIN:            vin,
		Protocol:       env.Protocol,
		StatDate:       statDate,
		TotalMileageKM: totalMileage,
		EventTime:      time.UnixMilli(eventMS).In(loc),
		SourceKey:      sourceKey(env),
		Phone:          strings.TrimSpace(env.Phone),
		DeviceID:       strings.TrimSpace(env.DeviceID),
		SourceEndpoint: strings.TrimSpace(env.SourceEndpoint),
		PlatformName:   strings.TrimSpace(env.PlatformName),
	}}, result, nil
}

func statEventTimeMS(env envelope.FrameEnvelope) (int64, bool) {
	return envelope.NormalizedEventTimeMS(env)
}

func sourceKey(env envelope.FrameEnvelope) string {
	return SourceKeyForSource(env.Protocol, env.Phone, env.DeviceID, NormalizeSourceIP(env.SourceEndpoint), env.SourceKind, env.SourceCode)
}

func isIgnorableSchemaChangeError(err error) bool {
	if err == nil {
		return false
	}
	text := strings.ToLower(err.Error())
	return strings.Contains(text, "duplicate column") ||
		strings.Contains(text, "1060") ||
		strings.Contains(text, "duplicate key name") ||
		strings.Contains(text, "1061") ||
		strings.Contains(text, "can't drop") ||
		strings.Contains(text, "1091")
}

type mileageFieldMapping = telemetry.MileageFieldMapping

func totalMileageKMFromEnvelope(env envelope.FrameEnvelope) (float64, bool) {
	return telemetry.TotalMileageKM(env.Protocol, env.Fields)
}

func mileageMappingsByProtocol(protocol envelope.Protocol) []mileageFieldMapping {
	return telemetry.MileageFieldMappings(protocol)
}

func isMileageCandidateEnvelope(env envelope.FrameEnvelope) bool {
	switch env.Protocol {
	case envelope.ProtocolJT808:
		messageID := strings.TrimSpace(env.MessageID)
		return strings.EqualFold(messageID, "0x0200") ||
			hasFieldPrefix(env.Fields, "jt808.location.") ||
			hasAnyField(env.Fields, envelope.FieldLatitude, envelope.FieldLongitude, envelope.FieldSpeedKMH)
	case envelope.ProtocolGB32960:
		return hasFieldPrefix(env.Fields, "gb32960.vehicle.") ||
			hasAnyField(env.Fields, envelope.FieldLatitude, envelope.FieldLongitude, envelope.FieldSpeedKMH, envelope.FieldSOCPercent)
	case envelope.ProtocolYutongMQTT:
		return hasFieldPrefix(env.Fields, "yutong_mqtt.data.") ||
			hasFieldPrefix(env.Fields, "yutong_mqtt.root.data.") ||
			hasAnyField(env.Fields, envelope.FieldLatitude, envelope.FieldLongitude, envelope.FieldSpeedKMH, envelope.FieldSOCPercent)
	default:
		return true
	}
}

func hasFieldPrefix(fields map[string]any, prefix string) bool {
	if len(fields) == 0 || prefix == "" {
		return false
	}
	for key := range fields {
		if strings.HasPrefix(key, prefix) {
			return true
		}
	}
	return false
}

func hasAnyField(fields map[string]any, keys ...string) bool {
	if len(fields) == 0 {
		return false
	}
	for _, key := range keys {
		if _, ok := fields[key]; ok {
			return true
		}
	}
	return false
}

func floatField(env envelope.FrameEnvelope, key string) (float64, bool) {
	if env.Fields == nil {
		return 0, false
	}
	value, ok := env.Fields[key]
	if !ok || value == nil {
		return 0, false
	}
	switch typed := value.(type) {
	case float64:
		return typed, true
	case float32:
		return float64(typed), true
	case int:
		return float64(typed), true
	case int64:
		return float64(typed), true
	case uint16:
		return float64(typed), true
	case uint32:
		return float64(typed), true
	case string:
		parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
		return parsed, err == nil
	default:
		return 0, false
	}
}
