package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"

	_ "github.com/go-sql-driver/mysql"

	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/authentication"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/eventbus"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/gateway"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/health"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/identity"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/observability"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/protocol/gb32960"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/protocol/jt808"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/topics"
)

func main() {
	logger := observability.NewLogger("vehicle-gateway")
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	registry := metrics.NewRegistry()
	sink, err := buildSink(ctx, logger, registry)
	if err != nil {
		logger.Error("build sink failed", "error", err)
		os.Exit(1)
	}
	defer sink.Close()
	resolver, jt808DeviceTokens, closeResolver, err := buildIdentityResolver(ctx, logger, registry)
	if err != nil {
		logger.Error("build identity resolver failed", "error", err)
		os.Exit(1)
	}
	defer closeResolver()
	health.Start(ctx, logger, health.NewServer(env("HEALTH_ADDR", ""), "vehicle-gateway", nil, registry))
	publishUnified := envBool("PUBLISH_UNIFIED_ENABLED", false)
	delegateFields := envBool("FIELDS_DERIVE_FROM_RAW_ENABLED", strings.TrimSpace(os.Getenv("NATS_URL")) != "")
	gb32960Authenticator, gb32960AuthMode, gb32960CredentialCount, err := buildGB32960Authenticator()
	if err != nil {
		logger.Error("build gb32960 authenticator failed", "error", err)
		os.Exit(1)
	}
	jt808AuthCode := env("JT808_REGISTER_AUTH_CODE", "g7gps")
	jt808Authenticator, jt808AuthMode, err := buildJT808Authenticator(jt808AuthCode, jt808DeviceTokens)
	if err != nil {
		logger.Error("build jt808 authenticator failed", "error", err)
		os.Exit(1)
	}
	recordAuthenticationConfig(registry, envelope.ProtocolGB32960, gb32960AuthMode, gb32960CredentialCount)
	recordAuthenticationConfig(registry, envelope.ProtocolJT808, jt808AuthMode, int(boolMetric(strings.TrimSpace(jt808AuthCode) != "")))
	logger.Info("protocol authentication configured", "gb32960_mode", gb32960AuthMode, "gb32960_accounts", gb32960CredentialCount, "jt808_mode", jt808AuthMode)

	protocols := []gateway.TCPProtocol{
		{
			Protocol:     envelope.ProtocolGB32960,
			Addr:         env("GB32960_TCP_ADDR", ":32960"),
			Extract:      gb32960.ExtractFrames,
			Parse:        gb32960.ParseFrame,
			Authenticate: gb32960Authenticator,
			Respond:      gb32960.AutoResponse,
		},
		{
			Protocol:     envelope.ProtocolJT808,
			Addr:         env("JT808_TCP_ADDR", ":808"),
			Extract:      jt808.ExtractFrames,
			Parse:        jt808.ParseFrame,
			Authenticate: jt808Authenticator,
			Respond:      jt808.NewAutoResponder(jt808AuthCode).Respond,
		},
	}

	var wg sync.WaitGroup
	errs := make(chan error, len(protocols))
	for _, protocol := range protocols {
		server, err := gateway.NewTCPServer(gateway.TCPServerConfig{
			Protocol:       protocol,
			Sink:           sink,
			Resolver:       resolver,
			Logger:         logger,
			Metrics:        registry,
			ReadBufferSize: envInt("TCP_READ_BUFFER_BYTES", 64*1024),
			IdleTimeout:    time.Duration(envInt("TCP_IDLE_TIMEOUT_SECONDS", 180)) * time.Second,
			MaxConnections: envInt("TCP_MAX_CONNECTIONS", 120_000),
			PublishUnified: publishUnified,
			DelegateFields: delegateFields,
		})
		if err != nil {
			logger.Error("build tcp server failed", "protocol", protocol.Protocol, "error", err)
			os.Exit(1)
		}
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := server.ListenAndServe(ctx); err != nil {
				errs <- err
			}
		}()
	}
	if envBool("YUTONG_MQTT_ENABLED", false) {
		client, err := gateway.NewMQTTClient(gateway.MQTTClientConfig{
			EndpointName:            env("YUTONG_MQTT_ENDPOINT", env("YUTONG_MQTT_ENDPOINT_NAME", "yutong")),
			Broker:                  env("YUTONG_MQTT_URI", ""),
			ClientID:                env("YUTONG_MQTT_CLIENT_ID", "lingniu-go-yutong-mqtt"),
			Username:                env("YUTONG_MQTT_USERNAME", ""),
			Password:                env("YUTONG_MQTT_PASSWORD", ""),
			Topics:                  splitCSV(env("YUTONG_MQTT_TOPICS", env("YUTONG_MQTT_TOPIC", "/ytforward/shln/+"))),
			QoS:                     byte(envInt("YUTONG_MQTT_QOS", 2)),
			CleanSession:            envBool("YUTONG_MQTT_CLEAN_SESSION", false),
			KeepAlive:               time.Duration(envInt("YUTONG_MQTT_KEEP_ALIVE_SECONDS", 20)) * time.Second,
			ConnectTimeout:          time.Duration(envInt("YUTONG_MQTT_CONNECTION_TIMEOUT_SECONDS", 10)) * time.Second,
			TLSCACertPath:           env("YUTONG_MQTT_TLS_CA_PEM", ""),
			TLSClientCertPath:       env("YUTONG_MQTT_TLS_CLIENT_PEM", ""),
			TLSClientKeyPath:        env("YUTONG_MQTT_TLS_CLIENT_KEY", ""),
			TLSHostnameVerification: envBool("YUTONG_MQTT_TLS_HOSTNAME_VERIFICATION_ENABLED", true),
			Sink:                    sink,
			Resolver:                resolver,
			Logger:                  logger,
			Metrics:                 registry,
			PublishUnified:          publishUnified,
			DelegateFields:          delegateFields,
		})
		if err != nil {
			logger.Error("build yutong mqtt client failed", "error", err)
			os.Exit(1)
		}
		if err := client.Start(ctx); err != nil {
			logger.Error("start yutong mqtt client failed", "error", err)
			os.Exit(1)
		}
		logger.Info("yutong mqtt client started")
	}

	logger.Info("vehicle gateway started", "fields_derive_from_raw_enabled", delegateFields)
	select {
	case <-ctx.Done():
	case err := <-errs:
		logger.Error("vehicle gateway listener failed", "error", err)
		stop()
	}
	wg.Wait()
}

func buildGB32960Authenticator() (authentication.Authenticator, authentication.Mode, int, error) {
	mode, err := authentication.ParseMode(os.Getenv("GB32960_AUTH_MODE"), authentication.ModeObserve)
	if err != nil {
		return nil, "", 0, err
	}
	credentials, err := loadGB32960Credentials()
	if err != nil {
		return nil, "", 0, err
	}
	if mode == authentication.ModeEnforce && len(credentials) == 0 {
		return nil, "", 0, fmt.Errorf("GB32960_AUTH_MODE=enforce requires configured platform credentials")
	}
	return authentication.NewGB32960PlatformAuthenticator(mode, credentials), mode, len(credentials), nil
}

func buildJT808Authenticator(authCode string, deviceTokens authentication.JT808DeviceTokenProvider) (authentication.Authenticator, authentication.Mode, error) {
	mode, err := authentication.ParseMode(os.Getenv("JT808_AUTH_MODE"), authentication.ModeObserve)
	if err != nil {
		return nil, "", err
	}
	if mode == authentication.ModeEnforce && strings.TrimSpace(authCode) == "" && deviceTokens == nil {
		return nil, "", fmt.Errorf("JT808_AUTH_MODE=enforce requires a configured or device token provider")
	}
	return authentication.NewJT808Authenticator(mode, authCode, deviceTokens), mode, nil
}

func loadGB32960Credentials() (map[string][]string, error) {
	payload := strings.TrimSpace(os.Getenv("GB32960_PLATFORM_CREDENTIALS_JSON"))
	if path := strings.TrimSpace(os.Getenv("GB32960_PLATFORM_CREDENTIALS_FILE")); path != "" {
		contents, err := os.ReadFile(path)
		if err != nil {
			return nil, fmt.Errorf("read gb32960 credentials file: %w", err)
		}
		payload = strings.TrimSpace(string(contents))
	}
	if payload == "" {
		return map[string][]string{}, nil
	}
	rawCredentials := map[string]json.RawMessage{}
	if err := json.Unmarshal([]byte(payload), &rawCredentials); err != nil {
		return nil, fmt.Errorf("parse gb32960 platform credentials: %w", err)
	}
	credentials := make(map[string][]string, len(rawCredentials))
	for username, rawPassword := range rawCredentials {
		trimmed := strings.TrimSpace(username)
		if trimmed == "" {
			continue
		}
		var passwords []string
		var single string
		if err := json.Unmarshal(rawPassword, &single); err == nil {
			passwords = []string{single}
		} else if err := json.Unmarshal(rawPassword, &passwords); err != nil {
			return nil, fmt.Errorf("parse gb32960 credentials for account %q: expected string or string array", trimmed)
		}
		for _, password := range passwords {
			if password != "" {
				credentials[trimmed] = append(credentials[trimmed], password)
			}
		}
		if len(credentials[trimmed]) == 0 {
			delete(credentials, trimmed)
		}
	}
	return credentials, nil
}

func recordAuthenticationConfig(registry *metrics.Registry, protocol envelope.Protocol, mode authentication.Mode, credentialCount int) {
	if registry == nil {
		return
	}
	registry.SetGauge("vehicle_gateway_authentication_mode", metrics.Labels{
		"protocol": string(protocol),
		"mode":     string(mode),
	}, 1)
	registry.SetGauge("vehicle_gateway_authentication_credentials", metrics.Labels{
		"protocol": string(protocol),
	}, float64(credentialCount))
}

func buildIdentityResolver(ctx context.Context, logger *slog.Logger, registry *metrics.Registry) (identity.Resolver, authentication.JT808DeviceTokenProvider, func(), error) {
	dsn := env("IDENTITY_MYSQL_DSN", strings.TrimSpace(os.Getenv("MYSQL_DSN")))
	if strings.TrimSpace(dsn) == "" {
		logger.Warn("identity mysql dsn is empty; using noop identity resolver")
		return identity.NoopResolver{}, nil, func() {}, nil
	}
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		logger.Error("identity mysql configuration failed; continuing with unresolved identities", "error", err)
		return identity.NoopResolver{}, nil, func() {}, nil
	}
	db.SetMaxOpenConns(envInt("IDENTITY_MYSQL_MAX_OPEN_CONNS", 16))
	db.SetMaxIdleConns(envInt("IDENTITY_MYSQL_MAX_IDLE_CONNS", 8))
	db.SetConnMaxLifetime(time.Duration(envInt("IDENTITY_MYSQL_CONN_MAX_LIFETIME_SECONDS", 300)) * time.Second)
	db.SetConnMaxIdleTime(time.Duration(envInt("IDENTITY_MYSQL_CONN_MAX_IDLE_SECONDS", 60)) * time.Second)
	table := env("VEHICLE_IDENTITY_TABLE", "vehicle_identity_binding")
	cacheMaxEntries := envInt("IDENTITY_LOOKUP_CACHE_MAX_ENTRIES", 300000)
	cacheCleanupInterval := time.Duration(envInt("IDENTITY_LOOKUP_CACHE_CLEANUP_INTERVAL_SECONDS", 60)) * time.Second
	staleLookupTTLSeconds := envInt("IDENTITY_STALE_LOOKUP_TTL_SECONDS", 3600)
	registrationGatewayWritesEnabled := envBool("JT808_REGISTRATION_GATEWAY_WRITES_ENABLED", false)
	resolver := identity.NewMySQLResolverWithOptions(db, table, identity.MySQLResolverOptions{
		SnapshotOnlyLookups:         envBool("IDENTITY_SNAPSHOT_ONLY_ENABLED", true),
		LocationTouchInterval:       time.Duration(envInt("JT808_REGISTRATION_LOCATION_TOUCH_INTERVAL_SECONDS", 600)) * time.Second,
		LocationTouchRetryInterval:  time.Duration(envInt("JT808_REGISTRATION_LOCATION_TOUCH_RETRY_INTERVAL_SECONDS", 5)) * time.Second,
		RegistrationWriteAttempts:   envInt("JT808_REGISTRATION_WRITE_RETRY_ATTEMPTS", 2),
		RegistrationWriteRetryDelay: time.Duration(envInt("JT808_REGISTRATION_WRITE_RETRY_DELAY_MS", 20)) * time.Millisecond,
		LookupCacheTTL:              time.Duration(envInt("IDENTITY_LOOKUP_CACHE_TTL_SECONDS", 600)) * time.Second,
		StaleLookupTTL:              time.Duration(staleLookupTTLSeconds) * time.Second,
		CacheCleanupInterval:        cacheCleanupInterval,
		MaxCacheEntries:             cacheMaxEntries,
		SourceCodeLookup:            envBool("IDENTITY_SOURCE_CODE_LOOKUP_ENABLED", true),
		AsyncRegistrationWrites:     envBool("JT808_REGISTRATION_ASYNC_WRITE_ENABLED", true),
		RegistrationWriteQueueSize:  envInt("JT808_REGISTRATION_WRITE_QUEUE_SIZE", 100000),
		RegistrationWriteWorkers:    envInt("JT808_REGISTRATION_WRITE_WORKERS", 4),
		RegistrationWriteTimeout:    time.Duration(envInt("JT808_REGISTRATION_WRITE_TIMEOUT_MS", 5000)) * time.Millisecond,
		RegistrationEnqueueTimeout:  time.Duration(envInt("JT808_REGISTRATION_WRITE_ENQUEUE_TIMEOUT_MS", 50)) * time.Millisecond,
		DisableRegistrationWrites:   !registrationGatewayWritesEnabled,
		OnRegistrationWriteResult: func(result identity.RegistrationWriteResult) {
			recordJT808RegistrationWriteResult(registry, result)
		},
		OnRegistrationWriteError: func(err error) {
			logger.Warn("jt808 registration async write failed", "error", err)
		},
	})
	startIdentityDatabaseMaintenance(
		ctx,
		logger,
		db,
		resolver,
		envBool("IDENTITY_MYSQL_ENSURE_SCHEMA", false),
		time.Duration(envInt("IDENTITY_MYSQL_PING_TIMEOUT_MS", 3000))*time.Millisecond,
		time.Duration(envInt("IDENTITY_MYSQL_SCHEMA_TIMEOUT_SECONDS", 10))*time.Second,
	)
	startIdentitySnapshotRefresh(
		ctx,
		logger,
		registry,
		resolver,
		time.Duration(envInt("IDENTITY_SNAPSHOT_REFRESH_INTERVAL_SECONDS", 60))*time.Second,
		time.Duration(envInt("IDENTITY_SNAPSHOT_REFRESH_TIMEOUT_SECONDS", 10))*time.Second,
	)
	startIdentityCacheMetrics(ctx, registry, resolver, time.Duration(envInt("IDENTITY_CACHE_METRICS_INTERVAL_SECONDS", 30))*time.Second)
	resolveTimeout := time.Duration(envInt("IDENTITY_RESOLVE_TIMEOUT_MS", 50)) * time.Millisecond
	registry.SetGauge("vehicle_gateway_jt808_registration_gateway_writes_enabled", nil, boolMetric(registrationGatewayWritesEnabled))
	logger.Info("identity mysql resolver enabled", "table", table, "snapshot_only_enabled", envBool("IDENTITY_SNAPSHOT_ONLY_ENABLED", true), "snapshot_refresh_interval_seconds", envInt("IDENTITY_SNAPSHOT_REFRESH_INTERVAL_SECONDS", 60), "lookup_cache_ttl_seconds", envInt("IDENTITY_LOOKUP_CACHE_TTL_SECONDS", 600), "stale_lookup_ttl_seconds", staleLookupTTLSeconds, "lookup_cache_max_entries", cacheMaxEntries, "lookup_cache_cleanup_interval_seconds", cacheCleanupInterval.Seconds(), "source_code_lookup_enabled", envBool("IDENTITY_SOURCE_CODE_LOOKUP_ENABLED", true), "resolve_timeout_ms", resolveTimeout.Milliseconds(), "jt808_registration_gateway_writes_enabled", registrationGatewayWritesEnabled, "jt808_registration_location_touch_retry_interval_seconds", envInt("JT808_REGISTRATION_LOCATION_TOUCH_RETRY_INTERVAL_SECONDS", 5), "jt808_registration_write_retry_attempts", envInt("JT808_REGISTRATION_WRITE_RETRY_ATTEMPTS", 2), "jt808_registration_write_retry_delay_ms", envInt("JT808_REGISTRATION_WRITE_RETRY_DELAY_MS", 20), "jt808_registration_async_write_enabled", envBool("JT808_REGISTRATION_ASYNC_WRITE_ENABLED", true), "jt808_registration_write_queue_size", envInt("JT808_REGISTRATION_WRITE_QUEUE_SIZE", 100000), "jt808_registration_write_workers", envInt("JT808_REGISTRATION_WRITE_WORKERS", 4), "jt808_registration_write_timeout_ms", envInt("JT808_REGISTRATION_WRITE_TIMEOUT_MS", 5000), "jt808_registration_write_enqueue_timeout_ms", envInt("JT808_REGISTRATION_WRITE_ENQUEUE_TIMEOUT_MS", 50))
	return identity.TimeoutResolver{Delegate: resolver, Timeout: resolveTimeout}, resolver, func() {
		_ = resolver.Close()
		_ = db.Close()
	}, nil
}

type identitySnapshotRefresher interface {
	RefreshSnapshot(context.Context) (identity.SnapshotRefreshResult, error)
}

type identityDatabasePinger interface {
	PingContext(context.Context) error
}

type identitySchemaEnsurer interface {
	EnsureSchema(context.Context) error
}

func startIdentityDatabaseMaintenance(ctx context.Context, logger *slog.Logger, db identityDatabasePinger, schema identitySchemaEnsurer, ensureSchema bool, pingTimeout time.Duration, schemaTimeout time.Duration) {
	if db == nil {
		return
	}
	if pingTimeout <= 0 {
		pingTimeout = 3 * time.Second
	}
	if schemaTimeout <= 0 {
		schemaTimeout = 10 * time.Second
	}
	go func() {
		pingCtx, cancelPing := context.WithTimeout(ctx, pingTimeout)
		err := db.PingContext(pingCtx)
		cancelPing()
		if err != nil {
			logger.Warn("identity mysql unavailable; ingress remains enabled and snapshot refresh will retry", "error", err)
			return
		}
		if !ensureSchema || schema == nil {
			return
		}
		schemaCtx, cancelSchema := context.WithTimeout(ctx, schemaTimeout)
		err = schema.EnsureSchema(schemaCtx)
		cancelSchema()
		if err != nil {
			logger.Warn("identity mysql schema check failed; ingress remains enabled", "error", err)
		}
	}()
}

func startIdentitySnapshotRefresh(ctx context.Context, logger *slog.Logger, registry *metrics.Registry, refresher identitySnapshotRefresher, interval time.Duration, timeout time.Duration) {
	if refresher == nil {
		return
	}
	if interval <= 0 {
		interval = time.Minute
	}
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	refresh := func() {
		refreshCtx, cancel := context.WithTimeout(ctx, timeout)
		result, err := refresher.RefreshSnapshot(refreshCtx)
		cancel()
		if err != nil {
			recordIdentitySnapshotRefresh(registry, identity.SnapshotRefreshResult{}, "error")
			logger.Warn("identity snapshot refresh failed; keeping last known good snapshot", "error", err)
			return
		}
		recordIdentitySnapshotRefresh(registry, result, "ok")
		logger.Info("identity snapshot refreshed", "binding_entries", result.BindingEntries, "identifier_entries", result.IdentifierEntries, "registration_entries", result.RegistrationEntries, "source_entries", result.SourceEntries)
	}
	go func() {
		refresh()
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				refresh()
			}
		}
	}()
}

func recordIdentitySnapshotRefresh(registry *metrics.Registry, result identity.SnapshotRefreshResult, status string) {
	if registry == nil {
		return
	}
	registry.IncCounter("vehicle_gateway_identity_snapshot_refresh_total", metrics.Labels{"status": status})
	if status != "ok" {
		return
	}
	for _, item := range []struct {
		kind  string
		value int
	}{
		{kind: "binding", value: result.BindingEntries},
		{kind: "identifier", value: result.IdentifierEntries},
		{kind: "registration", value: result.RegistrationEntries},
		{kind: "source", value: result.SourceEntries},
	} {
		registry.SetGauge("vehicle_gateway_identity_snapshot_entries", metrics.Labels{"kind": item.kind}, float64(item.value))
	}
	registry.SetGauge("vehicle_gateway_identity_snapshot_last_success_unix_seconds", nil, float64(result.RefreshedAt.Unix()))
}

type identityCacheStatsReporter interface {
	CacheStats() identity.CacheStats
}

func startIdentityCacheMetrics(ctx context.Context, registry *metrics.Registry, reporter identityCacheStatsReporter, interval time.Duration) {
	if registry == nil || reporter == nil {
		return
	}
	if interval <= 0 {
		interval = 30 * time.Second
	}
	recordIdentityCacheStats(registry, reporter.CacheStats())
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				recordIdentityCacheStats(registry, reporter.CacheStats())
			}
		}
	}()
}

func recordIdentityCacheStats(registry *metrics.Registry, stats identity.CacheStats) {
	if registry == nil {
		return
	}
	for _, item := range []struct {
		name  string
		value int
		max   int
	}{
		{name: "lookup", value: stats.LookupEntries, max: stats.MaxEntries},
		{name: "registration", value: stats.RegistrationEntries, max: stats.MaxEntries},
		{name: "source_code", value: stats.SourceCodeEntries, max: stats.MaxEntries},
		{name: "location_touch", value: stats.LocationTouchEntries, max: stats.MaxEntries},
		{name: "location_touch_failure", value: stats.LocationTouchFailureEntries, max: stats.MaxEntries},
		{name: "registration_write_queue", value: stats.RegistrationWriteQueueDepth, max: stats.RegistrationWriteQueueCap},
		{name: "snapshot_binding", value: stats.SnapshotBindingEntries, max: stats.MaxEntries},
		{name: "snapshot_identifier", value: stats.SnapshotIdentifierEntries, max: stats.MaxEntries},
		{name: "snapshot_registration", value: stats.SnapshotRegistrationEntries, max: stats.MaxEntries},
		{name: "snapshot_source", value: stats.SnapshotSourceEntries, max: stats.MaxEntries},
	} {
		registry.SetGauge("vehicle_gateway_identity_cache_entries", metrics.Labels{"cache": item.name}, float64(item.value))
		registry.SetGauge("vehicle_gateway_identity_cache_max_entries", metrics.Labels{"cache": item.name}, float64(item.max))
	}
	ready := 0.0
	if stats.SnapshotReady {
		ready = 1
	}
	registry.SetGauge("vehicle_gateway_identity_snapshot_ready", nil, ready)
	if !stats.SnapshotRefreshedAt.IsZero() {
		registry.SetGauge("vehicle_gateway_identity_snapshot_last_success_unix_seconds", nil, float64(stats.SnapshotRefreshedAt.Unix()))
	}
}

func recordJT808RegistrationWriteResult(registry *metrics.Registry, result identity.RegistrationWriteResult) {
	if registry == nil {
		return
	}
	mode := strings.TrimSpace(result.Mode)
	if mode == "" {
		mode = "unknown"
	}
	status := strings.TrimSpace(result.Status)
	if status == "" {
		status = "unknown"
	}
	registry.IncCounter("vehicle_gateway_jt808_registration_write_total", metrics.Labels{
		"mode":   mode,
		"status": status,
	})
	metrics.RecordLastActivity(registry, "vehicle_gateway_last_jt808_registration_write_unix_seconds", metrics.Labels{
		"mode":   mode,
		"status": status,
	})
}

func buildSink(ctx context.Context, logger *slog.Logger, registry *metrics.Registry) (eventbus.Sink, error) {
	if strings.TrimSpace(os.Getenv("NATS_URL")) != "" {
		natsConfig := natsSinkConfigFromEnv()
		sink, err := eventbus.NewNATSSink(natsConfig)
		if err != nil {
			return nil, err
		}
		outboxRuntime := natsOutboxConfigFromEnv(registry, func(err error) {
			logger.Warn("nats durable outbox publish failed", "error", err)
		})
		if outboxRuntime.Enabled {
			outbox, err := eventbus.NewDurableOutboxSink(sink, outboxRuntime.Config)
			if err != nil {
				_ = sink.Close()
				return nil, err
			}
			go outbox.ReplayLoop(ctx, outboxRuntime.ReplayInterval)
			logger.Info("nats durable outbox enabled",
				"dir", outboxRuntime.Config.Directory,
				"fsync", outboxRuntime.Config.SyncWrites,
				"replay_interval_ms", outboxRuntime.ReplayInterval.Milliseconds(),
				"replay_batch_size", outboxRuntime.Config.ReplayBatchSize,
				"close_timeout_ms", outboxRuntime.Config.CloseTimeout.Milliseconds(),
				"wal_segment_bytes", outboxRuntime.Config.WALSegmentBytes,
				"wal_segment_age_ms", outboxRuntime.Config.WALSegmentAge.Milliseconds(),
				"wal_append_queue_size", outboxRuntime.Config.WALAppendQueue,
				"wal_commit_batch_size", outboxRuntime.Config.WALCommitBatch,
				"wal_commit_interval_ms", outboxRuntime.Config.WALCommitWait.Milliseconds(),
				"max_inflight", natsConfig.AsyncMaxPending,
				"ack_timeout_ms", natsConfig.AsyncAckTimeout.Milliseconds(),
			)
			return outbox, nil
		}
		var out eventbus.Sink = eventbus.NewRetryingSink(sink, eventbus.RetryConfig{
			Attempts:       envInt("NATS_PUBLISH_ATTEMPTS", 3),
			Backoff:        time.Duration(envInt("NATS_PUBLISH_BACKOFF_MS", 100)) * time.Millisecond,
			AttemptTimeout: time.Duration(envInt("NATS_PUBLISH_TIMEOUT_MS", 3000)) * time.Millisecond,
		})
		spoolDir := strings.TrimSpace(os.Getenv("NATS_SPOOL_DIR"))
		if spoolDir != "" {
			replayBatchSize := envInt("NATS_SPOOL_REPLAY_BATCH_SIZE", 200)
			durable := eventbus.NewDurableSink(out, eventbus.DurableConfig{
				Directory:       spoolDir,
				ReplayBatchSize: replayBatchSize,
				Metrics:         registry,
				Name:            "nats",
			})
			interval := time.Duration(envInt("NATS_SPOOL_REPLAY_INTERVAL_MS", 1000)) * time.Millisecond
			go durable.ReplayLoop(ctx, interval, func(err error) {
				logger.Warn("nats spool replay failed", "error", err)
			})
			logger.Info("nats durable spool enabled", "dir", spoolDir, "replay_interval_ms", interval.Milliseconds(), "replay_batch_size", replayBatchSize)
			out = durable
		}
		if envBool("NATS_ASYNC_ENABLED", true) {
			queueSize := envInt("NATS_ASYNC_QUEUE_SIZE", envInt("KAFKA_ASYNC_QUEUE_SIZE", 100000))
			workers := envInt("NATS_ASYNC_WORKERS", envInt("KAFKA_ASYNC_WORKERS", 8))
			enqueueTimeout := time.Duration(envInt("NATS_ASYNC_ENQUEUE_TIMEOUT_MS", envInt("KAFKA_ASYNC_ENQUEUE_TIMEOUT_MS", 1000))) * time.Millisecond
			timeout := time.Duration(envInt("NATS_ASYNC_PUBLISH_TIMEOUT_MS", envInt("KAFKA_ASYNC_PUBLISH_TIMEOUT_MS", 30000))) * time.Millisecond
			if envBool("NATS_PARTITIONED_ASYNC_ENABLED", true) {
				rawQueueSize, derivedQueueSize, rawWorkers, derivedWorkers := partitionedAsyncConfigFromEnv("NATS", queueSize, workers)
				rawEnqueueTimeout, derivedEnqueueTimeout := partitionedAsyncEnqueueTimeoutsFromEnv("NATS", enqueueTimeout)
				out = eventbus.NewPartitionedAsyncSink(out, eventbus.PartitionedAsyncConfig{
					RawQueueSize:          rawQueueSize,
					DerivedQueueSize:      derivedQueueSize,
					RawWorkers:            rawWorkers,
					DerivedWorkers:        derivedWorkers,
					RawEnqueueTimeout:     rawEnqueueTimeout,
					DerivedEnqueueTimeout: derivedEnqueueTimeout,
					EnqueueTimeout:        enqueueTimeout,
					OperationTimeout:      timeout,
					Metrics:               registry,
					Name:                  "nats",
					OnError: func(err error) {
						logger.Warn("nats partitioned async publish failed", "error", err)
					},
				})
				logger.Info("nats partitioned async publish enabled", "raw_queue_size", rawQueueSize, "derived_queue_size", derivedQueueSize, "raw_workers", rawWorkers, "derived_workers", derivedWorkers, "raw_enqueue_timeout_ms", rawEnqueueTimeout.Milliseconds(), "derived_enqueue_timeout_ms", derivedEnqueueTimeout.Milliseconds(), "publish_timeout_ms", timeout.Milliseconds())
			} else {
				out = eventbus.NewAsyncSink(out, eventbus.AsyncConfig{
					QueueSize:        queueSize,
					Workers:          workers,
					EnqueueTimeout:   enqueueTimeout,
					OperationTimeout: timeout,
					Metrics:          registry,
					Name:             "nats",
					OnError: func(err error) {
						logger.Warn("nats async publish failed", "error", err)
					},
				})
				logger.Info("nats async publish enabled", "queue_size", queueSize, "workers", workers, "enqueue_timeout_ms", enqueueTimeout.Milliseconds(), "publish_timeout_ms", timeout.Milliseconds())
			}
		}
		logger.Info("nats jetstream sink enabled", "url", env("NATS_URL", ""), "unified_subject", natsSinkConfigFromEnv().UnifiedSubject, "publish_unified_enabled", envBool("PUBLISH_UNIFIED_ENABLED", false), "publish_fields_enabled", !envBool("FIELDS_DERIVE_FROM_RAW_ENABLED", true))
		return out, nil
	}
	brokers := splitCSV(os.Getenv("KAFKA_BROKERS"))
	if len(brokers) == 0 {
		logger.Warn("KAFKA_BROKERS is empty; using log sink")
		return eventbus.NewLogSink(logger), nil
	}
	sink, err := eventbus.NewKafkaSink(eventbus.KafkaConfig{
		Brokers: brokers,
		RawTopics: map[envelope.Protocol]string{
			envelope.ProtocolGB32960:    env("KAFKA_TOPIC_GB32960_RAW", topics.RawGB32960),
			envelope.ProtocolJT808:      env("KAFKA_TOPIC_JT808_RAW", topics.RawJT808),
			envelope.ProtocolYutongMQTT: env("KAFKA_TOPIC_YUTONG_MQTT_RAW", topics.RawYutongMQTT),
		},
		FieldsTopics: map[envelope.Protocol]string{
			envelope.ProtocolGB32960:    env("KAFKA_TOPIC_GB32960_FIELDS", topics.FieldsGB32960),
			envelope.ProtocolJT808:      env("KAFKA_TOPIC_JT808_FIELDS", topics.FieldsJT808),
			envelope.ProtocolYutongMQTT: env("KAFKA_TOPIC_YUTONG_MQTT_FIELDS", topics.FieldsYutongMQTT),
		},
		UnifiedTopic: env("KAFKA_TOPIC_UNIFIED", topics.Unified),
	})
	if err != nil {
		return nil, err
	}
	var out eventbus.Sink = eventbus.NewRetryingSink(sink, eventbus.RetryConfig{
		Attempts:       envInt("KAFKA_PUBLISH_ATTEMPTS", 3),
		Backoff:        time.Duration(envInt("KAFKA_PUBLISH_BACKOFF_MS", 100)) * time.Millisecond,
		AttemptTimeout: time.Duration(envInt("KAFKA_PUBLISH_TIMEOUT_MS", 3000)) * time.Millisecond,
	})
	spoolDir := strings.TrimSpace(os.Getenv("KAFKA_SPOOL_DIR"))
	if spoolDir != "" {
		replayBatchSize := envInt("KAFKA_SPOOL_REPLAY_BATCH_SIZE", 200)
		durable := eventbus.NewDurableSink(out, eventbus.DurableConfig{
			Directory:       spoolDir,
			ReplayBatchSize: replayBatchSize,
			Metrics:         registry,
			Name:            "kafka",
		})
		interval := time.Duration(envInt("KAFKA_SPOOL_REPLAY_INTERVAL_MS", 1000)) * time.Millisecond
		go durable.ReplayLoop(ctx, interval, func(err error) {
			logger.Warn("kafka spool replay failed", "error", err)
		})
		logger.Info("kafka durable spool enabled", "dir", spoolDir, "replay_interval_ms", interval.Milliseconds(), "replay_batch_size", replayBatchSize)
		out = durable
	}
	if envBool("KAFKA_ASYNC_ENABLED", true) {
		queueSize := envInt("KAFKA_ASYNC_QUEUE_SIZE", 100000)
		workers := envInt("KAFKA_ASYNC_WORKERS", 8)
		enqueueTimeout := time.Duration(envInt("KAFKA_ASYNC_ENQUEUE_TIMEOUT_MS", 1000)) * time.Millisecond
		timeout := time.Duration(envInt("KAFKA_ASYNC_PUBLISH_TIMEOUT_MS", 30000)) * time.Millisecond
		if envBool("KAFKA_PARTITIONED_ASYNC_ENABLED", true) {
			rawQueueSize, derivedQueueSize, rawWorkers, derivedWorkers := partitionedAsyncConfigFromEnv("KAFKA", queueSize, workers)
			rawEnqueueTimeout, derivedEnqueueTimeout := partitionedAsyncEnqueueTimeoutsFromEnv("KAFKA", enqueueTimeout)
			out = eventbus.NewPartitionedAsyncSink(out, eventbus.PartitionedAsyncConfig{
				RawQueueSize:          rawQueueSize,
				DerivedQueueSize:      derivedQueueSize,
				RawWorkers:            rawWorkers,
				DerivedWorkers:        derivedWorkers,
				RawEnqueueTimeout:     rawEnqueueTimeout,
				DerivedEnqueueTimeout: derivedEnqueueTimeout,
				EnqueueTimeout:        enqueueTimeout,
				OperationTimeout:      timeout,
				Metrics:               registry,
				Name:                  "kafka",
				OnError: func(err error) {
					logger.Warn("kafka partitioned async publish failed", "error", err)
				},
			})
			logger.Info("kafka partitioned async publish enabled", "raw_queue_size", rawQueueSize, "derived_queue_size", derivedQueueSize, "raw_workers", rawWorkers, "derived_workers", derivedWorkers, "raw_enqueue_timeout_ms", rawEnqueueTimeout.Milliseconds(), "derived_enqueue_timeout_ms", derivedEnqueueTimeout.Milliseconds(), "publish_timeout_ms", timeout.Milliseconds())
		} else {
			out = eventbus.NewAsyncSink(out, eventbus.AsyncConfig{
				QueueSize:        queueSize,
				Workers:          workers,
				EnqueueTimeout:   enqueueTimeout,
				OperationTimeout: timeout,
				Metrics:          registry,
				Name:             "kafka",
				OnError: func(err error) {
					logger.Warn("kafka async publish failed", "error", err)
				},
			})
			logger.Info("kafka async publish enabled", "queue_size", queueSize, "workers", workers, "enqueue_timeout_ms", enqueueTimeout.Milliseconds(), "publish_timeout_ms", timeout.Milliseconds())
		}
	}
	return out, nil
}

func partitionedAsyncConfigFromEnv(prefix string, queueSize int, workers int) (rawQueueSize int, derivedQueueSize int, rawWorkers int, derivedWorkers int) {
	derivedQueueDefault := queueSize / 2
	if derivedQueueDefault <= 0 {
		derivedQueueDefault = 1
	}
	derivedWorkersDefault := workers / 2
	if derivedWorkersDefault <= 0 {
		derivedWorkersDefault = 1
	}
	rawQueueSize = envInt(prefix+"_ASYNC_RAW_QUEUE_SIZE", queueSize)
	derivedQueueSize = envInt(prefix+"_ASYNC_DERIVED_QUEUE_SIZE", derivedQueueDefault)
	rawWorkers = envInt(prefix+"_ASYNC_RAW_WORKERS", workers)
	derivedWorkers = envInt(prefix+"_ASYNC_DERIVED_WORKERS", derivedWorkersDefault)
	return rawQueueSize, derivedQueueSize, rawWorkers, derivedWorkers
}

func partitionedAsyncEnqueueTimeoutsFromEnv(prefix string, enqueueTimeout time.Duration) (rawEnqueueTimeout time.Duration, derivedEnqueueTimeout time.Duration) {
	rawEnqueueTimeout = time.Duration(envInt(prefix+"_ASYNC_RAW_ENQUEUE_TIMEOUT_MS", int(enqueueTimeout/time.Millisecond))) * time.Millisecond
	derivedEnqueueTimeout = time.Duration(envInt(prefix+"_ASYNC_DERIVED_ENQUEUE_TIMEOUT_MS", 50)) * time.Millisecond
	return rawEnqueueTimeout, derivedEnqueueTimeout
}

func natsSinkConfigFromEnv() eventbus.NATSConfig {
	return eventbus.NATSConfig{
		URL:             env("NATS_URL", ""),
		Name:            env("NATS_CLIENT_NAME", "lingniu-vehicle-gateway"),
		AsyncMaxPending: envInt("NATS_OUTBOX_MAX_INFLIGHT", 10_000),
		AsyncAckTimeout: time.Duration(envInt("NATS_OUTBOX_ACK_TIMEOUT_MS", 3000)) * time.Millisecond,
		RawSubjects: map[envelope.Protocol]string{
			envelope.ProtocolGB32960:    env("NATS_SUBJECT_GB32960_RAW", env("KAFKA_TOPIC_GB32960_RAW", topics.RawGB32960)),
			envelope.ProtocolJT808:      env("NATS_SUBJECT_JT808_RAW", env("KAFKA_TOPIC_JT808_RAW", topics.RawJT808)),
			envelope.ProtocolYutongMQTT: env("NATS_SUBJECT_YUTONG_MQTT_RAW", env("KAFKA_TOPIC_YUTONG_MQTT_RAW", topics.RawYutongMQTT)),
		},
		FieldsSubjects: map[envelope.Protocol]string{
			envelope.ProtocolGB32960:    env("NATS_SUBJECT_GB32960_FIELDS", env("KAFKA_TOPIC_GB32960_FIELDS", topics.FieldsGB32960)),
			envelope.ProtocolJT808:      env("NATS_SUBJECT_JT808_FIELDS", env("KAFKA_TOPIC_JT808_FIELDS", topics.FieldsJT808)),
			envelope.ProtocolYutongMQTT: env("NATS_SUBJECT_YUTONG_MQTT_FIELDS", env("KAFKA_TOPIC_YUTONG_MQTT_FIELDS", topics.FieldsYutongMQTT)),
		},
		UnifiedSubject: env("NATS_SUBJECT_UNIFIED", env("KAFKA_TOPIC_UNIFIED", topics.Unified)),
	}
}

type natsOutboxRuntimeConfig struct {
	Enabled        bool
	Config         eventbus.DurableOutboxConfig
	ReplayInterval time.Duration
}

func natsOutboxConfigFromEnv(registry *metrics.Registry, onError func(error)) natsOutboxRuntimeConfig {
	directory := strings.TrimSpace(os.Getenv("NATS_OUTBOX_DIR"))
	return natsOutboxRuntimeConfig{
		Enabled: envBool("NATS_DURABLE_OUTBOX_ENABLED", false),
		Config: eventbus.DurableOutboxConfig{
			Directory:       directory,
			ReplayBatchSize: envInt("NATS_OUTBOX_REPLAY_BATCH_SIZE", 1000),
			SyncWrites:      envBool("NATS_OUTBOX_FSYNC", true),
			CloseTimeout:    time.Duration(envInt("NATS_OUTBOX_CLOSE_TIMEOUT_MS", 5000)) * time.Millisecond,
			WALSegmentBytes: int64(envInt("NATS_OUTBOX_WAL_SEGMENT_BYTES", 16<<20)),
			WALSegmentAge:   time.Duration(envInt("NATS_OUTBOX_WAL_SEGMENT_AGE_MS", 5000)) * time.Millisecond,
			WALAppendQueue:  envInt("NATS_OUTBOX_WAL_APPEND_QUEUE_SIZE", 100_000),
			WALCommitBatch:  envInt("NATS_OUTBOX_WAL_COMMIT_BATCH_SIZE", 256),
			WALCommitWait:   time.Duration(envInt("NATS_OUTBOX_WAL_COMMIT_INTERVAL_MS", 1)) * time.Millisecond,
			Metrics:         registry,
			Name:            "nats-outbox",
			OnError:         onError,
		},
		ReplayInterval: time.Duration(envInt("NATS_OUTBOX_REPLAY_INTERVAL_MS", 1000)) * time.Millisecond,
	}
}

func env(key string, fallback string) string {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	return value
}

func envInt(key string, fallback int) int {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	var out int
	for _, r := range value {
		if r < '0' || r > '9' {
			return fallback
		}
		out = out*10 + int(r-'0')
	}
	if out <= 0 {
		return fallback
	}
	return out
}

func envBool(key string, fallback bool) bool {
	value := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
	if value == "" {
		return fallback
	}
	return value == "1" || value == "true" || value == "yes" || value == "on"
}

func boolMetric(value bool) float64 {
	if value {
		return 1
	}
	return 0
}

func splitCSV(value string) []string {
	var out []string
	for _, item := range strings.Split(value, ",") {
		item = strings.TrimSpace(item)
		if item != "" {
			out = append(out, item)
		}
	}
	return out
}
