package gateway

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"strings"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"

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

type MQTTClientConfig struct {
	EndpointName            string
	Broker                  string
	ClientID                string
	Username                string
	Password                string
	Topics                  []string
	QoS                     byte
	CleanSession            bool
	KeepAlive               time.Duration
	ConnectTimeout          time.Duration
	TLSCACertPath           string
	TLSClientCertPath       string
	TLSClientKeyPath        string
	TLSHostnameVerification bool
	Sink                    eventbus.Sink
	Resolver                identity.Resolver
	Logger                  *slog.Logger
	Metrics                 *metrics.Registry
	PublishUnified          bool
	DelegateFields          bool
}

type MQTTClient struct {
	cfg    MQTTClientConfig
	client mqtt.Client
}

func NewMQTTClient(cfg MQTTClientConfig) (*MQTTClient, error) {
	if strings.TrimSpace(cfg.Broker) == "" {
		return nil, errors.New("mqtt broker is required")
	}
	if strings.TrimSpace(cfg.ClientID) == "" {
		return nil, errors.New("mqtt client id is required")
	}
	if len(cfg.Topics) == 0 {
		return nil, errors.New("mqtt topics are 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.EndpointName == "" {
		cfg.EndpointName = "yutong"
	}
	return &MQTTClient{cfg: cfg}, nil
}

func (c *MQTTClient) Start(ctx context.Context) error {
	opts, err := c.buildOptions(ctx)
	if err != nil {
		return err
	}

	c.client = mqtt.NewClient(opts)
	token := c.client.Connect()
	if token.Wait() && token.Error() != nil {
		return token.Error()
	}
	go func() {
		<-ctx.Done()
		if c.client != nil && c.client.IsConnected() {
			c.client.Disconnect(250)
		}
	}()
	return nil
}

func (c *MQTTClient) buildOptions(ctx context.Context) (*mqtt.ClientOptions, error) {
	keepAlive := c.cfg.KeepAlive
	if keepAlive <= 0 {
		keepAlive = 20 * time.Second
	}
	connectTimeout := c.cfg.ConnectTimeout
	if connectTimeout <= 0 {
		connectTimeout = 10 * time.Second
	}
	opts := mqtt.NewClientOptions().
		AddBroker(c.cfg.Broker).
		SetClientID(c.cfg.ClientID).
		SetUsername(c.cfg.Username).
		SetPassword(c.cfg.Password).
		SetCleanSession(c.cfg.CleanSession).
		SetAutoReconnect(true).
		SetConnectRetry(true).
		SetConnectRetryInterval(5 * time.Second).
		SetResumeSubs(true).
		SetOrderMatters(false).
		SetKeepAlive(keepAlive).
		SetConnectTimeout(connectTimeout)

	opts.SetDefaultPublishHandler(func(_ mqtt.Client, message mqtt.Message) {
		c.handleMessage(ctx, message.Topic(), message.Payload())
	})
	opts.OnConnect = func(client mqtt.Client) {
		for _, topic := range c.cfg.Topics {
			token := client.Subscribe(topic, c.cfg.QoS, nil)
			if token.Wait() && token.Error() != nil {
				c.cfg.Logger.Error("mqtt subscribe failed", "broker", c.cfg.Broker, "topic", topic, "error", token.Error())
				continue
			}
			c.cfg.Logger.Info("mqtt subscribed", "broker", c.cfg.Broker, "topic", topic, "qos", c.cfg.QoS)
		}
	}
	opts.OnConnectionLost = func(_ mqtt.Client, err error) {
		c.cfg.Logger.Warn("mqtt connection lost", "broker", c.cfg.Broker, "error", err)
	}

	tlsConfig, err := c.buildTLSConfig()
	if err != nil {
		return nil, err
	}
	if tlsConfig != nil {
		opts.SetTLSConfig(tlsConfig)
	}
	return opts, nil
}

func (c *MQTTClient) buildTLSConfig() (*tls.Config, error) {
	caPath := strings.TrimSpace(c.cfg.TLSCACertPath)
	certPath := strings.TrimSpace(c.cfg.TLSClientCertPath)
	keyPath := strings.TrimSpace(c.cfg.TLSClientKeyPath)
	if caPath == "" && certPath == "" && keyPath == "" {
		return nil, nil
	}
	config := &tls.Config{
		MinVersion:         tls.VersionTLS12,
		InsecureSkipVerify: !c.cfg.TLSHostnameVerification,
	}
	if caPath != "" {
		caPEM, err := os.ReadFile(caPath)
		if err != nil {
			return nil, fmt.Errorf("read mqtt ca certificate: %w", err)
		}
		roots := x509.NewCertPool()
		if !roots.AppendCertsFromPEM(caPEM) {
			return nil, fmt.Errorf("parse mqtt ca certificate %s", caPath)
		}
		config.RootCAs = roots
	}
	if certPath != "" || keyPath != "" {
		if certPath == "" || keyPath == "" {
			return nil, errors.New("mqtt client certificate and key must be configured together")
		}
		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
		if err != nil {
			return nil, fmt.Errorf("load mqtt client certificate: %w", err)
		}
		config.Certificates = []tls.Certificate{cert}
	}
	return config, nil
}

func (c *MQTTClient) handleMessage(ctx context.Context, topic string, payload []byte) {
	started := time.Now()
	frameStatus := envelope.ParseBadFrame
	defer func() {
		c.recordFrameDuration(frameStatus, time.Since(started))
	}()

	messageCtx, cancelMessage := context.WithTimeout(context.WithoutCancel(ctx), frameOperationTimeout)
	defer cancelMessage()

	receivedAtMS := time.Now().UnixMilli()
	env, err := yutongmqtt.ParseMessage(c.cfg.EndpointName, topic, payload, receivedAtMS)
	if err != nil {
		env = envelope.FrameEnvelope{
			Protocol:       envelope.ProtocolYutongMQTT,
			MessageID:      "MQTT",
			VehicleKeyHint: "unknown",
			SourceEndpoint: "mqtt://" + c.cfg.EndpointName + topic,
			EventTimeMS:    receivedAtMS,
			ReceivedAtMS:   receivedAtMS,
			RawText:        string(payload),
			ParseStatus:    envelope.ParseBadFrame,
			ParseError:     err.Error(),
		}
		env.EventID = env.StableEventID()
		c.recordParseErrorMetric(err)
	} else if envelope.RequiresVehicleIdentity(env) {
		resolveStarted := time.Now()
		resolved, resolveErr := c.cfg.Resolver.Resolve(messageCtx, env)
		if resolveErr != nil {
			if resolved.Protocol != "" || strings.TrimSpace(resolved.VIN) != "" || strings.TrimSpace(resolved.Phone) != "" {
				env = resolved
			}
			identityStatus := identityErrorStatus(resolveErr)
			c.recordIdentityDuration(identityStatus, time.Since(resolveStarted))
			c.cfg.Logger.Warn("mqtt identity resolve failed", "topic", topic, "event_id", env.StableEventID(), "error", resolveErr)
			annotateIdentityError(&env, resolveErr)
			env.ParseStatus = envelope.ParsePartial
			c.recordIdentityMetric(identityStatus)
			c.recordIdentityIssueMetric(identityStatus, env, identityIssueReason(identityStatus, resolveErr))
		} else {
			env = resolved
			annotateIdentityUnresolved(&env)
			identityStatus := identityStatus(env)
			c.recordIdentityDuration(identityStatus, time.Since(resolveStarted))
			c.recordIdentityMetric(identityStatus)
			if identityStatus != "resolved" {
				c.recordIdentityIssueMetric(identityStatus, env, "no_binding")
			}
			recordGatewayIdentityCacheStatus(c.cfg.Metrics, envelope.ProtocolYutongMQTT, env)
		}
	} else {
		c.recordIdentitySkipMetric("non_vehicle_frame")
	}
	frameStatus = env.ParseStatus
	env.EventKind = envelope.EventKindRaw
	c.recordFrameMetric(env.ParseStatus)
	if env.ParseStatus != envelope.ParseBadFrame {
		realtime.EnsureParsedFields(&env)
	}
	canonicalRaw := canonicalRawEnvelope(env)
	if err := c.cfg.Sink.PublishRaw(messageCtx, canonicalRaw); err != nil {
		c.recordPublishMetric("raw", "error")
		c.cfg.Logger.Error("publish mqtt raw failed", "topic", topic, "event_id", env.StableEventID(), "error", err)
		return
	}
	c.recordPublishMetric("raw", "ok")
	if env.ParseStatus == envelope.ParseBadFrame {
		return
	}
	if c.cfg.DelegateFields {
		fieldCount, fieldsStatus, ok := gatewayDelegatedFields(env)
		c.recordFieldsMetric(fieldsStatus)
		if ok {
			c.recordFieldsCount(fieldsStatus, fieldCount)
			c.recordPublishMetric("fields", "delegated")
		}
	} else {
		fieldsEnv, fieldsStatus, ok := gatewayFieldsEnvelope(env)
		if !ok {
			c.recordFieldsMetric(fieldsStatus)
		} else {
			if err := c.cfg.Sink.PublishFields(messageCtx, fieldsEnv); err != nil {
				c.recordFieldsMetric(gatewayFieldsPublishError)
				c.recordFieldsCount(gatewayFieldsPublishError, len(fieldsEnv.Fields))
				c.recordPublishMetric("fields", "error")
				c.cfg.Logger.Error("publish mqtt fields failed", "topic", topic, "event_id", env.StableEventID(), "error", err)
			} else {
				c.recordFieldsMetric(fieldsStatus)
				c.recordFieldsCount(fieldsStatus, len(fieldsEnv.Fields))
				c.recordPublishMetric("fields", "ok")
			}
		}
	}
	if c.cfg.PublishUnified {
		if err := c.cfg.Sink.PublishUnified(messageCtx, canonicalRaw); err != nil {
			c.recordPublishMetric("unified", "error")
			c.cfg.Logger.Error("publish mqtt unified failed", "topic", topic, "event_id", env.StableEventID(), "error", err)
		} else {
			c.recordPublishMetric("unified", "ok")
		}
	}
}

func (c *MQTTClient) recordFrameMetric(status envelope.ParseStatus) {
	if c.cfg.Metrics == nil {
		return
	}
	labels := metrics.Labels{
		"protocol": string(envelope.ProtocolYutongMQTT),
		"status":   string(status),
	}
	c.cfg.Metrics.IncCounter("vehicle_gateway_frames_total", labels)
	metrics.RecordLastActivity(c.cfg.Metrics, "vehicle_gateway_last_frame_unix_seconds", labels)
}

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

func (c *MQTTClient) recordPublishMetric(kind string, status string) {
	if c.cfg.Metrics == nil {
		return
	}
	labels := metrics.Labels{
		"protocol": string(envelope.ProtocolYutongMQTT),
		"kind":     kind,
		"status":   status,
	}
	c.cfg.Metrics.IncCounter("vehicle_gateway_publish_total", labels)
	metrics.RecordLastActivity(c.cfg.Metrics, "vehicle_gateway_last_publish_unix_seconds", labels)
}

func (c *MQTTClient) recordFieldsMetric(status string) {
	recordGatewayFieldsMetric(c.cfg.Metrics, envelope.ProtocolYutongMQTT, status)
}

func (c *MQTTClient) recordFieldsCount(status string, fieldCount int) {
	recordGatewayFieldsCount(c.cfg.Metrics, envelope.ProtocolYutongMQTT, status, fieldCount)
}

func (c *MQTTClient) recordParseErrorMetric(err error) {
	if c.cfg.Metrics == nil {
		return
	}
	c.cfg.Metrics.IncCounter("vehicle_gateway_parse_errors_total", metrics.Labels{
		"protocol": string(envelope.ProtocolYutongMQTT),
		"reason":   classifyError(err),
	})
}

func (c *MQTTClient) recordIdentityMetric(status string) {
	if c.cfg.Metrics == nil {
		return
	}
	c.cfg.Metrics.IncCounter("vehicle_gateway_identity_total", metrics.Labels{
		"protocol": string(envelope.ProtocolYutongMQTT),
		"status":   status,
	})
}

func (c *MQTTClient) recordIdentitySkipMetric(reason string) {
	if c.cfg.Metrics == nil {
		return
	}
	c.cfg.Metrics.IncCounter("vehicle_gateway_identity_skips_total", metrics.Labels{
		"protocol": string(envelope.ProtocolYutongMQTT),
		"reason":   reason,
	})
}

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

func (c *MQTTClient) recordIdentityDuration(status string, elapsed time.Duration) {
	recordGatewayIdentityDuration(c.cfg.Metrics, envelope.ProtocolYutongMQTT, status, elapsed)
}
