package gateway

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"strings"
	"sync"
	"syscall"
	"time"

	"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/identity"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/realtime"
)

type FrameExtractor func([]byte) (frames [][]byte, remainder []byte, err error)

type FrameParser func(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error)

type FrameResponder func(raw []byte, env envelope.FrameEnvelope) (response []byte, ok bool, err error)

type TCPProtocol struct {
	Protocol     envelope.Protocol
	Addr         string
	Extract      FrameExtractor
	Parse        FrameParser
	Authenticate authentication.Authenticator
	Respond      FrameResponder
}

type connectionState struct {
	platformName string
}

type TCPServer struct {
	protocol       TCPProtocol
	sink           eventbus.Sink
	resolver       identity.Resolver
	logger         *slog.Logger
	metrics        *metrics.Registry
	readBufferSize int
	idleTimeout    time.Duration
	maxConnections int
	publishUnified bool
	delegateFields bool
	activeMu       sync.Mutex
	activeConns    map[net.Conn]struct{}
}

type TCPServerConfig struct {
	Protocol       TCPProtocol
	Sink           eventbus.Sink
	Resolver       identity.Resolver
	Logger         *slog.Logger
	Metrics        *metrics.Registry
	ReadBufferSize int
	IdleTimeout    time.Duration
	MaxConnections int
	PublishUnified bool
	DelegateFields bool
}

const frameOperationTimeout = 30 * time.Second

var gatewayFrameDurationBucketsMS = []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000}
var gatewayResponseDurationBucketsMS = []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000}
var gatewayResponseE2ERecent = metrics.NewRecentLatencyByKey(512)

func NewTCPServer(cfg TCPServerConfig) (*TCPServer, error) {
	if cfg.Protocol.Protocol == "" {
		return nil, errors.New("protocol is required")
	}
	if strings.TrimSpace(cfg.Protocol.Addr) == "" {
		return nil, errors.New("listen addr is required")
	}
	if cfg.Protocol.Extract == nil {
		return nil, errors.New("frame extractor is required")
	}
	if cfg.Protocol.Parse == nil {
		return nil, errors.New("frame parser is required")
	}
	if cfg.Sink == nil {
		return nil, errors.New("sink is required")
	}
	if cfg.Logger == nil {
		cfg.Logger = slog.Default()
	}
	if cfg.Resolver == nil {
		cfg.Resolver = identity.NoopResolver{}
	}
	if cfg.ReadBufferSize <= 0 {
		cfg.ReadBufferSize = 32 * 1024
	}
	if cfg.IdleTimeout <= 0 {
		cfg.IdleTimeout = 2 * time.Minute
	}
	if cfg.MaxConnections <= 0 {
		cfg.MaxConnections = 10_000
	}
	return &TCPServer{
		protocol:       cfg.Protocol,
		sink:           cfg.Sink,
		resolver:       cfg.Resolver,
		logger:         cfg.Logger,
		metrics:        cfg.Metrics,
		readBufferSize: cfg.ReadBufferSize,
		idleTimeout:    cfg.IdleTimeout,
		maxConnections: cfg.MaxConnections,
		publishUnified: cfg.PublishUnified,
		delegateFields: cfg.DelegateFields,
		activeConns:    map[net.Conn]struct{}{},
	}, nil
}

func (s *TCPServer) ListenAndServe(ctx context.Context) error {
	var lc net.ListenConfig
	listener, err := lc.Listen(ctx, "tcp", s.protocol.Addr)
	if err != nil {
		return err
	}
	defer listener.Close()

	go func() {
		<-ctx.Done()
		_ = listener.Close()
		s.closeActiveConnections()
	}()

	s.logger.Info("tcp listener started", "protocol", s.protocol.Protocol, "addr", listener.Addr().String())
	sem := make(chan struct{}, s.maxConnections)
	var wg sync.WaitGroup
	defer wg.Wait()

	for {
		conn, err := listener.Accept()
		if err != nil {
			if ctx.Err() != nil {
				return nil
			}
			s.logger.Warn("tcp accept failed", "protocol", s.protocol.Protocol, "error", err)
			continue
		}
		select {
		case sem <- struct{}{}:
			wg.Add(1)
			go func() {
				defer wg.Done()
				defer func() { <-sem }()
				s.handleConnection(ctx, conn)
			}()
		default:
			s.recordConnectionRejection("max_connections")
			s.recordConnectionClose("max_connections")
			s.logger.Warn("tcp connection rejected: max connections reached", "protocol", s.protocol.Protocol, "remote", conn.RemoteAddr().String())
			_ = conn.Close()
		}
	}
}

func (s *TCPServer) handleConnection(ctx context.Context, conn net.Conn) {
	defer conn.Close()
	s.trackConnection(conn)
	defer s.untrackConnection(conn)

	source := conn.RemoteAddr().String()
	log := s.logger.With("protocol", s.protocol.Protocol, "remote", source)
	s.recordConnectionMetric(1)
	defer s.recordConnectionMetric(-1)
	log.Debug("tcp connection opened")
	defer log.Debug("tcp connection closed")

	readBuffer := make([]byte, s.readBufferSize)
	var pending []byte
	state := &connectionState{}
	for {
		_ = conn.SetReadDeadline(time.Now().Add(s.idleTimeout))
		n, err := conn.Read(readBuffer)
		if n > 0 {
			pending = append(pending, readBuffer[:n]...)
			frames, remainder, extractErr := s.protocol.Extract(pending)
			if extractErr != nil {
				log.Warn("frame extraction failed", "error", extractErr)
				s.recordConnectionClose("extract_error")
				return
			}
			pending = remainder
			for _, frame := range frames {
				s.handleFrame(ctx, conn, frame, source, state)
			}
		}
		if err != nil {
			if ctx.Err() != nil {
				s.recordConnectionClose("context_cancelled")
				return
			}
			var netErr net.Error
			if errors.As(err, &netErr) && netErr.Timeout() {
				log.Debug("tcp connection idle timeout")
				s.recordConnectionClose("read_timeout")
				return
			}
			if isRoutineTCPReadClose(err) {
				log.Debug("tcp connection closed by peer", "error", err)
				s.recordConnectionClose("remote_closed")
				return
			}
			log.Warn("tcp read failed", "error", err)
			s.recordConnectionClose("read_error")
			return
		}
		if ctx.Err() != nil {
			s.recordConnectionClose("context_cancelled")
			return
		}
	}
}

func isRoutineTCPReadClose(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, io.EOF) ||
		errors.Is(err, net.ErrClosed) ||
		errors.Is(err, syscall.ECONNRESET) ||
		errors.Is(err, syscall.EPIPE) {
		return true
	}
	text := strings.ToLower(err.Error())
	return strings.Contains(text, "connection reset by peer") ||
		strings.Contains(text, "broken pipe") ||
		strings.Contains(text, "use of closed network connection")
}

func (s *TCPServer) trackConnection(conn net.Conn) {
	if conn == nil {
		return
	}
	s.activeMu.Lock()
	defer s.activeMu.Unlock()
	if s.activeConns == nil {
		s.activeConns = map[net.Conn]struct{}{}
	}
	s.activeConns[conn] = struct{}{}
}

func (s *TCPServer) untrackConnection(conn net.Conn) {
	if conn == nil {
		return
	}
	s.activeMu.Lock()
	defer s.activeMu.Unlock()
	delete(s.activeConns, conn)
}

func (s *TCPServer) closeActiveConnections() {
	s.activeMu.Lock()
	conns := make([]net.Conn, 0, len(s.activeConns))
	for conn := range s.activeConns {
		conns = append(conns, conn)
	}
	s.activeMu.Unlock()
	for _, conn := range conns {
		_ = conn.Close()
	}
}

func (s *TCPServer) handleFrame(ctx context.Context, conn net.Conn, raw []byte, source string, state *connectionState) {
	started := time.Now()
	frameStatus := envelope.ParseBadFrame
	defer func() {
		s.recordFrameDuration(frameStatus, time.Since(started))
	}()

	frameCtx, cancelFrame := context.WithTimeout(context.WithoutCancel(ctx), frameOperationTimeout)
	defer cancelFrame()

	receivedAtMS := time.Now().UnixMilli()
	env, err := s.protocol.Parse(raw, receivedAtMS, source)
	if err != nil {
		env = envelope.FrameEnvelope{
			Protocol:       s.protocol.Protocol,
			SourceEndpoint: source,
			ReceivedAtMS:   receivedAtMS,
			EventTimeMS:    receivedAtMS,
			RawHex:         strings.ToUpper(hex.EncodeToString(raw)),
			ParseStatus:    envelope.ParseBadFrame,
			ParseError:     err.Error(),
		}
		env.EventID = env.StableEventID()
		s.recordParseErrorMetric(err)
	} else {
		if s.protocol.Authenticate != nil {
			result := s.protocol.Authenticate.Authenticate(env)
			authentication.Apply(&env, result)
			if result.Applicable {
				s.recordAuthenticationMetric(result)
			}
		}
		authentication.RedactParsedCredentials(&env)
		if envelope.RequiresVehicleIdentity(env) {
			resolveStarted := time.Now()
			resolved, resolveErr := s.resolver.Resolve(frameCtx, env)
			if resolveErr != nil {
				if resolved.Protocol != "" || strings.TrimSpace(resolved.VIN) != "" || strings.TrimSpace(resolved.Phone) != "" {
					env = resolved
				}
				identityStatus := identityErrorStatus(resolveErr)
				s.recordIdentityDuration(identityStatus, time.Since(resolveStarted))
				s.logger.Warn("identity resolve failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", resolveErr)
				annotateIdentityError(&env, resolveErr)
				env.ParseStatus = envelope.ParsePartial
				s.recordIdentityMetric(identityStatus)
				s.recordIdentityIssueMetric(identityStatus, env, identityIssueReason(identityStatus, resolveErr))
			} else {
				env = resolved
				annotateIdentityUnresolved(&env)
				identityStatus := identityStatus(env)
				s.recordIdentityDuration(identityStatus, time.Since(resolveStarted))
				s.recordIdentityMetric(identityStatus)
				if identityStatus != "resolved" {
					s.recordIdentityIssueMetric(identityStatus, env, "no_binding")
				}
				recordGatewayIdentityCacheStatus(s.metrics, s.protocol.Protocol, env)
			}
		} else {
			s.recordIdentitySkipMetric("non_vehicle_frame")
		}
	}
	enrichConnectionPlatform(&env, state)
	frameStatus = env.ParseStatus
	env.EventKind = envelope.EventKindRaw
	s.recordFrameMetric(env.ParseStatus)
	if env.ParseStatus != envelope.ParseBadFrame {
		realtime.EnsureParsedFields(&env)
	}

	canonicalRaw := canonicalRawEnvelope(env)
	if err := s.sink.PublishRaw(frameCtx, canonicalRaw); err != nil {
		s.recordPublishMetric("raw", "error")
		s.logger.Error("publish raw failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", err)
		return
	}
	s.recordPublishMetric("raw", "ok")
	if env.ParseStatus == envelope.ParseBadFrame {
		return
	}
	s.writeProtocolResponse(conn, raw, env, started)
	if shouldCloseAfterAuthentication(env) && conn != nil {
		_ = conn.Close()
		return
	}
	if s.delegateFields {
		fieldCount, fieldsStatus, ok := gatewayDelegatedFields(env)
		s.recordFieldsMetric(fieldsStatus)
		if ok {
			s.recordFieldsCount(fieldsStatus, fieldCount)
			s.recordPublishMetric("fields", "delegated")
		}
	} else {
		fieldsEnv, fieldsStatus, ok := gatewayFieldsEnvelope(env)
		if !ok {
			s.recordFieldsMetric(fieldsStatus)
		} else {
			if err := s.sink.PublishFields(frameCtx, fieldsEnv); err != nil {
				s.recordFieldsMetric(gatewayFieldsPublishError)
				s.recordFieldsCount(gatewayFieldsPublishError, len(fieldsEnv.Fields))
				s.recordPublishMetric("fields", "error")
				s.logger.Error("publish fields failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", err)
			} else {
				s.recordFieldsMetric(fieldsStatus)
				s.recordFieldsCount(fieldsStatus, len(fieldsEnv.Fields))
				s.recordPublishMetric("fields", "ok")
			}
		}
	}
	if s.publishUnified {
		if err := s.sink.PublishUnified(frameCtx, canonicalRaw); err != nil {
			s.recordPublishMetric("unified", "error")
			s.logger.Error("publish unified failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", err)
		} else {
			s.recordPublishMetric("unified", "ok")
		}
	}
}

func (s *TCPServer) writeProtocolResponse(conn net.Conn, raw []byte, env envelope.FrameEnvelope, frameStarted time.Time) {
	if s.protocol.Respond == nil {
		return
	}
	status := "skipped"
	defer func() {
		s.recordResponseDuration(env.MessageID, status, time.Since(frameStarted))
	}()
	response, ok, err := s.protocol.Respond(raw, env)
	if err != nil {
		status = "build_error"
		s.recordResponseMetric(env.MessageID, "build_error")
		s.logger.Warn("build protocol response failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", err)
		return
	}
	if !ok || len(response) == 0 {
		s.recordResponseMetric(env.MessageID, "skipped")
		return
	}
	if conn == nil {
		status = "write_error"
		s.recordResponseMetric(env.MessageID, "write_error")
		s.logger.Warn("write protocol response failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", "nil connection")
		return
	}
	_ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
	if _, err := conn.Write(response); err != nil {
		status = "write_error"
		s.recordResponseMetric(env.MessageID, "write_error")
		s.logger.Warn("write protocol response failed", "protocol", s.protocol.Protocol, "event_id", env.StableEventID(), "error", err)
		return
	}
	status = "ok"
	s.recordResponseMetric(env.MessageID, "ok")
}

func enrichConnectionPlatform(env *envelope.FrameEnvelope, state *connectionState) {
	if env == nil || state == nil || env.ParseStatus == envelope.ParseBadFrame {
		return
	}
	if env.Fields == nil {
		env.Fields = map[string]any{}
	}
	if shouldCloseAfterAuthentication(*env) {
		return
	}
	if current := strings.TrimSpace(fmt.Sprint(env.Fields["platform_account"])); current != "" && current != "<nil>" {
		state.platformName = current
	}
	if state.platformName == "" {
		return
	}
	if strings.TrimSpace(fmt.Sprint(env.Fields["platform_account"])) == "" || fmt.Sprint(env.Fields["platform_account"]) == "<nil>" {
		env.Fields["platform_account"] = state.platformName
	}
	if strings.TrimSpace(env.PlatformName) == "" {
		env.PlatformName = state.platformName
	}
	if strings.TrimSpace(env.SourceCode) == "" {
		env.SourceCode = sourceCodeFromPlatformName(state.platformName)
	}
	if strings.TrimSpace(env.SourceKind) == "" || strings.EqualFold(strings.TrimSpace(env.SourceKind), "UNKNOWN") {
		env.SourceKind = "PLATFORM"
	}
	if env.Parsed == nil {
		env.Parsed = map[string]any{}
	}
	if _, ok := env.Parsed["platform_name"]; !ok {
		env.Parsed["platform_name"] = state.platformName
	}
}

func sourceCodeFromPlatformName(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	var builder strings.Builder
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z':
			builder.WriteRune(r)
		case r >= 'A' && r <= 'Z':
			builder.WriteRune(r)
		case r >= '0' && r <= '9':
			builder.WriteRune(r)
		case r == '_' || r == '-' || r == '.':
			builder.WriteRune(r)
		}
	}
	return builder.String()
}

func (s *TCPServer) recordFrameMetric(status envelope.ParseStatus) {
	if s.metrics == nil {
		return
	}
	labels := metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"status":   string(status),
	}
	s.metrics.IncCounter("vehicle_gateway_frames_total", labels)
	metrics.RecordLastActivity(s.metrics, "vehicle_gateway_last_frame_unix_seconds", labels)
}

func (s *TCPServer) recordFrameDuration(status envelope.ParseStatus, elapsed time.Duration) {
	if s.metrics == nil {
		return
	}
	elapsedMS := float64(elapsed.Milliseconds())
	labels := metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"status":   string(status),
	}
	s.metrics.SetGauge("vehicle_gateway_frame_duration_ms", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"status":   string(status),
	}, elapsedMS)
	s.metrics.ObserveHistogram("vehicle_gateway_frame_duration_ms_histogram", labels, gatewayFrameDurationBucketsMS, elapsedMS)
}

func (s *TCPServer) recordPublishMetric(kind string, status string) {
	if s.metrics == nil {
		return
	}
	labels := metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"kind":     kind,
		"status":   status,
	}
	s.metrics.IncCounter("vehicle_gateway_publish_total", labels)
	metrics.RecordLastActivity(s.metrics, "vehicle_gateway_last_publish_unix_seconds", labels)
}

func (s *TCPServer) recordFieldsMetric(status string) {
	recordGatewayFieldsMetric(s.metrics, s.protocol.Protocol, status)
}

func (s *TCPServer) recordFieldsCount(status string, fieldCount int) {
	recordGatewayFieldsCount(s.metrics, s.protocol.Protocol, status, fieldCount)
}

func (s *TCPServer) recordResponseMetric(messageID string, status string) {
	if s.metrics == nil {
		return
	}
	messageID = strings.TrimSpace(messageID)
	if messageID == "" {
		messageID = "unknown"
	}
	labels := metrics.Labels{
		"protocol":   string(s.protocol.Protocol),
		"message_id": messageID,
		"status":     status,
	}
	s.metrics.IncCounter("vehicle_gateway_response_total", labels)
	metrics.RecordLastActivity(s.metrics, "vehicle_gateway_last_response_unix_seconds", labels)
}

func (s *TCPServer) recordResponseDuration(messageID string, status string, elapsed time.Duration) {
	if s.metrics == nil {
		return
	}
	messageID = strings.TrimSpace(messageID)
	if messageID == "" {
		messageID = "unknown"
	}
	elapsedMS := float64(elapsed.Microseconds()) / 1000
	labels := metrics.Labels{
		"protocol":   string(s.protocol.Protocol),
		"message_id": messageID,
		"status":     status,
	}
	s.metrics.ObserveHistogram("vehicle_gateway_response_duration_ms_histogram", labels, gatewayResponseDurationBucketsMS, elapsedMS)
	if status != "ok" {
		return
	}
	protocol := string(s.protocol.Protocol)
	p99, samples := gatewayResponseE2ERecent.Observe(protocol, elapsedMS)
	protocolLabels := metrics.Labels{"protocol": protocol}
	s.metrics.SetGauge("vehicle_gateway_response_e2e_recent_p99_ms", protocolLabels, p99)
	s.metrics.SetGauge("vehicle_gateway_response_e2e_recent_samples", protocolLabels, float64(samples))
}

func (s *TCPServer) recordAuthenticationMetric(result authentication.Result) {
	if s.metrics == nil || !result.Applicable {
		return
	}
	s.metrics.IncCounter("vehicle_gateway_authentication_total", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"mode":     string(result.Mode),
		"source":   result.Source,
		"status":   result.Status,
	})
}

func (s *TCPServer) recordParseErrorMetric(err error) {
	if s.metrics == nil {
		return
	}
	s.metrics.IncCounter("vehicle_gateway_parse_errors_total", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"reason":   classifyError(err),
	})
}

func (s *TCPServer) recordIdentityMetric(status string) {
	if s.metrics == nil {
		return
	}
	s.metrics.IncCounter("vehicle_gateway_identity_total", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"status":   status,
	})
}

func (s *TCPServer) recordIdentitySkipMetric(reason string) {
	if s.metrics == nil {
		return
	}
	s.metrics.IncCounter("vehicle_gateway_identity_skips_total", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"reason":   reason,
	})
}

func (s *TCPServer) recordIdentityIssueMetric(status string, env envelope.FrameEnvelope, reason string) {
	if s.metrics == nil {
		return
	}
	messageID := strings.TrimSpace(env.MessageID)
	if messageID == "" {
		messageID = "unknown"
	}
	reason = strings.TrimSpace(reason)
	if reason == "" {
		reason = "unknown"
	}
	s.metrics.IncCounter("vehicle_gateway_identity_issues_total", metrics.Labels{
		"protocol":   string(s.protocol.Protocol),
		"status":     status,
		"message_id": messageID,
		"reason":     reason,
	})
}

func (s *TCPServer) recordIdentityDuration(status string, elapsed time.Duration) {
	recordGatewayIdentityDuration(s.metrics, s.protocol.Protocol, status, elapsed)
}

func (s *TCPServer) recordConnectionMetric(delta float64) {
	if s.metrics == nil {
		return
	}
	s.metrics.AddGauge("vehicle_gateway_active_connections", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
	}, delta)
}

func (s *TCPServer) recordConnectionRejection(reason string) {
	if s.metrics == nil {
		return
	}
	s.metrics.IncCounter("vehicle_gateway_connection_rejections_total", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"reason":   reason,
	})
}

func (s *TCPServer) recordConnectionClose(reason string) {
	if s.metrics == nil {
		return
	}
	s.metrics.IncCounter("vehicle_gateway_connection_closes_total", metrics.Labels{
		"protocol": string(s.protocol.Protocol),
		"reason":   reason,
	})
}

func shouldCloseAfterAuthentication(env envelope.FrameEnvelope) bool {
	return env.AuthenticationEnforced &&
		strings.TrimSpace(env.AuthenticationStatus) != "" &&
		env.AuthenticationStatus != authentication.StatusAccepted
}

func identityStatus(env envelope.FrameEnvelope) string {
	if strings.TrimSpace(env.VIN) != "" {
		return "resolved"
	}
	if env.ParseStatus == envelope.ParsePartial {
		return "error"
	}
	return "unresolved"
}

func identityIssueReason(status string, err error) string {
	switch status {
	case "timeout":
		return "timeout"
	case "error":
		if err == nil {
			return "resolver_error"
		}
		text := strings.ToLower(strings.TrimSpace(err.Error()))
		switch {
		case strings.Contains(text, "connection"), strings.Contains(text, "tcp"), strings.Contains(text, "network"):
			return "identity_store_connection"
		case strings.Contains(text, "timeout"), strings.Contains(text, "deadline"):
			return "timeout"
		default:
			return "resolver_error"
		}
	default:
		return "no_binding"
	}
}

func annotateIdentityUnresolved(env *envelope.FrameEnvelope) {
	if env == nil || env.ParseStatus == envelope.ParseBadFrame || strings.TrimSpace(env.VIN) != "" {
		return
	}
	if env.Parsed == nil {
		env.Parsed = map[string]any{}
	}
	if _, exists := env.Parsed["identity"]; exists {
		return
	}
	env.Parsed["identity"] = map[string]any{
		"resolved": false,
		"reason":   "no_binding",
	}
}

func classifyError(err error) string {
	text := strings.ToLower(strings.TrimSpace(fmt.Sprint(err)))
	switch {
	case text == "":
		return "unknown"
	case strings.Contains(text, "bcc") || strings.Contains(text, "checksum"):
		return "checksum"
	case strings.Contains(text, "short") || strings.Contains(text, "truncated") || strings.Contains(text, "length"):
		return "length"
	case strings.Contains(text, "json"):
		return "json"
	case strings.Contains(text, "start"):
		return "start_symbol"
	default:
		return "parse"
	}
}

func (p TCPProtocol) String() string {
	return fmt.Sprintf("%s@%s", p.Protocol, p.Addr)
}
