package platform

import (
	"context"
	"database/sql"
	"encoding/json"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"
)

type ProductionStore struct {
	db                *sql.DB
	tdengine          *sql.DB
	tdDatabase        string
	redisOnline       redisOnlineKeyCounter
	capacityCheck     capacityChecker
	alertStreamGroup  string
	alertStreamMode   string
	accessSchemaOnce  sync.Once
	accessSchemaErr   error
	alertSchemaOnce   sync.Once
	alertSchemaErr    error
	profileSchemaOnce sync.Once
	profileSchemaErr  error
}

type redisOnlineKeyCounter interface {
	CountOnlineKeys(context.Context) (int, error)
}

type capacityChecker interface {
	CheckCapacity(context.Context) (CapacityCheckResult, error)
}

func NewProductionStore(db *sql.DB, tdengine *sql.DB, tdengineDatabase string) *ProductionStore {
	if db == nil {
		panic("production db must not be nil")
	}
	return &ProductionStore{db: db, tdengine: tdengine, tdDatabase: tdengineDatabase}
}

func (s *ProductionStore) WithRedisOnlineKeyCounter(counter redisOnlineKeyCounter) *ProductionStore {
	s.redisOnline = counter
	return s
}

func (s *ProductionStore) WithCapacityChecker(checker capacityChecker) *ProductionStore {
	s.capacityCheck = checker
	return s
}

func (s *ProductionStore) WithAlertStreamConfig(mode, consumerGroup string) *ProductionStore {
	s.alertStreamMode = strings.TrimSpace(mode)
	s.alertStreamGroup = strings.TrimSpace(consumerGroup)
	return s
}

func OpenSQL(ctx context.Context, driver, dsn string) (*sql.DB, error) {
	db, err := sql.Open(driver, dsn)
	if err != nil {
		return nil, err
	}
	db.SetMaxOpenConns(20)
	db.SetMaxIdleConns(10)
	db.SetConnMaxLifetime(30 * time.Minute)
	if err := db.PingContext(ctx); err != nil {
		_ = db.Close()
		return nil, err
	}
	return db, nil
}

func (s *ProductionStore) DashboardSummary(ctx context.Context) (DashboardSummary, error) {
	var summary DashboardSummary
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT vin) FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> '' AND updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)`).Scan(&summary.OnlineVehicles); err != nil {
		return DashboardSummary{}, err
	}
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT vin) FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> '' AND updated_at >= CURDATE()`).Scan(&summary.ActiveToday); err != nil {
		return DashboardSummary{}, err
	}
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM jt808_registration r LEFT JOIN vehicle_identity_binding b ON b.phone = r.phone WHERE r.vin = '' OR r.vin = 'unknown' OR r.vin IS NULL`).Scan(&summary.IssueVehicles); err != nil {
		return DashboardSummary{}, err
	}
	frameToday, err := s.frameToday(ctx, time.Now())
	if err != nil {
		return DashboardSummary{}, err
	}
	summary.FrameToday = frameToday
	protocols, err := s.protocolStats(ctx)
	if err != nil {
		return DashboardSummary{}, err
	}
	summary.Protocols = protocols
	serviceStatuses, err := s.serviceStatusStats(ctx)
	if err != nil {
		return DashboardSummary{}, err
	}
	summary.ServiceStatuses = serviceStatuses
	health, err := s.OpsHealth(ctx)
	if err != nil {
		return DashboardSummary{}, err
	}
	summary.KafkaLag = health.KafkaLag
	summary.LinkHealth = health.LinkHealth
	return summary, nil
}

func (s *ProductionStore) serviceStatusStats(ctx context.Context) ([]ServiceStatusStat, error) {
	definitions := []ServiceStatusStat{
		{Status: "healthy", Title: "服务正常"},
		{Status: "degraded", Title: "来源不完整"},
		{Status: "offline", Title: "车辆离线"},
		{Status: "no_data", Title: "暂无数据来源"},
		{Status: "identity_required", Title: "身份未绑定"},
	}
	out := make([]ServiceStatusStat, 0, len(definitions))
	for _, definition := range definitions {
		query := url.Values{"serviceStatus": {definition.Status}, "limit": {"1"}}
		built := buildVehicleCoverageSQL(query)
		count := 0
		if err := s.db.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&count); err != nil {
			return nil, err
		}
		definition.Count = count
		out = append(out, definition)
	}
	return out, nil
}

func (s *ProductionStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) {
	canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols))
	rows, err := s.db.QueryContext(ctx, `SELECT 
COUNT(*) AS total_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 THEN 1 ELSE 0 END), 0) AS bound_vehicles,
COALESCE(SUM(CASE WHEN online_source_count > 0 THEN 1 ELSE 0 END), 0) AS online_vehicles,
COALESCE(SUM(CASE WHEN source_count = 1 THEN 1 ELSE 0 END), 0) AS single_source_vehicles,
COALESCE(SUM(CASE WHEN source_count > 1 THEN 1 ELSE 0 END), 0) AS multi_source_vehicles,
COALESCE(SUM(CASE WHEN source_count = 0 THEN 1 ELSE 0 END), 0) AS no_data_vehicles,
COALESCE(SUM(CASE WHEN bound = 0 THEN 1 ELSE 0 END), 0) AS identity_required_vehicles,
COALESCE(SUM(CASE WHEN archive_complete = 0 THEN 1 ELSE 0 END), 0) AS archive_incomplete_vehicles,
COALESCE(SUM(CASE WHEN missing_plate = 1 THEN 1 ELSE 0 END), 0) AS missing_plate_vehicles,
COALESCE(SUM(CASE WHEN missing_phone = 1 THEN 1 ELSE 0 END), 0) AS missing_phone_vehicles,
COALESCE(SUM(CASE WHEN missing_oem = 1 THEN 1 ELSE 0 END), 0) AS missing_oem_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count = `+canonicalSourceCount+` AND online_source_count = source_count THEN 1 ELSE 0 END), 0) AS healthy_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count > 0 AND (online_source_count < source_count OR source_count < `+canonicalSourceCount+`) THEN 1 ELSE 0 END), 0) AS degraded_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count = 0 THEN 1 ELSE 0 END), 0) AS offline_vehicles
FROM (
SELECT v.vin,
MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) AS bound,
COUNT(DISTINCT s.protocol) AS source_count,
COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) AS online_source_count,
CASE WHEN v.vin IS NOT NULL AND v.vin <> ''
AND COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NOT NULL
AND NULLIF(MAX(NULLIF(b.phone, '')), '') IS NOT NULL
AND NULLIF(MAX(NULLIF(b.oem, '')), '') IS NOT NULL
THEN 1 ELSE 0 END AS archive_complete,
CASE WHEN COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL THEN 1 ELSE 0 END AS missing_plate,
CASE WHEN NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL THEN 1 ELSE 0 END AS missing_phone,
CASE WHEN NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL THEN 1 ELSE 0 END AS missing_oem
FROM (
SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''
UNION
SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''
) v
LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin
LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin
GROUP BY v.vin
) vehicle_service_summary`)
	if err != nil {
		return VehicleServiceSummary{}, err
	}
	defer rows.Close()
	var summary VehicleServiceSummary
	var healthy, degraded, offline int
	var missingPlate, missingPhone, missingOEM int
	if rows.Next() {
		if err := rows.Scan(
			&summary.TotalVehicles,
			&summary.BoundVehicles,
			&summary.OnlineVehicles,
			&summary.SingleSourceVehicles,
			&summary.MultiSourceVehicles,
			&summary.NoDataVehicles,
			&summary.IdentityRequiredVehicles,
			&summary.ArchiveIncompleteVehicles,
			&missingPlate,
			&missingPhone,
			&missingOEM,
			&healthy,
			&degraded,
			&offline,
		); err != nil {
			return VehicleServiceSummary{}, err
		}
	}
	if err := rows.Err(); err != nil {
		return VehicleServiceSummary{}, err
	}
	summary.ServiceStatuses = []ServiceStatusStat{
		{Status: "healthy", Title: "服务正常", Count: healthy},
		{Status: "degraded", Title: "来源不完整", Count: degraded},
		{Status: "offline", Title: "车辆离线", Count: offline},
		{Status: "no_data", Title: "暂无数据来源", Count: summary.NoDataVehicles},
		{Status: "identity_required", Title: "身份未绑定", Count: summary.IdentityRequiredVehicles},
	}
	summary.ArchiveMissingFields = buildArchiveMissingFieldStats(missingPlate, missingPhone, missingOEM)
	protocols, err := s.protocolStats(ctx)
	if err != nil {
		return VehicleServiceSummary{}, err
	}
	summary.Protocols = protocols
	missingSources, err := s.missingSourceStats(ctx)
	if err != nil {
		return VehicleServiceSummary{}, err
	}
	summary.MissingSources = missingSources
	return summary, nil
}

func (s *ProductionStore) missingSourceStats(ctx context.Context) ([]MissingSourceStat, error) {
	const vehicleSetSQL = `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''
UNION
SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''`
	out := make([]MissingSourceStat, 0, len(canonicalVehicleProtocols))
	for _, protocol := range canonicalVehicleProtocols {
		var count int
		err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM (`+vehicleSetSQL+`) v
WHERE NOT EXISTS (
SELECT 1 FROM vehicle_realtime_snapshot s
WHERE s.vin = v.vin AND s.protocol = ?
)`, protocol).Scan(&count)
		if err != nil {
			return nil, err
		}
		out = append(out, MissingSourceStat{Protocol: protocol, Count: count})
	}
	return out, nil
}

func (s *ProductionStore) frameToday(ctx context.Context, now time.Time) (int, error) {
	if s.tdengine == nil {
		return 0, nil
	}
	built := buildTodayRawFrameCountSQL(s.tdDatabase, now)
	total := 0
	if err := s.tdengine.QueryRowContext(ctx, built.Text).Scan(&total); err != nil {
		return 0, err
	}
	return total, nil
}

func (s *ProductionStore) Vehicles(ctx context.Context, query url.Values) (Page[VehicleRow], error) {
	built := buildVehicleListSQL(query)
	total := 0
	if built.CountText != "" {
		if err := s.db.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
			return Page[VehicleRow]{}, err
		}
	}
	rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		return Page[VehicleRow]{}, err
	}
	defer rows.Close()
	items := make([]VehicleRow, 0)
	for rows.Next() {
		var row VehicleRow
		var online int
		var sourceCount int
		var onlineSourceCount int
		var bindingStatus string
		if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &row.Protocol, &online, &row.LastSeen, &row.LocationText, &row.BindingScore, &sourceCount, &onlineSourceCount, &bindingStatus); err != nil {
			return Page[VehicleRow]{}, err
		}
		row.Online = online == 1
		row.ServiceStatus = buildVehicleCoverageServiceStatus(VehicleCoverageRow{
			VIN:               row.VIN,
			Plate:             row.Plate,
			Phone:             row.Phone,
			OEM:               row.OEM,
			Protocols:         []string{row.Protocol},
			SourceCount:       sourceCount,
			OnlineSourceCount: onlineSourceCount,
			Online:            onlineSourceCount > 0,
			LastSeen:          row.LastSeen,
			BindingStatus:     bindingStatus,
		})
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[VehicleRow]{}, err
	}
	limit, offset := buildLimitOffset(query)
	if built.CountText == "" {
		total = len(items)
	}
	return Page[VehicleRow]{Items: items, Total: total, Limit: limit, Offset: offset}, nil
}

func (s *ProductionStore) VehicleCoverage(ctx context.Context, query url.Values) (Page[VehicleCoverageRow], error) {
	built := buildVehicleCoverageSQL(query)
	total := 0
	if built.CountText != "" {
		if err := s.db.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
			return Page[VehicleCoverageRow]{}, err
		}
	}
	rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		return Page[VehicleCoverageRow]{}, err
	}
	defer rows.Close()
	items := make([]VehicleCoverageRow, 0)
	for rows.Next() {
		var row VehicleCoverageRow
		var protocols string
		var onlineProtocols string
		var online int
		if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &protocols, &onlineProtocols, &row.SourceCount, &row.OnlineSourceCount, &online, &row.LastSeen, &row.BindingStatus); err != nil {
			return Page[VehicleCoverageRow]{}, err
		}
		row.Protocols = splitCSV(protocols)
		row.MissingProtocols = missingCanonicalProtocols(row.Protocols)
		row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, splitCSV(onlineProtocols), row.LastSeen)
		row.Online = online == 1
		row.ServiceStatus = buildVehicleCoverageServiceStatus(row)
		row.SourceConsistency = buildVehicleCoverageSourceConsistency(row)
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[VehicleCoverageRow]{}, err
	}
	limit, offset := buildLimitOffset(query)
	if built.CountText == "" {
		total = len(items)
	}
	return Page[VehicleCoverageRow]{Items: items, Total: total, Limit: limit, Offset: offset}, nil
}

func (s *ProductionStore) VehicleCoverageSummary(ctx context.Context, query url.Values) (VehicleCoverageSummary, error) {
	built := buildVehicleCoverageSummarySQL(query)
	var summary VehicleCoverageSummary
	var missingPlate, missingPhone, missingOEM int
	if err := s.db.QueryRowContext(ctx, built.Text, built.Args...).Scan(
		&summary.TotalVehicles,
		&summary.OnlineVehicles,
		&summary.SingleSourceVehicles,
		&summary.MultiSourceVehicles,
		&summary.NoDataVehicles,
		&summary.UnboundVehicles,
		&summary.ArchiveIncompleteVehicles,
		&missingPlate,
		&missingPhone,
		&missingOEM,
	); err != nil {
		return VehicleCoverageSummary{}, err
	}
	summary.ArchiveMissingFields = buildArchiveMissingFieldStats(missingPlate, missingPhone, missingOEM)
	missingSources, err := s.coverageMissingSourceStats(ctx, query)
	if err != nil {
		return VehicleCoverageSummary{}, err
	}
	summary.MissingSources = missingSources
	return summary, nil
}

func (s *ProductionStore) coverageMissingSourceStats(ctx context.Context, query url.Values) ([]MissingSourceStat, error) {
	out := make([]MissingSourceStat, 0, len(canonicalVehicleProtocols))
	for _, protocol := range canonicalVehicleProtocols {
		next := copyValues(query)
		next.Set("missingProtocol", protocol)
		next.Set("limit", "1")
		next.Set("offset", "0")
		page, err := s.VehicleCoverage(ctx, next)
		if err != nil {
			return nil, err
		}
		out = append(out, MissingSourceStat{Protocol: protocol, Count: page.Total})
	}
	return out, nil
}

func (s *ProductionStore) VehicleRealtime(ctx context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
	built := buildVehicleRealtimeSQL(query)
	total := 0
	if err := s.db.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
		return Page[VehicleRealtimeRow]{}, err
	}
	rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		return Page[VehicleRealtimeRow]{}, err
	}
	defer rows.Close()
	items := make([]VehicleRealtimeRow, 0)
	for rows.Next() {
		var row VehicleRealtimeRow
		var protocols string
		var onlineProtocols string
		var online int
		var longitude, latitude, speed, soc, mileage string
		var conflictDistance sql.NullString
		var locationConflict int
		var locationAvailable int
		var speedAvailable int
		var socAvailable int
		var mileageAvailable int
		var todayMileageAvailable int
		var reportIntervalMs int64
		var todayMileage string
		if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &protocols, &onlineProtocols, &row.SourceCount, &row.OnlineSourceCount, &online, &row.BindingStatus, &row.PrimaryProtocol, &row.LocationSource, &locationConflict, &conflictDistance, &locationAvailable, &longitude, &latitude, &speedAvailable, &speed, &socAvailable, &soc, &mileageAvailable, &mileage, &todayMileageAvailable, &todayMileage, &row.LastSeen, &reportIntervalMs); err != nil {
			return Page[VehicleRealtimeRow]{}, err
		}
		row.Protocols = splitCSV(protocols)
		row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, splitCSV(onlineProtocols), row.LastSeen)
		row.Online = online == 1
		row.LocationConflict = locationConflict == 1
		row.LocationAvailable = locationAvailable == 1
		row.SpeedAvailable = speedAvailable == 1
		row.SOCAvailable = socAvailable == 1
		row.MileageAvailable = mileageAvailable == 1
		row.TodayMileageAvailable = todayMileageAvailable == 1
		if conflictDistance.Valid {
			value := parseFloatString(conflictDistance.String)
			if value >= 0 {
				row.ConflictDistanceM = &value
			}
		}
		row.ServiceStatus = buildRealtimeServiceStatus(row)
		row.Longitude = parseFloatString(longitude)
		row.Latitude = parseFloatString(latitude)
		row.SpeedKmh = parseFloatString(speed)
		row.SOCPercent = parseFloatString(soc)
		row.TotalMileageKm = parseFloatString(mileage)
		row.TodayMileageKm = parseFloatString(todayMileage)
		if reportIntervalMs > 0 {
			row.ReportIntervalMs = &reportIntervalMs
		}
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[VehicleRealtimeRow]{}, err
	}
	limit, offset := buildLimitOffset(query)
	return Page[VehicleRealtimeRow]{Items: items, Total: total, Limit: limit, Offset: offset}, nil
}

func (s *ProductionStore) VehicleServiceOverviews(ctx context.Context, query VehicleOverviewBatchQuery) (Page[VehicleServiceOverview], error) {
	keywords := normalizedKeywords(query.Keywords)
	if len(keywords) == 0 {
		return Page[VehicleServiceOverview]{Items: []VehicleServiceOverview{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
	}
	built := buildVehicleServiceOverviewBatchSQL(query)
	rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		return Page[VehicleServiceOverview]{}, err
	}
	defer rows.Close()
	byKey := map[string]*VehicleServiceOverview{}
	overviews := make([]VehicleServiceOverview, 0, len(keywords))
	for rows.Next() {
		var overview VehicleServiceOverview
		var protocols string
		if err := rows.Scan(&overview.VIN, &overview.Plate, &overview.Phone, &overview.OEM, &protocols, &overview.SourceCount, &overview.OnlineSourceCount, &overview.LastSeen, &overview.PrimaryProtocol); err != nil {
			return Page[VehicleServiceOverview]{}, err
		}
		overview.Protocols = splitCSV(protocols)
		if overview.PrimaryProtocol == "" && len(overview.Protocols) > 0 {
			overview.PrimaryProtocol = overview.Protocols[0]
		}
		overview.RealtimeCount = overview.SourceCount
		applyVehicleServiceOverviewDerivedFields(&overview, strings.TrimSpace(overview.VIN) != "")
		overviews = append(overviews, overview)
		current := &overviews[len(overviews)-1]
		for _, key := range []string{overview.VIN, overview.Plate, overview.Phone} {
			if trimmed := strings.ToLower(strings.TrimSpace(key)); trimmed != "" {
				byKey[trimmed] = current
			}
		}
	}
	if err := rows.Err(); err != nil {
		return Page[VehicleServiceOverview]{}, err
	}
	items := make([]VehicleServiceOverview, 0, len(keywords))
	for _, keyword := range keywords {
		if overview := byKey[strings.ToLower(strings.TrimSpace(keyword))]; overview != nil {
			items = append(items, *overview)
			continue
		}
		matched := false
		for index := range overviews {
			if vehicleServiceOverviewMatchesKeyword(overviews[index], keyword) {
				items = append(items, overviews[index])
				matched = true
				break
			}
		}
		if matched {
			continue
		}
		resolution := VehicleIdentityResolution{LookupKey: keyword, Protocols: []string{}}
		items = append(items, *buildVehicleServiceOverview("", keyword, &resolution, nil, nil, nil, Page[HistoryLocationRow]{}, Page[RawFrameRow]{}, Page[DailyMileageRow]{}, Page[QualityIssueRow]{}))
	}
	return Page[VehicleServiceOverview]{Items: items, Total: len(items), Limit: query.Limit, Offset: query.Offset}, nil
}

func buildVehicleServiceOverviewBatchSQL(query VehicleOverviewBatchQuery) SQLQuery {
	keywords := normalizedKeywords(query.Keywords)
	identityWhere, args := fuzzyKeywordWhere("b", []string{"vin", "plate", "phone", "oem"}, keywords)
	realtimeWhere, realtimeArgs := fuzzyKeywordWhere("s", []string{"vin", "plate"}, keywords)
	args = append(args, realtimeArgs...)
	protocolJoin := ""
	if protocol := strings.TrimSpace(query.Protocol); protocol != "" {
		protocolJoin = " AND s.protocol = ? "
		args = append(args, protocol)
	}
	return SQLQuery{
		Text: `SELECT i.vin, COALESCE(MAX(NULLIF(i.plate, '')), '') AS plate, ` +
			`COALESCE(MAX(NULLIF(i.phone, '')), '') AS phone, COALESCE(MAX(NULLIF(i.oem, '')), '') AS oem, ` +
			`COALESCE(GROUP_CONCAT(DISTINCT s.protocol ORDER BY s.protocol SEPARATOR ','), '') AS protocols, ` +
			`COUNT(DISTINCT s.protocol) AS source_count, ` +
			`COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) AS online_source_count, ` +
			`COALESCE(DATE_FORMAT(MAX(s.updated_at), '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
			`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(s.protocol ORDER BY s.updated_at DESC, s.protocol ASC), ',', 1), '') AS primary_protocol ` +
			`FROM (` +
			`SELECT b.vin, b.plate, b.phone, b.oem FROM vehicle_identity_binding b WHERE ` + identityWhere + ` ` +
			`UNION ` +
			`SELECT s.vin, COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), b.plate, '') AS plate, COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem ` +
			`FROM vehicle_realtime_snapshot s LEFT JOIN vehicle_identity_binding b ON b.vin = s.vin ` +
			`WHERE ` + realtimeWhere + ` ` +
			`GROUP BY s.vin, b.plate, b.phone, b.oem` +
			`) i LEFT JOIN vehicle_realtime_snapshot s ON s.vin = i.vin ` + protocolJoin +
			`WHERE i.vin IS NOT NULL AND i.vin <> '' GROUP BY i.vin`,
		Args: args,
	}
}

func fuzzyKeywordWhere(alias string, fields []string, keywords []string) (string, []any) {
	clauses := make([]string, 0, len(keywords))
	args := make([]any, 0, len(keywords)*len(fields))
	for _, keyword := range keywords {
		fieldClauses := make([]string, 0, len(fields))
		for _, field := range fields {
			fieldClauses = append(fieldClauses, alias+"."+field+" LIKE ?")
			args = append(args, "%"+keyword+"%")
		}
		clauses = append(clauses, "("+strings.Join(fieldClauses, " OR ")+")")
	}
	return strings.Join(clauses, " OR "), args
}

func vehicleServiceOverviewMatchesKeyword(overview VehicleServiceOverview, keyword string) bool {
	keyword = strings.ToLower(strings.TrimSpace(keyword))
	if keyword == "" {
		return false
	}
	for _, value := range []string{overview.VIN, overview.Plate, overview.Phone, overview.OEM} {
		if strings.Contains(strings.ToLower(strings.TrimSpace(value)), keyword) {
			return true
		}
	}
	return false
}

func (s *ProductionStore) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
	built := buildRealtimeLocationSQL(query)
	total := 0
	if err := s.db.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
		return Page[RealtimeLocationRow]{}, err
	}
	rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		return Page[RealtimeLocationRow]{}, err
	}
	defer rows.Close()
	items := make([]RealtimeLocationRow, 0)
	for rows.Next() {
		var row RealtimeLocationRow
		if err := rows.Scan(&row.VIN, &row.Plate, &row.Protocol, &row.Longitude, &row.Latitude, &row.SpeedKmh, &row.SOCPercent, &row.TotalMileageKm, &row.LastSeen); err != nil {
			return Page[RealtimeLocationRow]{}, err
		}
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[RealtimeLocationRow]{}, err
	}
	limit, offset := buildLimitOffset(query)
	return Page[RealtimeLocationRow]{Items: items, Total: total, Limit: limit, Offset: offset}, nil
}

func parseFloatString(value string) float64 {
	parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
	if err != nil {
		return 0
	}
	return parsed
}

func isTDengineTableNotExist(err error) bool {
	if err == nil {
		return false
	}
	message := strings.ToLower(err.Error())
	return strings.Contains(message, "table does not exist") || strings.Contains(message, "fail to get table info")
}

func (s *ProductionStore) HistoryLocations(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
	realtime, err := s.RealtimeLocations(ctx, query)
	if err != nil {
		return Page[HistoryLocationRow]{}, err
	}
	items := make([]HistoryLocationRow, 0, len(realtime.Items))
	for _, row := range realtime.Items {
		items = append(items, HistoryLocationRow{
			VIN: row.VIN, Plate: row.Plate, Protocol: row.Protocol, Longitude: row.Longitude, Latitude: row.Latitude,
			SpeedKmh: row.SpeedKmh, SOCPercent: row.SOCPercent, SOCAvailable: row.SOCPercent > 0, TotalMileageKm: row.TotalMileageKm, DeviceTime: row.LastSeen, ServerTime: row.LastSeen,
		})
	}
	return Page[HistoryLocationRow]{Items: items, Total: realtime.Total, Limit: realtime.Limit, Offset: realtime.Offset}, nil
}

func (s *ProductionStore) HistoryLocationsFromTDengine(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
	if s.tdengine == nil {
		return s.HistoryLocations(ctx, query)
	}
	limit, offset := buildLimitOffset(query)
	tdQuery := map[string]string{
		"protocol": query.Get("protocol"),
		"vin":      query.Get("vin"),
		"limit":    strconv.Itoa(limit),
		"offset":   strconv.Itoa(offset),
	}
	if value := strings.TrimSpace(query.Get("dateFrom")); value != "" {
		tdQuery["dateFrom"] = value
	}
	if value := strings.TrimSpace(query.Get("dateTo")); value != "" {
		tdQuery["dateTo"] = value
	}
	built := buildHistoryLocationSQL(s.tdDatabase, tdQuery)
	total := 0
	if built.CountText != "" {
		if err := s.tdengine.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
			if isTDengineTableNotExist(err) {
				return Page[HistoryLocationRow]{Items: []HistoryLocationRow{}, Total: 0, Limit: limit, Offset: offset}, nil
			}
			return Page[HistoryLocationRow]{}, err
		}
	}
	rows, err := s.tdengine.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		if isTDengineTableNotExist(err) {
			return Page[HistoryLocationRow]{Items: []HistoryLocationRow{}, Total: 0, Limit: limit, Offset: offset}, nil
		}
		return Page[HistoryLocationRow]{}, err
	}
	defer rows.Close()
	items := make([]HistoryLocationRow, 0)
	for rows.Next() {
		var row HistoryLocationRow
		var ts int64
		var receivedAt sql.NullInt64
		var longitude, latitude, speed, soc, mileage sql.NullFloat64
		var direction, alarm sql.NullInt64
		if err := rows.Scan(&ts, &row.VIN, &row.Protocol, &longitude, &latitude, &speed, &soc, &direction, &alarm, &mileage, &receivedAt); err != nil {
			return Page[HistoryLocationRow]{}, err
		}
		row.Longitude = nullFloat64(longitude)
		row.Latitude = nullFloat64(latitude)
		row.SpeedKmh = nullFloat64(speed)
		row.SOCPercent = nullFloat64(soc)
		row.SOCAvailable = soc.Valid
		row.DirectionDeg = nullInt64Pointer(direction)
		row.AlarmFlag = nullInt64Pointer(alarm)
		row.TotalMileageKm = nullFloat64(mileage)
		row.DeviceTime = time.UnixMilli(ts).UTC().Format(time.RFC3339Nano)
		row.ServerTime = row.DeviceTime
		if receivedAt.Valid {
			row.ServerTime = time.UnixMilli(receivedAt.Int64).UTC().Format(time.RFC3339Nano)
		}
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[HistoryLocationRow]{}, err
	}
	if built.CountText == "" {
		total = len(items)
	}
	if err := s.enrichHistoryLocationPlates(ctx, items); err != nil {
		return Page[HistoryLocationRow]{}, err
	}
	return Page[HistoryLocationRow]{Items: items, Total: total, Limit: limit, Offset: offset}, nil
}

func (s *ProductionStore) HistoryLocationSeries(ctx context.Context, query HistoryLocationSeriesQuery) ([]HistoryLocationSeriesBucket, error) {
	if s.tdengine == nil {
		return []HistoryLocationSeriesBucket{}, nil
	}
	built := buildHistoryLocationSeriesSQL(s.tdDatabase, query)
	rows, err := s.tdengine.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		if isTDengineTableNotExist(err) {
			return []HistoryLocationSeriesBucket{}, nil
		}
		return nil, err
	}
	defer rows.Close()
	buckets := make([]HistoryLocationSeriesBucket, 0)
	for rows.Next() {
		var bucket HistoryLocationSeriesBucket
		var speedAverage, speedMinimum, speedMaximum, speedLast sql.NullFloat64
		var mileageAverage, mileageMinimum, mileageMaximum, mileageLast sql.NullFloat64
		if err := rows.Scan(&bucket.Time, &bucket.Protocol, &bucket.Count, &speedAverage, &speedMinimum, &speedMaximum, &speedLast, &mileageAverage, &mileageMinimum, &mileageMaximum, &mileageLast); err != nil {
			return nil, err
		}
		if parsed, ok := parseVehicleServiceTime(bucket.Time); ok {
			bucket.Time = parsed.UTC().Format(time.RFC3339)
		}
		bucket.SpeedAverage = nullFloat64Pointer(speedAverage)
		bucket.SpeedMinimum = nullFloat64Pointer(speedMinimum)
		bucket.SpeedMaximum = nullFloat64Pointer(speedMaximum)
		bucket.SpeedLast = nullFloat64Pointer(speedLast)
		bucket.MileageAverage = nullFloat64Pointer(mileageAverage)
		bucket.MileageMinimum = nullFloat64Pointer(mileageMinimum)
		bucket.MileageMaximum = nullFloat64Pointer(mileageMaximum)
		bucket.MileageLast = nullFloat64Pointer(mileageLast)
		buckets = append(buckets, bucket)
	}
	return buckets, rows.Err()
}

func (s *ProductionStore) HistoryExportCount(ctx context.Context, query HistoryExportStoreQuery) (int64, error) {
	if query.Category == "mileage" {
		page, err := s.DailyMileage(ctx, historyExportMileageQuery(query, 1, 0))
		return int64(page.Total), err
	}
	if s.tdengine == nil {
		return 0, nil
	}
	built := buildHistoryExportBatchSQL(s.tdDatabase, query, HistoryExportCursor{}, 1)
	var total int64
	if err := s.tdengine.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
		if isTDengineTableNotExist(err) {
			return 0, nil
		}
		return 0, err
	}
	return total, nil
}

func (s *ProductionStore) HistoryExportScopeActive(ctx context.Context, job HistoryExportJob) (bool, error) {
	userID, err := strconv.ParseUint(strings.TrimSpace(job.OwnerSubjectID), 10, 64)
	if err != nil || userID == 0 {
		return false, nil
	}
	var status, userType string
	if err := s.db.QueryRowContext(ctx, `SELECT status,user_type FROM platform_user WHERE id=?`, userID).Scan(&status, &userType); err != nil {
		if err == sql.ErrNoRows {
			return false, nil
		}
		return false, err
	}
	if status != "enabled" || userType != "customer" {
		return false, nil
	}
	for _, scope := range job.VehicleScopes {
		dateFrom, fromOK := parseTrackRequestTime(scope.DateFrom)
		dateTo, toOK := parseTrackRequestTime(scope.DateTo)
		if !fromOK || !toOK || !dateTo.After(dateFrom) {
			return false, nil
		}
		var count int
		if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM platform_user_vehicle WHERE user_id=? AND vin=? AND COALESCE(valid_from,granted_at)<=? AND (valid_to IS NULL OR valid_to>=?)`, userID, scope.VIN, dateFrom, dateTo).Scan(&count); err != nil {
			return false, err
		}
		if count != 1 {
			return false, nil
		}
	}
	return len(job.VehicleScopes) > 0, nil
}

func (s *ProductionStore) HistoryExportBatch(ctx context.Context, query HistoryExportStoreQuery, cursor HistoryExportCursor, limit int) ([]HistoryDataRow, HistoryExportCursor, error) {
	if query.Category == "mileage" {
		page, err := s.DailyMileage(ctx, historyExportMileageQuery(query, limit, cursor.Offset))
		if err != nil {
			return nil, cursor, err
		}
		result := make([]HistoryDataRow, 0, len(page.Items))
		for index, item := range page.Items {
			quality, reason := historyMileageQuality(item)
			result = append(result, HistoryDataRow{ID: "mileage-" + item.VIN + "-" + strconv.Itoa(cursor.Offset+index), VIN: item.VIN, Plate: item.Plate, Protocol: item.Source, DeviceTime: item.Date, ServerTime: item.Date, Quality: quality, QualityReason: reason, Values: map[string]any{"startMileageKm": item.StartMileageKm, "endMileageKm": item.EndMileageKm, "dailyMileageKm": item.DailyMileageKm}})
		}
		cursor.Offset += len(result)
		return result, cursor, nil
	}
	if s.tdengine == nil {
		return []HistoryDataRow{}, cursor, nil
	}
	built := buildHistoryExportBatchSQL(s.tdDatabase, query, cursor, limit)
	rows, err := s.tdengine.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		if isTDengineTableNotExist(err) {
			return []HistoryDataRow{}, cursor, nil
		}
		return nil, cursor, err
	}
	defer rows.Close()
	plates, err := s.platesByVIN(ctx, []string{query.VIN})
	if err != nil {
		return nil, cursor, err
	}
	if query.Category == "raw" {
		return scanRawHistoryExportRows(rows, query, cursor, plates[query.VIN])
	}
	return scanLocationHistoryExportRows(rows, cursor, plates[query.VIN])
}

func scanLocationHistoryExportRows(rows *sql.Rows, cursor HistoryExportCursor, plate string) ([]HistoryDataRow, HistoryExportCursor, error) {
	items := make([]HistoryDataRow, 0)
	for rows.Next() {
		var vin, protocol, ts, receivedAt string
		var longitude, latitude, speed, mileage sql.NullFloat64
		if err := rows.Scan(&ts, &vin, &protocol, &longitude, &latitude, &speed, &mileage, &receivedAt); err != nil {
			return nil, cursor, err
		}
		location := HistoryLocationRow{Longitude: nullFloat64(longitude), Latitude: nullFloat64(latitude), SpeedKmh: nullFloat64(speed), TotalMileageKm: nullFloat64(mileage), DeviceTime: ts, ServerTime: firstNonEmpty(receivedAt, ts)}
		quality, reason := historyLocationQuality(location)
		items = append(items, HistoryDataRow{ID: "location-" + vin + "-" + protocol + "-" + ts, VIN: vin, Plate: plate, Protocol: protocol, DeviceTime: ts, ServerTime: firstNonEmpty(receivedAt, ts), Quality: quality, QualityReason: reason, Values: map[string]any{"speedKmh": nullFloat64(speed), "totalMileageKm": nullFloat64(mileage), "longitude": nullFloat64(longitude), "latitude": nullFloat64(latitude)}})
		cursor = HistoryExportCursor{Time: ts, Protocol: protocol}
	}
	return items, cursor, rows.Err()
}

func scanRawHistoryExportRows(rows *sql.Rows, query HistoryExportStoreQuery, cursor HistoryExportCursor, plate string) ([]HistoryDataRow, HistoryExportCursor, error) {
	items := make([]HistoryDataRow, 0)
	for rows.Next() {
		var ts, frameID, eventTime, receivedAt, parsedFields, parseStatus, parseError, sourceEndpoint, protocol, vehicleKey, vin, phone string
		var rawSizeBytes int
		if err := rows.Scan(&ts, &frameID, &eventTime, &receivedAt, &rawSizeBytes, &parsedFields, &parseStatus, &parseError, &sourceEndpoint, &protocol, &vehicleKey, &vin, &phone); err != nil {
			return nil, cursor, err
		}
		values := map[string]any{"frameType": vehicleKey, "rawSizeBytes": rawSizeBytes}
		fields := parsedFieldsFromString(parsedFields)
		if len(query.Metrics) > 0 {
			fields = filterParsedFieldsMap(fields, query.Metrics)
		}
		for key, value := range fields {
			values[key] = value
		}
		frame := RawFrameRow{ParseStatus: parseStatus, ParseError: parseError, DeviceTime: firstNonEmpty(eventTime, ts), ServerTime: firstNonEmpty(receivedAt, ts)}
		quality, reason := historyRawQuality(frame)
		items = append(items, HistoryDataRow{ID: frameID, VIN: vin, Plate: plate, Protocol: protocol, DeviceTime: frame.DeviceTime, ServerTime: frame.ServerTime, Quality: quality, QualityReason: reason, EvidenceID: frameID, Values: values})
		cursor = HistoryExportCursor{Time: ts, Protocol: protocol, ID: frameID}
	}
	return items, cursor, rows.Err()
}

func historyExportMileageQuery(query HistoryExportStoreQuery, limit, offset int) url.Values {
	values := url.Values{"vin": {query.VIN}, "limit": {strconv.Itoa(limit)}, "offset": {strconv.Itoa(offset)}}
	if query.Protocol != "" {
		values.Set("protocol", query.Protocol)
	}
	if query.DateFrom != "" {
		values.Set("dateFrom", strings.Split(query.DateFrom, "T")[0])
	}
	if query.DateTo != "" {
		values.Set("dateTo", strings.Split(query.DateTo, "T")[0])
	}
	return values
}

func nullFloat64Pointer(value sql.NullFloat64) *float64 {
	if !value.Valid {
		return nil
	}
	result := value.Float64
	return &result
}

func nullInt64Pointer(value sql.NullInt64) *int64 {
	if !value.Valid {
		return nil
	}
	result := value.Int64
	return &result
}

func (s *ProductionStore) enrichHistoryLocationPlates(ctx context.Context, items []HistoryLocationRow) error {
	vins := make([]string, 0)
	seen := map[string]struct{}{}
	for _, item := range items {
		vin := strings.TrimSpace(item.VIN)
		if vin == "" {
			continue
		}
		if _, ok := seen[vin]; ok {
			continue
		}
		seen[vin] = struct{}{}
		vins = append(vins, vin)
	}
	plates, err := s.platesByVIN(ctx, vins)
	if err != nil {
		return err
	}
	for index := range items {
		if strings.TrimSpace(items[index].Plate) != "" {
			continue
		}
		items[index].Plate = plates[items[index].VIN]
	}
	return nil
}

func (s *ProductionStore) enrichRawFramePlates(ctx context.Context, items []RawFrameRow) error {
	vins := make([]string, 0)
	seen := map[string]struct{}{}
	for _, item := range items {
		vin := strings.TrimSpace(item.VIN)
		if vin == "" {
			continue
		}
		if _, ok := seen[vin]; ok {
			continue
		}
		seen[vin] = struct{}{}
		vins = append(vins, vin)
	}
	plates, err := s.platesByVIN(ctx, vins)
	if err != nil {
		return err
	}
	for index := range items {
		items[index].Plate = plates[items[index].VIN]
	}
	return nil
}

func (s *ProductionStore) platesByVIN(ctx context.Context, vins []string) (map[string]string, error) {
	if len(vins) == 0 {
		return map[string]string{}, nil
	}
	placeholders := make([]string, len(vins))
	args := make([]any, len(vins))
	for index, vin := range vins {
		placeholders[index] = "?"
		args[index] = vin
	}
	rows, err := s.db.QueryContext(ctx, `SELECT vin, COALESCE(plate, '') FROM vehicle_identity_binding WHERE vin IN (`+strings.Join(placeholders, ",")+`)`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	plates := map[string]string{}
	for rows.Next() {
		var vin, plate string
		if err := rows.Scan(&vin, &plate); err != nil {
			return nil, err
		}
		plates[vin] = plate
	}
	return plates, rows.Err()
}

func (s *ProductionStore) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
	if s.tdengine == nil {
		return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
	}
	built := buildRawFrameSQL(s.tdDatabase, query)
	total := 0
	if built.CountText != "" {
		if err := s.tdengine.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
			if isTDengineTableNotExist(err) {
				return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
			}
			return Page[RawFrameRow]{}, err
		}
	}
	rows, err := s.tdengine.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		if isTDengineTableNotExist(err) {
			return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
		}
		return Page[RawFrameRow]{}, err
	}
	defer rows.Close()
	items := make([]RawFrameRow, 0)
	for rows.Next() {
		var row RawFrameRow
		var ts, frameID, eventTime, receivedAt, parsedFields, parseStatus, parseError, sourceEndpoint, protocol, vehicleKey, vin, phone string
		var rawSizeBytes int
		if err := rows.Scan(&ts, &frameID, &eventTime, &receivedAt, &rawSizeBytes, &parsedFields, &parseStatus, &parseError, &sourceEndpoint, &protocol, &vehicleKey, &vin, &phone); err != nil {
			return Page[RawFrameRow]{}, err
		}
		row.ID = frameID
		row.VIN = vin
		row.Protocol = protocol
		row.FrameType = vehicleKey
		row.DeviceTime = firstNonEmpty(eventTime, ts)
		row.ServerTime = firstNonEmpty(receivedAt, ts)
		row.RawSizeBytes = rawSizeBytes
		row.ParseStatus = parseStatus
		row.ParseError = parseError
		row.SourceEndpoint = sourceEndpoint
		row.ParsedFields = parsedFieldsFromString(parsedFields)
		if len(query.Fields) > 0 {
			row.ParsedFields = filterParsedFieldsMap(row.ParsedFields, query.Fields)
		}
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[RawFrameRow]{}, err
	}
	if built.CountText == "" {
		total = len(items)
	}
	if err := s.enrichRawFramePlates(ctx, items); err != nil {
		return Page[RawFrameRow]{}, err
	}
	return Page[RawFrameRow]{Items: items, Total: total, Limit: query.Limit, Offset: query.Offset}, nil
}

func (s *ProductionStore) MileageSummary(ctx context.Context, query url.Values) (MileageSummary, error) {
	built := buildMileageSummarySQL(query)
	var summary MileageSummary
	if err := s.db.QueryRowContext(ctx, built.Text, built.Args...).Scan(&summary.VehicleCount, &summary.RecordCount, &summary.SourceCount, &summary.TotalMileageKm); err != nil {
		return MileageSummary{}, err
	}
	if summary.VehicleCount > 0 {
		summary.AverageMileagePerVIN = summary.TotalMileageKm / float64(summary.VehicleCount)
	}
	return summary, nil
}

func (s *ProductionStore) DailyMileage(ctx context.Context, query url.Values) (Page[DailyMileageRow], error) {
	built := buildDailyMileageSQL(query)
	total := 0
	if built.CountText != "" {
		if err := s.db.QueryRowContext(ctx, built.CountText, built.CountArgs...).Scan(&total); err != nil {
			return Page[DailyMileageRow]{}, err
		}
	}
	rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
	if err != nil {
		return Page[DailyMileageRow]{}, err
	}
	defer rows.Close()
	items := make([]DailyMileageRow, 0)
	for rows.Next() {
		var row DailyMileageRow
		if err := rows.Scan(&row.VIN, &row.Plate, &row.Date, &row.StartMileageKm, &row.EndMileageKm, &row.DailyMileageKm, &row.Source); err != nil {
			return Page[DailyMileageRow]{}, err
		}
		items = append(items, row)
	}
	if err := rows.Err(); err != nil {
		return Page[DailyMileageRow]{}, err
	}
	limit, offset := buildLimitOffset(query)
	if built.CountText == "" {
		total = len(items)
	}
	return Page[DailyMileageRow]{Items: items, Total: total, Limit: limit, Offset: offset}, nil
}

func (s *ProductionStore) MileageStatistics(ctx context.Context, query url.Values) (MileageStatistics, error) {
	result := MileageStatistics{
		DateFrom: query.Get("dateFrom"), DateTo: query.Get("dateTo"),
		Trend: []MileageTrendPoint{}, Ranking: []MileageVehicleRank{},
		Evidence: "vehicle_daily_mileage（按车辆和日期去重）/ vehicle_realtime_location（最新里程表）",
	}
	summary := buildMileageStatisticsSummarySQL(query)
	if err := s.db.QueryRowContext(ctx, summary.Text, summary.Args...).Scan(&result.VehicleCount, &result.RecordCount, &result.PeriodMileageKm, &result.AverageDailyMileageKm); err != nil {
		return MileageStatistics{}, err
	}
	if result.VehicleCount > 0 {
		result.AverageMileagePerVIN = result.PeriodMileageKm / float64(result.VehicleCount)
	}
	sources := buildMileageStatisticsSourceCountSQL(query)
	if err := s.db.QueryRowContext(ctx, sources.Text, sources.Args...).Scan(&result.SourceCount); err != nil {
		return MileageStatistics{}, err
	}
	fleet := buildFleetLatestMileageSQL(query)
	if err := s.db.QueryRowContext(ctx, fleet.Text, fleet.Args...).Scan(&result.FleetLatestMileageKm); err != nil {
		return MileageStatistics{}, err
	}
	trend := buildMileageStatisticsTrendSQL(query)
	rows, err := s.db.QueryContext(ctx, trend.Text, trend.Args...)
	if err != nil {
		return MileageStatistics{}, err
	}
	for rows.Next() {
		var point MileageTrendPoint
		if err := rows.Scan(&point.Date, &point.MileageKm, &point.Vehicles); err != nil {
			rows.Close()
			return MileageStatistics{}, err
		}
		result.Trend = append(result.Trend, point)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return MileageStatistics{}, err
	}
	if err := rows.Close(); err != nil {
		return MileageStatistics{}, err
	}
	ranking := buildMileageStatisticsRankingSQL(query)
	rows, err = s.db.QueryContext(ctx, ranking.Text, ranking.Args...)
	if err != nil {
		return MileageStatistics{}, err
	}
	defer rows.Close()
	for rows.Next() {
		var item MileageVehicleRank
		if err := rows.Scan(&item.VIN, &item.Plate, &item.MileageKm, &item.LatestMileageKm, &item.ActiveDays); err != nil {
			return MileageStatistics{}, err
		}
		result.Ranking = append(result.Ranking, item)
	}
	if err := rows.Err(); err != nil {
		return MileageStatistics{}, err
	}
	result.AsOf = time.Now().Format("2006-01-02 15:04:05")
	return result, nil
}

func buildQualityIssueWhere(query url.Values) (string, []any) {
	where := []string{"1 = 1"}
	args := []any{}
	if vin := strings.TrimSpace(query.Get("vin")); vin != "" {
		where = append(where, "q.vin = ?")
		args = append(args, vin)
	}
	if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
		where = append(where, "q.protocol = ?")
		args = append(args, protocol)
	}
	if issueType := strings.TrimSpace(query.Get("issueType")); issueType != "" {
		where = append(where, "q.issue_type = ?")
		args = append(args, issueType)
	}
	if keyword := strings.TrimSpace(query.Get("keyword")); keyword != "" {
		where = append(where, "(q.vin LIKE ? OR q.plate LIKE ? OR q.phone LIKE ? OR q.source_endpoint LIKE ?)")
		like := "%" + keyword + "%"
		args = append(args, like, like, like, like)
	}
	sourceSQL := `SELECT COALESCE(NULLIF(b.vin, ''), 'unknown') AS vin, COALESCE(b.plate, '') AS plate, r.phone AS phone, ` +
		`COALESCE(r.source_endpoint, '') AS source_endpoint, 'JT808' AS protocol, 'VIN_MISSING' AS issue_type, ` +
		`'warning' AS severity, COALESCE(DATE_FORMAT(r.latest_seen_at, '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
		`CONCAT('phone ', r.phone, ' 未命中 binding 表，来源 ', COALESCE(r.source_endpoint, '')) AS detail ` +
		`FROM jt808_registration r LEFT JOIN vehicle_identity_binding b ON b.phone = r.phone ` +
		`WHERE r.vin = '' OR r.vin = 'unknown' OR r.vin IS NULL ` +
		`UNION ALL ` +
		`SELECT b.vin AS vin, COALESCE(b.plate, '') AS plate, COALESCE(b.phone, '') AS phone, ` +
		`'vehicle_identity_binding' AS source_endpoint, 'VEHICLE_SERVICE' AS protocol, 'NO_SOURCE' AS issue_type, ` +
		`'warning' AS severity, '' AS last_seen, '车辆已绑定，但暂无 32960、808 或 MQTT 数据来源' AS detail ` +
		`FROM vehicle_identity_binding b LEFT JOIN vehicle_realtime_snapshot s ON s.vin = b.vin ` +
		`WHERE b.vin IS NOT NULL AND b.vin <> '' ` +
		`GROUP BY b.vin, b.plate, b.phone HAVING COUNT(DISTINCT s.protocol) = 0`
	return `FROM (` + sourceSQL + `) q WHERE ` + strings.Join(where, " AND "), args
}

func (s *ProductionStore) QualitySummary(ctx context.Context, query url.Values) (QualitySummary, error) {
	fromSQL, args := buildQualityIssueWhere(query)
	var summary QualitySummary
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT COALESCE(NULLIF(q.vin, ''), q.phone)) AS issue_vehicle_count, COUNT(*) AS issue_record_count `+fromSQL, args...).Scan(&summary.IssueVehicleCount, &summary.IssueRecordCount); err != nil {
		return QualitySummary{}, err
	}
	summary.WarningCount = summary.IssueRecordCount
	protocols, err := s.qualityBuckets(ctx, fromSQL, args, "q.protocol")
	if err != nil {
		return QualitySummary{}, err
	}
	issueTypes, err := s.qualityBuckets(ctx, fromSQL, args, "q.issue_type")
	if err != nil {
		return QualitySummary{}, err
	}
	summary.Protocols = protocols
	summary.IssueTypes = issueTypes
	return summary, nil
}

func (s *ProductionStore) qualityBuckets(ctx context.Context, fromSQL string, args []any, field string) ([]QualityBucketStat, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT `+field+`, COUNT(*) `+fromSQL+` GROUP BY `+field+` ORDER BY COUNT(*) DESC, `+field+` ASC`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []QualityBucketStat{}
	for rows.Next() {
		var item QualityBucketStat
		if err := rows.Scan(&item.Name, &item.Count); err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	return out, rows.Err()
}

func (s *ProductionStore) QualityIssues(ctx context.Context, query url.Values) (Page[QualityIssueRow], error) {
	limit, offset := buildLimitOffset(query)
	fromSQL, args := buildQualityIssueWhere(query)
	countArgs := append([]any(nil), args...)
	args = append(args, limit, offset)
	total := 0
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) `+fromSQL, countArgs...).Scan(&total); err != nil {
		return Page[QualityIssueRow]{}, err
	}
	rows, err := s.db.QueryContext(ctx, `SELECT q.vin, q.plate, q.phone, q.source_endpoint, q.protocol, q.issue_type, q.severity, q.last_seen, q.detail `+
		fromSQL+` ORDER BY q.last_seen DESC, q.vin ASC, q.phone ASC LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return Page[QualityIssueRow]{}, err
	}
	defer rows.Close()
	items := make([]QualityIssueRow, 0)
	for rows.Next() {
		var item QualityIssueRow
		if err := rows.Scan(&item.VIN, &item.Plate, &item.Phone, &item.SourceEndpoint, &item.Protocol, &item.IssueType, &item.Severity, &item.LastSeen, &item.Detail); err != nil {
			return Page[QualityIssueRow]{}, err
		}
		items = append(items, item)
	}
	return Page[QualityIssueRow]{Items: items, Total: total, Limit: limit, Offset: offset}, rows.Err()
}

func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
	mysqlStatus := "ok"
	mysqlDetail := "MySQL ping 正常"
	if err := s.db.PingContext(ctx); err != nil {
		mysqlStatus = "error"
		mysqlDetail = err.Error()
	}
	snapshotHealth := s.mysqlTableReadHealth(ctx, "vehicle_realtime_snapshot", "读取实时快照表正常")
	locationHealth := s.mysqlTableReadHealth(ctx, "vehicle_realtime_location", "读取实时位置表正常")
	tdengineHealth := s.tdengineRawFrameHealth(ctx)
	redisHealth, redisOnlineKeys := s.redisOnlineKeyHealth(ctx)
	capacityHealth, kafkaLag, activeConnections, capacityMetrics, capacityFindings := s.capacityCheckHealth(ctx)
	alertStreamHealth, alertStream := s.alertStreamCheckpointHealth(ctx)
	mysqlWritable := mysqlStatus == "ok" && snapshotHealth.Status == "ok" && locationHealth.Status == "ok"
	return OpsHealth{
		LinkHealth: []LinkHealth{
			{Name: "MySQL realtime", Status: mysqlStatus, Detail: mysqlDetail},
			snapshotHealth,
			locationHealth,
			tdengineHealth,
			capacityHealth,
			redisHealth,
			alertStreamHealth,
		},
		KafkaLag:          kafkaLag,
		ActiveConnections: activeConnections,
		CapacityMetrics:   capacityMetrics,
		CapacityFindings:  capacityFindings,
		RedisOnlineKeys:   redisOnlineKeys,
		TDengineWritable:  tdengineHealth.Status == "ok",
		MySQLWritable:     mysqlWritable,
		AlertStream:       alertStream,
	}, nil
}

func (s *ProductionStore) alertStreamCheckpointHealth(ctx context.Context) (LinkHealth, AlertStreamHealth) {
	result := AlertStreamHealth{Mode: firstNonEmpty(s.alertStreamMode, "disabled"), ConsumerGroup: s.alertStreamGroup}
	health := LinkHealth{Name: "Alert Kafka stream", Status: "warning", Detail: "Kafka event-time consumer 未配置"}
	if s.alertStreamGroup == "" {
		return health, result
	}
	var updatedAt, lastInvalidAt sql.NullTime
	err := s.db.QueryRowContext(ctx, `SELECT COUNT(*),COALESCE(SUM(GREATEST(high_watermark-next_offset,0)),0),COALESCE(SUM(processed_count),0),COALESCE(SUM(valid_count),0),COALESCE(SUM(invalid_count),0),COALESCE(SUM(late_count),0),COALESCE(SUM(replay_skipped_count),0),MAX(updated_at),COALESCE((SELECT c2.last_invalid_code FROM vehicle_alert_stream_checkpoint c2 WHERE c2.consumer_group=? AND c2.last_invalid_at IS NOT NULL ORDER BY c2.last_invalid_at DESC LIMIT 1),''),MAX(last_invalid_at) FROM vehicle_alert_stream_checkpoint WHERE consumer_group=?`, s.alertStreamGroup, s.alertStreamGroup).Scan(&result.Partitions, &result.Lag, &result.Processed, &result.Valid, &result.Invalid, &result.Late, &result.ReplaySkipped, &updatedAt, &result.LastInvalidCode, &lastInvalidAt)
	if err != nil {
		health.Status = "error"
		health.Detail = "读取告警流 checkpoint 失败：" + err.Error()
		return health, result
	}
	if updatedAt.Valid {
		result.UpdatedAt = updatedAt.Time.Format(time.RFC3339)
	}
	if lastInvalidAt.Valid {
		result.LastInvalidAt = lastInvalidAt.Time.Format(time.RFC3339)
	}
	if result.Partitions == 0 || !updatedAt.Valid {
		health.Detail = "告警流尚未建立 Kafka 分区 checkpoint"
		return health, result
	}
	age := time.Since(updatedAt.Time)
	health.Status = "ok"
	health.Detail = result.Mode + " checkpoint 正常"
	if age > 2*time.Minute {
		health.Status = "error"
		health.Detail = "告警流 checkpoint 超过 2 分钟未推进"
	} else if result.Lag > 1000 || (lastInvalidAt.Valid && time.Since(lastInvalidAt.Time) < 5*time.Minute) {
		health.Status = "warning"
		health.Detail = "告警流存在积压或最近非法消息"
		if result.LastInvalidCode != "" {
			health.Detail += "：" + result.LastInvalidCode
		}
	}
	return health, result
}

func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, *int, *int, CapacityMetrics, []string) {
	health := LinkHealth{Name: "Capacity check", Status: "warning", Detail: "平台暂未接入 capacity-check"}
	if s.capacityCheck == nil {
		return health, nil, nil, CapacityMetrics{}, []string{}
	}
	result, err := s.capacityCheck.CheckCapacity(ctx)
	if err != nil {
		health.Status = "error"
		health.Detail = err.Error()
		return health, nil, nil, CapacityMetrics{}, []string{}
	}
	capacityMetrics := result.CapacityMetrics
	kafkaLag := capacityMetrics.KafkaLag
	activeConnections := capacityMetrics.ActiveConnections
	detail := "active_connections=" + strconv.Itoa(activeConnections) + ", kafka_lag=" + strconv.Itoa(kafkaLag)
	if len(result.Findings) > 0 {
		detail += ", findings=" + strings.Join(result.Findings, "; ")
	}
	if strings.EqualFold(result.Status, "ok") {
		health.Status = "ok"
	} else {
		health.Status = "warning"
	}
	health.Detail = detail
	return health, &kafkaLag, &activeConnections, capacityMetrics, append([]string{}, result.Findings...)
}

func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) {
	health := LinkHealth{Name: "Redis online keys", Status: "warning", Detail: "平台暂未接入 Redis 在线 key 读取"}
	if s.redisOnline == nil {
		return health, nil
	}
	count, err := s.redisOnline.CountOnlineKeys(ctx)
	if err != nil {
		health.Status = "error"
		health.Detail = err.Error()
		return health, nil
	}
	health.Status = "ok"
	health.Detail = "Redis 在线 key 读取正常：" + strconv.Itoa(count)
	return health, &count
}

func (s *ProductionStore) mysqlTableReadHealth(ctx context.Context, table string, okDetail string) LinkHealth {
	health := LinkHealth{Name: table, Status: "ok", Detail: okDetail}
	query := "SELECT 1 FROM " + table + " LIMIT 1"
	rows, err := s.db.QueryContext(ctx, query)
	if err != nil {
		health.Status = "error"
		health.Detail = err.Error()
		return health
	}
	_ = rows.Close()
	return health
}

func (s *ProductionStore) tdengineRawFrameHealth(ctx context.Context) LinkHealth {
	health := LinkHealth{Name: "TDengine raw_frames", Status: "ok", Detail: "读取 TDengine raw_frames 正常"}
	if s.tdengine == nil {
		health.Status = "warning"
		health.Detail = "TDengine 未配置或连接失败，历史查询降级"
		return health
	}
	rows, err := s.tdengine.QueryContext(ctx, "SELECT ts FROM "+qualifyTDengine(s.tdDatabase, "raw_frames")+" LIMIT 1")
	if err != nil {
		health.Status = "error"
		health.Detail = err.Error()
		return health
	}
	_ = rows.Close()
	return health
}

func (s *ProductionStore) protocolStats(ctx context.Context) ([]ProtocolStat, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT protocol, SUM(CASE WHEN updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN 1 ELSE 0 END) AS online_count, COUNT(*) AS total_count FROM vehicle_realtime_snapshot GROUP BY protocol ORDER BY protocol`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]ProtocolStat, 0)
	for rows.Next() {
		var row ProtocolStat
		if err := rows.Scan(&row.Protocol, &row.Online, &row.Total); err != nil {
			return nil, err
		}
		out = append(out, row)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return completeProtocolStats(out), nil
}

func parsedFieldsFromString(value string) map[string]any {
	if strings.TrimSpace(value) == "" {
		return nil
	}
	var fields map[string]any
	if err := json.Unmarshal([]byte(value), &fields); err != nil {
		return map[string]any{"raw": value}
	}
	return fields
}

func filterParsedFieldsMap(fields map[string]any, names []string) map[string]any {
	if len(names) == 0 || len(fields) == 0 {
		return fields
	}
	out := make(map[string]any, len(names))
	for _, name := range names {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		if value, ok := fields[name]; ok {
			out[name] = value
		}
	}
	return out
}

func nullFloat64(value sql.NullFloat64) float64 {
	if value.Valid {
		return value.Float64
	}
	return 0
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func repeatPlaceholders(count int) string {
	if count <= 0 {
		return ""
	}
	return strings.TrimRight(strings.Repeat("?,", count), ",")
}
