package realtime

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"math"
	"net"
	"strings"
	"sync"
	"time"

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

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

type PlateResolver interface {
	PlateByVIN(context.Context, string) (string, error)
}

type CachedPlateResolver struct {
	delegate   PlateResolver
	ttl        time.Duration
	maxEntries int
	now        func() time.Time
	observe    func(PlateCacheStats)
	mu         sync.Mutex
	entries    map[string]cachedPlateEntry
	evictions  int
}

type PlateCacheStats struct {
	Entries    int
	MaxEntries int
	Evictions  int
}

type cachedPlateEntry struct {
	plate     string
	notFound  bool
	expiresAt time.Time
}

func NewCachedPlateResolver(delegate PlateResolver, ttl time.Duration) *CachedPlateResolver {
	return NewCachedPlateResolverWithMaxEntries(delegate, ttl, 200000)
}

func NewCachedPlateResolverWithMaxEntries(delegate PlateResolver, ttl time.Duration, maxEntries int) *CachedPlateResolver {
	if delegate == nil {
		panic("cached plate resolver delegate must not be nil")
	}
	if ttl <= 0 {
		ttl = 10 * time.Minute
	}
	if maxEntries < 0 {
		maxEntries = 0
	}
	return &CachedPlateResolver{
		delegate:   delegate,
		ttl:        ttl,
		maxEntries: maxEntries,
		now:        time.Now,
		entries:    map[string]cachedPlateEntry{},
	}
}

func (r *CachedPlateResolver) SetStatsObserver(observe func(PlateCacheStats)) {
	r.mu.Lock()
	r.observe = observe
	r.mu.Unlock()
}

func (r *CachedPlateResolver) CacheStats() PlateCacheStats {
	r.mu.Lock()
	defer r.mu.Unlock()
	return PlateCacheStats{Entries: len(r.entries), MaxEntries: r.maxEntries, Evictions: r.evictions}
}

func (r *CachedPlateResolver) PlateByVIN(ctx context.Context, vin string) (string, error) {
	vin = strings.TrimSpace(vin)
	if vin == "" {
		return "", sql.ErrNoRows
	}
	now := r.now()
	r.mu.Lock()
	entry, ok := r.entries[vin]
	if ok && now.Before(entry.expiresAt) {
		r.mu.Unlock()
		if entry.notFound {
			return "", sql.ErrNoRows
		}
		return entry.plate, nil
	}
	r.mu.Unlock()

	plate, err := r.delegate.PlateByVIN(ctx, vin)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return "", err
	}
	r.mu.Lock()
	r.entries[vin] = cachedPlateEntry{
		plate:     strings.TrimSpace(plate),
		notFound:  errors.Is(err, sql.ErrNoRows),
		expiresAt: now.Add(r.ttl),
	}
	r.enforceLimitLocked(now)
	stats := PlateCacheStats{Entries: len(r.entries), MaxEntries: r.maxEntries, Evictions: r.evictions}
	observe := r.observe
	r.mu.Unlock()
	if observe != nil {
		observe(stats)
	}
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(plate), nil
}

func (r *CachedPlateResolver) enforceLimitLocked(now time.Time) {
	if r.maxEntries <= 0 || len(r.entries) <= r.maxEntries {
		return
	}
	for vin, entry := range r.entries {
		if now.After(entry.expiresAt) || now.Equal(entry.expiresAt) {
			delete(r.entries, vin)
			r.evictions++
		}
	}
	for len(r.entries) > r.maxEntries {
		victim := ""
		var victimExpiresAt time.Time
		for vin, entry := range r.entries {
			if victim == "" || entry.expiresAt.Before(victimExpiresAt) {
				victim = vin
				victimExpiresAt = entry.expiresAt
			}
		}
		if victim == "" {
			return
		}
		delete(r.entries, victim)
		r.evictions++
	}
}

type SnapshotWriter struct {
	exec          SnapshotExecer
	plateResolver PlateResolver
}

func NewSnapshotWriter(exec SnapshotExecer) *SnapshotWriter {
	return NewSnapshotWriterWithPlateResolver(exec, nil)
}

func NewSnapshotWriterWithPlateResolver(exec SnapshotExecer, plateResolver PlateResolver) *SnapshotWriter {
	if exec == nil {
		panic("snapshot execer must not be nil")
	}
	return &SnapshotWriter{exec: exec, plateResolver: plateResolver}
}

func (w *SnapshotWriter) EnsureSchema(ctx context.Context) error {
	if _, err := w.exec.ExecContext(ctx, realtimeSnapshotTableSQL); err != nil {
		return err
	}
	for _, statement := range realtimeSnapshotCompatibilitySQL {
		if _, err := w.exec.ExecContext(ctx, statement); err != nil && !isDuplicateColumnError(err) {
			return err
		}
	}
	if _, err := w.exec.ExecContext(ctx, realtimeLocationTableSQL); err != nil {
		return err
	}
	for _, statement := range realtimeLocationCompatibilitySQL {
		if _, err := w.exec.ExecContext(ctx, statement); err != nil && !isDuplicateColumnError(err) {
			return err
		}
	}
	if _, err := w.exec.ExecContext(ctx, jt808RealtimeLocationSourceTableSQL); err != nil {
		return err
	}
	if _, err := w.exec.ExecContext(ctx, vehicleLocationSourcePolicyTableSQL); err != nil {
		return err
	}
	for _, statement := range cleanupInvalidRealtimeTotalMileageSQL {
		if _, err := w.exec.ExecContext(ctx, statement); err != nil {
			return err
		}
	}
	if _, err := w.exec.ExecContext(ctx, backfillRealtimeAccessProjectionSQL); err != nil {
		return err
	}
	return nil
}

func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope) error {
	if !envelope.IsRealtimeTelemetryFrame(env) {
		return nil
	}
	vin := strings.TrimSpace(env.VIN)
	if vin == "" {
		return nil
	}
	if !isRealtimeSnapshotEvent(env) {
		return nil
	}
	plate, err := w.plateForEnvelope(ctx, env)
	if err != nil {
		return err
	}
	eventTimeMS, _ := envelope.NormalizedEventTimeMS(env)
	eventTime := nullableTime(eventTimeMS)
	receivedAt := nullableTime(env.ReceivedAtMS)
	platformName := platformNameFromEnvelope(env)
	parsed := snapshotFieldsForEnvelope(env)
	if _, err = w.exec.ExecContext(ctx, upsertRealtimeSnapshotSQL,
		string(env.Protocol),
		vin,
		plate,
		platformName,
		env.SourceEndpoint,
		marshalParsedJSON(parsed),
		eventTime,
		receivedAt,
		env.StableEventID(),
		receivedAt,
		receivedAt,
		env.StableEventID(),
	); err != nil {
		return err
	}
	location, ok := realtimeLocationFromEnvelope(env, vin, plate)
	if !ok {
		return nil
	}
	locationArgs := []any{
		location.Protocol,
		location.VIN,
		location.Plate,
		location.EventTime,
		location.Latitude,
		location.Longitude,
		location.SpeedKMH,
		location.TotalMileageKM,
		location.TotalMileageAt,
		location.SOCPercent,
		location.AltitudeM,
		location.DirectionDeg,
		location.AlarmFlag,
		location.StatusFlag,
		location.ReceivedAt,
		location.EventID,
	}
	if env.Protocol != envelope.ProtocolJT808 {
		_, err = w.exec.ExecContext(ctx, upsertRealtimeLocationSQL, locationArgs...)
		return err
	}
	sourceKey := realtimeLocationSourceKey(env, vin)
	sourceArgs := append([]any{sourceKey, strings.TrimSpace(env.Phone), strings.TrimSpace(env.DeviceID), strings.TrimSpace(env.SourceCode), normalizedSourceKind(env.SourceKind), strings.TrimSpace(env.SourceEndpoint)}, locationArgs...)
	sourceArgs = append(sourceArgs, location.ReceivedAt)
	if _, err = w.exec.ExecContext(ctx, upsertJT808RealtimeLocationSourceSQL, sourceArgs...); err != nil {
		return err
	}
	_, err = w.exec.ExecContext(ctx, electJT808RealtimeLocationSQL, vin)
	return err
}

func realtimeLocationSourceKey(env envelope.FrameEnvelope, vin string) string {
	identity := strings.TrimSpace(env.Phone)
	if identity == "" {
		identity = strings.TrimSpace(env.DeviceID)
	}
	if identity == "" {
		identity = strings.TrimSpace(vin)
	}
	scope := strings.TrimSpace(env.SourceCode)
	if scope == "" {
		scope = normalizedSourceKind(env.SourceKind)
	}
	if scope == "" || scope == "UNKNOWN" {
		scope = sourceHost(env.SourceEndpoint)
	}
	if scope == "" {
		scope = "unknown"
	}
	return string(env.Protocol) + ":" + identity + "@" + scope
}

func normalizedSourceKind(value string) string {
	value = strings.ToUpper(strings.TrimSpace(value))
	switch value {
	case "DIRECT", "PLATFORM":
		return value
	default:
		return "UNKNOWN"
	}
}

func sourceHost(endpoint string) string {
	endpoint = strings.TrimSpace(endpoint)
	if endpoint == "" {
		return ""
	}
	if host, _, err := net.SplitHostPort(endpoint); err == nil {
		return strings.TrimSpace(host)
	}
	if index := strings.LastIndex(endpoint, ":"); index > 0 && strings.Count(endpoint, ":") == 1 {
		return endpoint[:index]
	}
	return endpoint
}

func snapshotFieldsForEnvelope(env envelope.FrameEnvelope) map[string]any {
	if len(env.ParsedFields) == 0 {
		return nil
	}
	return realtimeSnapshotFlatFields(env)
}

func realtimeSnapshotFlatFields(env envelope.FrameEnvelope) map[string]any {
	fields := cloneMap(env.ParsedFields)
	filterInvalidRealtimeMeasurementFields(fields)
	if len(fields) == 0 {
		return nil
	}
	return fields
}

func platformNameFromEnvelope(env envelope.FrameEnvelope) string {
	if value := strings.TrimSpace(env.PlatformName); value != "" {
		return value
	}
	if env.Parsed != nil {
		if value := strings.TrimSpace(stringValue(env.Parsed["platform_name"])); value != "" {
			return value
		}
		if login, ok := env.Parsed["platform_login"].(map[string]any); ok {
			return strings.TrimSpace(stringValue(login["username"]))
		}
	}
	return ""
}

func stringValue(value any) string {
	switch typed := value.(type) {
	case string:
		return typed
	default:
		return ""
	}
}

func marshalParsedJSON(parsed map[string]any) any {
	if len(parsed) == 0 {
		return nil
	}
	data, err := json.Marshal(parsed)
	if err != nil {
		return nil
	}
	return string(data)
}

func (w *SnapshotWriter) plateForEnvelope(ctx context.Context, env envelope.FrameEnvelope) (string, error) {
	if plate := strings.TrimSpace(env.Plate); plate != "" {
		return plate, nil
	}
	if w.plateResolver == nil {
		return "", nil
	}
	vin := strings.TrimSpace(env.VIN)
	if vin == "" {
		return "", nil
	}
	plate, err := w.plateResolver.PlateByVIN(ctx, vin)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return "", nil
		}
		return "", err
	}
	return strings.TrimSpace(plate), nil
}

type realtimeLocationRow struct {
	Protocol       string
	VIN            string
	Plate          string
	EventTime      any
	Latitude       float64
	Longitude      float64
	SpeedKMH       any
	TotalMileageKM any
	TotalMileageAt any
	SOCPercent     any
	AltitudeM      any
	DirectionDeg   any
	AlarmFlag      any
	StatusFlag     any
	ReceivedAt     any
	EventID        string
}

func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vin string, plate string) (realtimeLocationRow, bool) {
	location, ok := telemetry.LocationProjectionForProtocol(env.Protocol, env.ParsedFields)
	if !ok {
		return realtimeLocationRow{}, false
	}
	if math.IsNaN(location.Latitude) || math.IsNaN(location.Longitude) || math.IsInf(location.Latitude, 0) || math.IsInf(location.Longitude, 0) ||
		location.Latitude < -90 || location.Latitude > 90 || location.Longitude < -180 || location.Longitude > 180 ||
		(math.Abs(location.Latitude) < 0.000001 && math.Abs(location.Longitude) < 0.000001) {
		return realtimeLocationRow{}, false
	}
	eventTimeMS, _ := envelope.NormalizedEventTimeMS(env)
	totalMileageKM, hasTotalMileage := telemetry.TotalMileageKM(env.Protocol, env.ParsedFields)
	if totalMileageKM <= 0 {
		hasTotalMileage = false
	}
	var totalMileageValue any
	var totalMileageAt any
	if hasTotalMileage {
		totalMileageValue = totalMileageKM
		totalMileageAt = nullableTime(eventTimeMS)
	}
	return realtimeLocationRow{
		Protocol:       string(env.Protocol),
		VIN:            vin,
		Plate:          plate,
		EventTime:      nullableTime(eventTimeMS),
		Latitude:       location.Latitude,
		Longitude:      location.Longitude,
		SpeedKMH:       optionalFloatValue(location.SpeedKMH),
		TotalMileageKM: totalMileageValue,
		TotalMileageAt: totalMileageAt,
		SOCPercent:     optionalFloatValue(location.SOCPercent),
		AltitudeM:      optionalFloatValue(location.AltitudeM),
		DirectionDeg:   optionalFloatValue(location.DirectionDeg),
		AlarmFlag:      optionalInt64Value(location.AlarmFlag),
		StatusFlag:     optionalInt64Value(location.StatusFlag),
		ReceivedAt:     nullableTime(env.ReceivedAtMS),
		EventID:        env.StableEventID(),
	}, true
}

func optionalFloatValue(value *float64) any {
	if value == nil {
		return nil
	}
	return *value
}

func optionalInt64Value(value *int64) any {
	if value == nil {
		return nil
	}
	return *value
}

type BindingPlateResolver struct {
	queryer Queryer
	table   string
}

type Queryer interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

func NewBindingPlateResolver(queryer Queryer, table string) *BindingPlateResolver {
	if queryer == nil {
		panic("plate binding queryer must not be nil")
	}
	table = strings.TrimSpace(table)
	if table == "" || !safeIdentifier(table) {
		table = "vehicle_identity_binding"
	}
	return &BindingPlateResolver{queryer: queryer, table: table}
}

func (r *BindingPlateResolver) PlateByVIN(ctx context.Context, vin string) (string, error) {
	vin = strings.TrimSpace(vin)
	if vin == "" {
		return "", sql.ErrNoRows
	}
	query := "SELECT plate FROM " + r.table + " WHERE vin = ? AND plate IS NOT NULL AND plate <> ''"
	var plate string
	err := r.queryer.QueryRowContext(ctx, query, vin).Scan(&plate)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(plate), nil
}

func safeIdentifier(value string) bool {
	if value == "" {
		return false
	}
	for _, r := range value {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
			continue
		}
		return false
	}
	return true
}

func nullableTime(ms int64) any {
	if ms <= 0 {
		return nil
	}
	return time.UnixMilli(ms)
}

func isRealtimeSnapshotEvent(env envelope.FrameEnvelope) bool {
	switch env.Protocol {
	case envelope.ProtocolGB32960:
		return (env.MessageID == "0x02" || env.MessageID == "0x03") && telemetry.HasRealtimeFields(env.Protocol, env.ParsedFields)
	case envelope.ProtocolJT808:
		return env.MessageID == "0x0200" && hasLocationFields(env)
	case envelope.ProtocolYutongMQTT:
		return telemetry.HasRealtimeFields(env.Protocol, env.ParsedFields)
	default:
		return false
	}
}

func hasLocationFields(env envelope.FrameEnvelope) bool {
	_, ok := telemetry.LocationProjectionForProtocol(env.Protocol, env.ParsedFields)
	return ok
}

const realtimeSnapshotTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_snapshot (
  protocol VARCHAR(32) NOT NULL,
  vin VARCHAR(32) NOT NULL DEFAULT '',
  plate VARCHAR(32) NOT NULL DEFAULT '',
  platform_name VARCHAR(64) NOT NULL DEFAULT '',
  peer VARCHAR(128) NOT NULL DEFAULT '',
  parsed_json LONGTEXT NULL,
  event_time DATETIME(3) NULL,
  received_at DATETIME(3) NULL,
  event_id VARCHAR(64) NOT NULL DEFAULT '',
  access_first_seen_at DATETIME(3) NULL,
  access_previous_received_at DATETIME(3) NULL,
  access_latest_received_at DATETIME(3) NULL,
  access_report_interval_ms BIGINT UNSIGNED NULL,
  access_sample_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
  access_latest_event_id VARCHAR(64) NOT NULL DEFAULT '',
  access_first_seen_source VARCHAR(32) NOT NULL DEFAULT '',
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (protocol, vin),
  KEY idx_vin (vin),
  KEY idx_protocol_updated (protocol, updated_at),
  KEY idx_access_latest (access_latest_received_at, vin)
)`

var realtimeSnapshotCompatibilitySQL = []string{
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN platform_name VARCHAR(64) NOT NULL DEFAULT '' AFTER plate",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN peer VARCHAR(128) NOT NULL DEFAULT '' AFTER platform_name",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN parsed_json LONGTEXT NULL AFTER peer",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_first_seen_at DATETIME(3) NULL AFTER event_id",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_previous_received_at DATETIME(3) NULL AFTER access_first_seen_at",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_latest_received_at DATETIME(3) NULL AFTER access_previous_received_at",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_report_interval_ms BIGINT UNSIGNED NULL AFTER access_latest_received_at",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_sample_count BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER access_report_interval_ms",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_latest_event_id VARCHAR(64) NOT NULL DEFAULT '' AFTER access_sample_count",
	"ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_first_seen_source VARCHAR(32) NOT NULL DEFAULT '' AFTER access_latest_event_id",
}

func isDuplicateColumnError(err error) bool {
	text := strings.ToLower(err.Error())
	return strings.Contains(text, "duplicate column") || strings.Contains(text, "1060")
}

const upsertRealtimeSnapshotSQL = `
INSERT INTO vehicle_realtime_snapshot
  (protocol, vin, plate, platform_name, peer, parsed_json, event_time, received_at, event_id,
   access_first_seen_at, access_latest_received_at, access_sample_count, access_latest_event_id, access_first_seen_source)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 'live_writer')
ON DUPLICATE KEY UPDATE
  plate = IF(VALUES(plate) <> '', VALUES(plate), plate),
  platform_name = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(platform_name), platform_name),
  peer = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(peer), peer),
  parsed_json = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time),
    IF(VALUES(parsed_json) IS NULL OR VALUES(parsed_json) = '', parsed_json, JSON_MERGE_PATCH(COALESCE(NULLIF(vehicle_realtime_snapshot.parsed_json, ''), JSON_OBJECT()), VALUES(parsed_json))),
    parsed_json),
  event_time = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(event_time), event_time),
  received_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(received_at), received_at),
  event_id = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(event_id), event_id),
  updated_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), CURRENT_TIMESTAMP, updated_at),
  access_previous_received_at = IF(VALUES(access_latest_received_at) IS NOT NULL AND (vehicle_realtime_snapshot.access_latest_received_at IS NULL OR VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at) AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, vehicle_realtime_snapshot.access_latest_received_at, access_previous_received_at),
  access_report_interval_ms = IF(VALUES(access_latest_received_at) IS NOT NULL AND vehicle_realtime_snapshot.access_latest_received_at IS NOT NULL AND VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, TIMESTAMPDIFF(MICROSECOND, vehicle_realtime_snapshot.access_latest_received_at, VALUES(access_latest_received_at)) DIV 1000, access_report_interval_ms),
  access_sample_count = IF(VALUES(access_latest_received_at) IS NOT NULL AND (vehicle_realtime_snapshot.access_latest_received_at IS NULL OR VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at) AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, access_sample_count + 1, access_sample_count),
  access_latest_received_at = IF(VALUES(access_latest_received_at) IS NOT NULL AND (vehicle_realtime_snapshot.access_latest_received_at IS NULL OR VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at) AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, VALUES(access_latest_received_at), access_latest_received_at),
  access_latest_event_id = IF(VALUES(access_latest_received_at) IS NOT NULL AND VALUES(access_latest_received_at) = vehicle_realtime_snapshot.access_latest_received_at AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, VALUES(access_latest_event_id), access_latest_event_id)
`

const realtimeLocationTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_location (
  protocol VARCHAR(32) NOT NULL,
  vin VARCHAR(32) NOT NULL DEFAULT '',
  plate VARCHAR(32) NOT NULL DEFAULT '',
  event_time DATETIME(3) NULL,
  latitude DECIMAL(12,6) NOT NULL,
  longitude DECIMAL(12,6) NOT NULL,
  speed_kmh DECIMAL(10,3) NULL,
  total_mileage_km DECIMAL(18,3) NULL,
  total_mileage_event_time DATETIME(3) NULL,
  soc_percent DECIMAL(6,2) NULL,
  altitude_m DECIMAL(10,3) NULL,
  direction_deg DECIMAL(10,3) NULL,
  alarm_flag BIGINT NULL,
  status_flag BIGINT NULL,
  received_at DATETIME(3) NULL,
  event_id VARCHAR(64) NOT NULL DEFAULT '',
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (protocol, vin),
  KEY idx_vin (vin),
  KEY idx_protocol_updated (protocol, updated_at)
)`

var realtimeLocationCompatibilitySQL = []string{
	"ALTER TABLE vehicle_realtime_location ADD COLUMN total_mileage_event_time DATETIME(3) NULL AFTER total_mileage_km",
	"ALTER TABLE vehicle_realtime_location ADD COLUMN source_key VARCHAR(256) NOT NULL DEFAULT '' AFTER plate",
	"ALTER TABLE vehicle_realtime_location ADD COLUMN location_conflict TINYINT(1) NOT NULL DEFAULT 0 AFTER status_flag",
	"ALTER TABLE vehicle_realtime_location ADD COLUMN location_conflict_distance_m DECIMAL(12,1) NULL AFTER location_conflict",
}

const jt808RealtimeLocationSourceTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_location_source (
  protocol VARCHAR(32) NOT NULL,
  vin VARCHAR(32) NOT NULL,
  source_key VARCHAR(256) NOT NULL,
  phone VARCHAR(32) NOT NULL DEFAULT '',
  device_id VARCHAR(64) NOT NULL DEFAULT '',
  source_code VARCHAR(64) NOT NULL DEFAULT '',
  source_kind VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN',
  source_endpoint VARCHAR(128) NOT NULL DEFAULT '',
  plate VARCHAR(32) NOT NULL DEFAULT '',
  event_time DATETIME(3) NULL,
  latitude DECIMAL(12,6) NOT NULL,
  longitude DECIMAL(12,6) NOT NULL,
  speed_kmh DECIMAL(10,3) NULL,
  total_mileage_km DECIMAL(18,3) NULL,
  total_mileage_event_time DATETIME(3) NULL,
  soc_percent DECIMAL(6,2) NULL,
  altitude_m DECIMAL(10,3) NULL,
  direction_deg DECIMAL(10,3) NULL,
  alarm_flag BIGINT NULL,
  status_flag BIGINT NULL,
  received_at DATETIME(3) NULL,
  event_id VARCHAR(64) NOT NULL DEFAULT '',
  quality_status VARCHAR(32) NOT NULL DEFAULT 'OK',
  quality_reason VARCHAR(64) NOT NULL DEFAULT '',
  consecutive_good_samples INT NOT NULL DEFAULT 1,
  latest_sample_received_at DATETIME(3) NULL,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (protocol, vin, source_key),
  KEY idx_location_source_vehicle_fresh (vin, protocol, received_at),
  KEY idx_location_source_quality (quality_status, updated_at)
)`

const vehicleLocationSourcePolicyTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_location_source_policy (
  vin VARCHAR(32) NOT NULL,
  protocol VARCHAR(32) NOT NULL,
  source_key VARCHAR(256) NOT NULL,
  enabled TINYINT(1) NOT NULL DEFAULT 1,
  priority INT NOT NULL DEFAULT 100,
  remark VARCHAR(255) NOT NULL DEFAULT '',
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (vin, protocol, source_key),
  KEY idx_location_policy_enabled (vin, protocol, enabled, priority)
)`

const jt808AcceptedLocationCondition = `VALUES(event_time) IS NOT NULL
    AND (vehicle_realtime_location_source.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location_source.event_time)
	AND (vehicle_realtime_location_source.event_time IS NULL
			OR ST_Distance_Sphere(
				POINT(vehicle_realtime_location_source.longitude, vehicle_realtime_location_source.latitude),
				POINT(VALUES(longitude), VALUES(latitude))
			) <= GREATEST(500, GREATEST(1, TIMESTAMPDIFF(SECOND, vehicle_realtime_location_source.event_time, VALUES(event_time))) * 70))`

var upsertJT808RealtimeLocationSourceSQL = `
INSERT INTO vehicle_realtime_location_source
  (source_key, phone, device_id, source_code, source_kind, source_endpoint,
   protocol, vin, plate, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time,
   soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, received_at, event_id, latest_sample_received_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
	quality_status = IF(` + jt808AcceptedLocationCondition + `, 'OK', 'REJECTED'),
	quality_reason = IF(quality_status = 'OK', '', 'impossible_jump'),
	consecutive_good_samples = IF(quality_status = 'OK', IF(VALUES(event_id) <> event_id, consecutive_good_samples + 1, consecutive_good_samples), 0),
  latest_sample_received_at = VALUES(latest_sample_received_at),
  phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
  device_id = IF(VALUES(device_id) <> '', VALUES(device_id), device_id),
  source_code = IF(VALUES(source_code) <> '', VALUES(source_code), source_code),
  source_kind = IF(VALUES(source_kind) <> 'UNKNOWN', VALUES(source_kind), source_kind),
  source_endpoint = IF(VALUES(source_endpoint) <> '', VALUES(source_endpoint), source_endpoint),
  plate = IF(VALUES(plate) <> '', VALUES(plate), plate),
  event_time = IF(quality_status = 'OK', VALUES(event_time), event_time),
  latitude = IF(quality_status = 'OK', VALUES(latitude), latitude),
  longitude = IF(quality_status = 'OK', VALUES(longitude), longitude),
  speed_kmh = IF(quality_status = 'OK', COALESCE(VALUES(speed_kmh), speed_kmh), speed_kmh),
  total_mileage_km = IF(quality_status = 'OK', COALESCE(VALUES(total_mileage_km), total_mileage_km), total_mileage_km),
  total_mileage_event_time = IF(quality_status = 'OK' AND VALUES(total_mileage_km) IS NOT NULL, COALESCE(VALUES(total_mileage_event_time), total_mileage_event_time), total_mileage_event_time),
  soc_percent = IF(quality_status = 'OK', COALESCE(VALUES(soc_percent), soc_percent), soc_percent),
  altitude_m = IF(quality_status = 'OK', COALESCE(VALUES(altitude_m), altitude_m), altitude_m),
  direction_deg = IF(quality_status = 'OK', COALESCE(VALUES(direction_deg), direction_deg), direction_deg),
  alarm_flag = IF(quality_status = 'OK', COALESCE(VALUES(alarm_flag), alarm_flag), alarm_flag),
  status_flag = IF(quality_status = 'OK', COALESCE(VALUES(status_flag), status_flag), status_flag),
  received_at = IF(quality_status = 'OK', VALUES(received_at), received_at),
  event_id = IF(quality_status = 'OK', VALUES(event_id), event_id)
`

const electJT808RealtimeLocationSQL = `
INSERT INTO vehicle_realtime_location
  (protocol, vin, plate, source_key, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time,
   soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, location_conflict, location_conflict_distance_m,
   received_at, event_id, updated_at)
SELECT s.protocol, s.vin, s.plate, s.source_key, s.event_time, s.latitude, s.longitude, s.speed_kmh,
       s.total_mileage_km, s.total_mileage_event_time, s.soc_percent, s.altitude_m, s.direction_deg,
       s.alarm_flag, s.status_flag,
       EXISTS(SELECT 1 FROM vehicle_realtime_location_source other
              WHERE other.vin = s.vin AND other.protocol = s.protocol AND other.source_key <> s.source_key
                AND other.quality_status = 'OK' AND other.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
                AND ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(other.longitude, other.latitude)) > 200),
       (SELECT MAX(ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(other.longitude, other.latitude)))
          FROM vehicle_realtime_location_source other
         WHERE other.vin = s.vin AND other.protocol = s.protocol AND other.source_key <> s.source_key
           AND other.quality_status = 'OK' AND other.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)),
       s.received_at, s.event_id, COALESCE(s.received_at, s.event_time, CURRENT_TIMESTAMP)
FROM vehicle_realtime_location_source s
LEFT JOIN vehicle_location_source_policy policy
  ON policy.vin = s.vin AND policy.protocol = s.protocol AND policy.source_key = s.source_key
LEFT JOIN vehicle_realtime_location cur
	ON cur.vin = s.vin AND cur.protocol = s.protocol
WHERE s.vin = ? AND s.protocol = 'JT808' AND s.quality_status = 'OK' AND COALESCE(policy.enabled, 1) = 1
ORDER BY
  CASE WHEN s.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
         OR (cur.source_key = s.source_key AND EXISTS(
           SELECT 1 FROM vehicle_realtime_location_source conflicting
            WHERE conflicting.vin = s.vin AND conflicting.protocol = s.protocol
              AND conflicting.source_key <> s.source_key AND conflicting.quality_status = 'OK'
              AND conflicting.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
              AND ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(conflicting.longitude, conflicting.latitude)) > 200
         )) THEN 0 ELSE 1 END ASC,
  (COALESCE(policy.priority, CASE s.source_kind WHEN 'DIRECT' THEN 20 WHEN 'PLATFORM' THEN 30 ELSE 40 END)
    + CASE WHEN cur.source_key = s.source_key AND EXISTS(
        SELECT 1 FROM vehicle_realtime_location_source conflicting
         WHERE conflicting.vin = s.vin AND conflicting.protocol = s.protocol
           AND conflicting.source_key <> s.source_key AND conflicting.quality_status = 'OK'
           AND conflicting.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
           AND ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(conflicting.longitude, conflicting.latitude)) > 200
      ) THEN -10000 ELSE 0 END
    + CASE WHEN cur.source_key = s.source_key AND cur.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN -5 ELSE 0 END
    + CASE WHEN cur.source_key <> '' AND cur.source_key <> s.source_key AND s.consecutive_good_samples < 3 THEN 1000 ELSE 0 END) ASC,
  s.received_at DESC, s.source_key ASC
LIMIT 1
ON DUPLICATE KEY UPDATE
  plate = IF(VALUES(plate) <> '', VALUES(plate), vehicle_realtime_location.plate), source_key = VALUES(source_key),
  event_time = VALUES(event_time), latitude = VALUES(latitude), longitude = VALUES(longitude),
  speed_kmh = VALUES(speed_kmh), total_mileage_km = VALUES(total_mileage_km),
  total_mileage_event_time = VALUES(total_mileage_event_time), soc_percent = VALUES(soc_percent),
  altitude_m = VALUES(altitude_m), direction_deg = VALUES(direction_deg), alarm_flag = VALUES(alarm_flag),
  status_flag = VALUES(status_flag), location_conflict = VALUES(location_conflict),
  location_conflict_distance_m = VALUES(location_conflict_distance_m), received_at = VALUES(received_at),
  event_id = VALUES(event_id), updated_at = VALUES(updated_at)
`

var cleanupInvalidRealtimeTotalMileageSQL = []string{
	`UPDATE vehicle_realtime_location
SET total_mileage_km = NULL,
    total_mileage_event_time = NULL
WHERE total_mileage_km IS NOT NULL
  AND total_mileage_km <= 0`,
	`UPDATE vehicle_realtime_snapshot
SET parsed_json = JSON_REMOVE(parsed_json, '$."jt808.location.total_mileage_km"')
WHERE protocol = 'JT808'
  AND parsed_json IS NOT NULL
  AND CAST(JSON_UNQUOTE(JSON_EXTRACT(parsed_json, '$."jt808.location.total_mileage_km"')) AS DECIMAL(18,3)) <= 0`,
	`UPDATE vehicle_realtime_snapshot
SET parsed_json = JSON_REMOVE(parsed_json, '$."gb32960.vehicle.total_mileage_km"')
WHERE protocol = 'GB32960'
  AND parsed_json IS NOT NULL
  AND CAST(JSON_UNQUOTE(JSON_EXTRACT(parsed_json, '$."gb32960.vehicle.total_mileage_km"')) AS DECIMAL(18,3)) <= 0`,
	`UPDATE vehicle_realtime_snapshot
SET parsed_json = JSON_REMOVE(parsed_json, '$."yutong_mqtt.data.total_mileage_km"')
WHERE protocol = 'YUTONG_MQTT'
  AND parsed_json IS NOT NULL
	  AND CAST(JSON_UNQUOTE(JSON_EXTRACT(parsed_json, '$."yutong_mqtt.data.total_mileage_km"')) AS DECIMAL(18,3)) <= 0`,
}

const backfillRealtimeAccessProjectionSQL = `UPDATE vehicle_realtime_snapshot
SET access_first_seen_at = COALESCE(access_first_seen_at, received_at, updated_at),
    access_latest_received_at = COALESCE(access_latest_received_at, received_at, updated_at),
    access_sample_count = IF(access_sample_count = 0, 1, access_sample_count),
    access_latest_event_id = IF(access_latest_event_id = '', event_id, access_latest_event_id),
    access_first_seen_source = IF(access_first_seen_source = '', 'snapshot_backfill', access_first_seen_source)
WHERE access_first_seen_at IS NULL
   OR access_latest_received_at IS NULL
   OR access_sample_count = 0
   OR access_latest_event_id = ''
   OR access_first_seen_source = ''`

const upsertRealtimeLocationSQL = `
INSERT INTO vehicle_realtime_location
  (protocol, vin, plate, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time,
   soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, received_at, event_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
  plate = IF(VALUES(plate) <> '', VALUES(plate), plate),
  event_time = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(event_time), event_time),
  latitude = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(latitude), latitude),
  longitude = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(longitude), longitude),
  speed_kmh = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(speed_kmh), speed_kmh), speed_kmh),
  total_mileage_km = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(total_mileage_km), total_mileage_km), total_mileage_km),
  total_mileage_event_time = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), IF(VALUES(total_mileage_km) IS NOT NULL, COALESCE(VALUES(total_mileage_event_time), total_mileage_event_time), total_mileage_event_time), total_mileage_event_time),
  soc_percent = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(soc_percent), soc_percent), soc_percent),
  altitude_m = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(altitude_m), altitude_m), altitude_m),
  direction_deg = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(direction_deg), direction_deg), direction_deg),
  alarm_flag = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(alarm_flag), alarm_flag), alarm_flag),
  status_flag = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(status_flag), status_flag), status_flag),
  received_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(received_at), received_at),
  event_id = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(event_id), event_id),
  updated_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), CURRENT_TIMESTAMP, updated_at)
`
