package jt808

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"
	"unicode/utf8"

	"golang.org/x/text/encoding/simplifiedchinese"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)

var (
	ErrFrameTooShort = errors.New("jt808 frame too short")
	ErrChecksum      = errors.New("jt808 checksum mismatch")
	ErrEscape        = errors.New("jt808 invalid escape sequence")
)

type Location struct {
	AlarmFlag      uint32
	StatusFlag     uint32
	Latitude       float64
	Longitude      float64
	AltitudeM      uint16
	SpeedKMH       float64
	DirectionDeg   uint16
	Time           time.Time
	TotalMileageKM *float64
	Additional     map[string]any
}

// ExtractFrames splits JT/T 808 TCP streams by 0x7e delimiters and unescapes
// complete frame payloads. Returned frames exclude the leading and trailing 0x7e.
func ExtractFrames(stream []byte) (frames [][]byte, remainder []byte, err error) {
	start := -1
	for i, value := range stream {
		if value != 0x7e {
			continue
		}
		if start < 0 {
			start = i
			continue
		}
		if i == start+1 {
			start = i
			continue
		}
		frame, err := unescape(stream[start+1 : i])
		if err != nil {
			return nil, nil, err
		}
		frames = append(frames, frame)
		start = i
	}
	if start >= 0 && start < len(stream)-1 {
		return frames, append([]byte(nil), stream[start:]...), nil
	}
	return frames, nil, nil
}

func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) {
	if len(raw) < 13 {
		return envelope.FrameEnvelope{}, fmt.Errorf("%w: len=%d", ErrFrameTooShort, len(raw))
	}
	if got, want := checksum(raw[:len(raw)-1]), raw[len(raw)-1]; got != want {
		return envelope.FrameEnvelope{}, fmt.Errorf("%w: got=0x%02x want=0x%02x", ErrChecksum, got, want)
	}

	messageID := binary.BigEndian.Uint16(raw[0:2])
	props := binary.BigEndian.Uint16(raw[2:4])
	bodySize := props & 0x03ff
	versioned := props&0x4000 != 0
	headerLen := 12
	phoneStart := 4
	phoneEnd := 10
	serialStart := 10
	packageInfo := map[string]any(nil)
	protocolVersion := byte(0)
	if versioned {
		headerLen = 17
		if len(raw) < headerLen {
			return envelope.FrameEnvelope{}, fmt.Errorf("%w: versioned header len=%d", ErrFrameTooShort, len(raw))
		}
		protocolVersion = raw[4]
		phoneStart = 5
		phoneEnd = 15
		serialStart = 15
	}
	subpackage := props&0x2000 != 0
	if subpackage {
		headerLen += 4
	}
	if len(raw) < headerLen+int(bodySize)+1 {
		return envelope.FrameEnvelope{}, fmt.Errorf("%w: bodySize=%d len=%d", ErrFrameTooShort, bodySize, len(raw))
	}
	if subpackage {
		packageInfo = map[string]any{
			"total": binary.BigEndian.Uint16(raw[serialStart+2 : serialStart+4]),
			"index": binary.BigEndian.Uint16(raw[serialStart+4 : serialStart+6]),
		}
	}

	phoneBCD := raw[phoneStart:phoneEnd]
	phoneRaw := bcdString(phoneBCD)
	phone := normalizePhone(phoneRaw)
	body := raw[headerLen : headerLen+int(bodySize)]
	parsed := map[string]any{
		"header": map[string]any{
			"message_id":       fmt.Sprintf("0x%04X", messageID),
			"protocol_version": protocolVersion,
			"versioned":        versioned,
			"properties":       props,
			"body_size":        bodySize,
			"phone":            phone,
			"sequence":         binary.BigEndian.Uint16(raw[serialStart : serialStart+2]),
			"subpackage":       subpackage,
			"package_info":     packageInfo,
			"encryption_flags": (props >> 10) & 0x07,
		},
	}
	fields := map[string]any{}
	eventTimeMS := receivedAtMS

	if messageID == 0x0200 {
		location, err := parseLocation(body)
		if err != nil {
			return envelope.FrameEnvelope{}, err
		}
		parsed["location"] = locationToMap(location)
		fields[envelope.FieldLatitude] = location.Latitude
		fields[envelope.FieldLongitude] = location.Longitude
		fields[envelope.FieldSpeedKMH] = location.SpeedKMH
		fields["altitude_m"] = location.AltitudeM
		fields["direction_deg"] = location.DirectionDeg
		fields["alarm_flag"] = location.AlarmFlag
		fields["status_flag"] = location.StatusFlag
		if !location.Time.IsZero() {
			eventTimeMS = location.Time.UnixMilli()
			fields["device_time"] = location.Time.Format(time.RFC3339)
		}
		if location.TotalMileageKM != nil {
			fields[envelope.FieldTotalMileageKM] = *location.TotalMileageKM
		}
	} else if messageID == 0x0100 {
		registration, err := parseRegistration(body, versioned)
		if err != nil {
			return envelope.FrameEnvelope{}, err
		}
		parsed["registration"] = registration
		if deviceID, ok := registration["device_id"].(string); ok {
			fields["device_id"] = deviceID
		}
		if plate, ok := registration["plate"].(string); ok {
			fields["plate"] = plate
		}
	} else if messageID == 0x0102 {
		authentication := parseAuthentication(body, versioned)
		parsed["authentication"] = authentication
		if token, ok := authentication["token"].(string); ok {
			fields["auth_token"] = token
		}
	}

	env := envelope.FrameEnvelope{
		Protocol:       envelope.ProtocolJT808,
		MessageID:      fmt.Sprintf("0x%04X", messageID),
		Sequence:       binary.BigEndian.Uint16(raw[serialStart : serialStart+2]),
		Phone:          phone,
		SourceEndpoint: sourceEndpoint,
		EventTimeMS:    eventTimeMS,
		ReceivedAtMS:   receivedAtMS,
		RawHex:         strings.ToUpper(hex.EncodeToString(raw)),
		Parsed:         parsed,
		Fields:         fields,
		ParseStatus:    envelope.ParseOK,
	}
	if registration, ok := parsed["registration"].(map[string]any); ok {
		env.DeviceID, _ = registration["device_id"].(string)
		env.Plate, _ = registration["plate"].(string)
	}
	env.EventID = env.StableEventID()
	return env, nil
}

func parseRegistration(body []byte, versioned bool) (map[string]any, error) {
	schemaVersion := "2011_2013"
	manufacturerSize := 5
	deviceTypeSize := 20
	deviceIDSize := 7
	if versioned {
		schemaVersion = "2019"
		manufacturerSize = 11
		deviceTypeSize = 30
		deviceIDSize = 30
	}
	minLen := 2 + 2 + manufacturerSize + deviceTypeSize + deviceIDSize + 1
	if len(body) < minLen {
		return nil, fmt.Errorf("%w: registration body len=%d", ErrFrameTooShort, len(body))
	}
	manufacturerStart := 4
	deviceTypeStart := manufacturerStart + manufacturerSize
	deviceIDStart := deviceTypeStart + deviceTypeSize
	plateColorStart := deviceIDStart + deviceIDSize
	registration := map[string]any{
		"schema_version": schemaVersion,
		"province":       binary.BigEndian.Uint16(body[0:2]),
		"city":           binary.BigEndian.Uint16(body[2:4]),
		"manufacturer":   fixedText(body[manufacturerStart:deviceTypeStart]),
		"device_type":    fixedText(body[deviceTypeStart:deviceIDStart]),
		"device_id":      fixedText(body[deviceIDStart:plateColorStart]),
		"plate_color":    body[plateColorStart],
		"plate":          freeText(body[plateColorStart+1:]),
	}
	return registration, nil
}

func normalizePhone(phone string) string {
	trimmed := strings.TrimLeft(strings.TrimSpace(phone), "0")
	if trimmed == "" {
		return strings.TrimSpace(phone)
	}
	return trimmed
}

func parseAuthentication(body []byte, versioned bool) map[string]any {
	if versioned {
		if out, ok := parseVersionedAuthentication(body); ok {
			return out
		}
		if out, ok := parseDelimitedVersionedAuthentication(body); ok {
			return out
		}
	}
	return map[string]any{"token": freeText(body)}
}

func parseVersionedAuthentication(body []byte) (map[string]any, bool) {
	if len(body) < 17 {
		return nil, false
	}
	tokenLen := int(body[0])
	imeiStart := 1 + tokenLen
	softwareLenStart := imeiStart + 15
	if tokenLen <= 0 || len(body) < softwareLenStart+1 {
		return nil, false
	}
	softwareLen := int(body[softwareLenStart])
	softwareStart := softwareLenStart + 1
	if len(body) < softwareStart+softwareLen {
		return nil, false
	}
	return map[string]any{
		"schema_version":   "2019",
		"token":            freeText(body[1:imeiStart]),
		"imei":             fixedText(body[imeiStart:softwareLenStart]),
		"software_version": freeText(body[softwareStart : softwareStart+softwareLen]),
	}, true
}

func parseDelimitedVersionedAuthentication(body []byte) (map[string]any, bool) {
	parts := bytes.Split(body, []byte{0xff, 0xff})
	if len(parts) < 2 {
		return nil, false
	}
	out := map[string]any{
		"schema_version": "2019_delimited",
		"token":          freeText(parts[0]),
		"imei":           freeText(parts[1]),
	}
	if len(parts) > 2 {
		out["software_version"] = freeText(parts[2])
	}
	return out, true
}

func parseLocation(body []byte) (Location, error) {
	if len(body) < 28 {
		return Location{}, fmt.Errorf("%w: location body len=%d", ErrFrameTooShort, len(body))
	}
	status := binary.BigEndian.Uint32(body[4:8])
	latitude := float64(binary.BigEndian.Uint32(body[8:12])) / 1_000_000
	longitude := float64(binary.BigEndian.Uint32(body[12:16])) / 1_000_000
	if status&(1<<2) != 0 {
		latitude = -latitude
	}
	if status&(1<<3) != 0 {
		longitude = -longitude
	}
	location := Location{
		AlarmFlag:    binary.BigEndian.Uint32(body[0:4]),
		StatusFlag:   status,
		Latitude:     latitude,
		Longitude:    longitude,
		AltitudeM:    binary.BigEndian.Uint16(body[16:18]),
		SpeedKMH:     float64(binary.BigEndian.Uint16(body[18:20])) / 10,
		DirectionDeg: binary.BigEndian.Uint16(body[20:22]),
		Time:         parseBCDTime(body[22:28]),
	}
	location.Additional = parseAdditional(body[28:], &location)
	return location, nil
}

func parseAdditional(data []byte, location *Location) map[string]any {
	out := map[string]any{}
	for len(data) >= 2 {
		id := data[0]
		size := int(data[1])
		data = data[2:]
		if len(data) < size {
			return out
		}
		value := data[:size]
		var parsed map[string]any
		switch {
		case id == 0x01 && size == 4:
			rawMileage := binary.BigEndian.Uint32(value)
			mileage := float64(rawMileage) / 10
			location.TotalMileageKM = &mileage
			parsed = map[string]any{
				"name":  envelope.FieldTotalMileageKM,
				"value": mileage,
			}
		case id == 0x02 && size == 2:
			rawFuel := binary.BigEndian.Uint16(value)
			parsed = map[string]any{
				"name":  "fuel_l",
				"value": float64(rawFuel) / 10,
			}
		case id == 0x03 && size == 2:
			rawSpeed := binary.BigEndian.Uint16(value)
			parsed = map[string]any{
				"name":  "recorder_speed_kmh",
				"value": float64(rawSpeed) / 10,
			}
		case id == 0x04 && size == 2:
			parsed = map[string]any{
				"name":  "manual_alarm_event_id",
				"value": binary.BigEndian.Uint16(value),
			}
		case id == 0x05 && size == 30:
			parsed = map[string]any{
				"name":   "tire_pressure",
				"values": parseTirePressure(value),
			}
		case id == 0x06 && size == 2:
			parsed = map[string]any{
				"name":  "carriage_temperature_c",
				"value": int16(binary.BigEndian.Uint16(value)),
			}
		case id == 0x11 && (size == 1 || size == 5):
			parsed = map[string]any{
				"name":          "overspeed_alarm",
				"location_type": value[0],
			}
			if size == 5 {
				parsed["area_id"] = binary.BigEndian.Uint32(value[1:5])
			}
		case id == 0x12 && size == 6:
			parsed = map[string]any{
				"name":          "area_route_alarm",
				"location_type": value[0],
				"area_id":       binary.BigEndian.Uint32(value[1:5]),
				"direction":     value[5],
			}
		case id == 0x13 && size == 7:
			parsed = map[string]any{
				"name":                       "road_section_driving_time_alarm",
				"road_section_id":            binary.BigEndian.Uint32(value[0:4]),
				"driving_time_seconds":       binary.BigEndian.Uint16(value[4:6]),
				"result":                     value[6],
				"result_description":         roadSectionDrivingTimeResult(value[6]),
				"driving_time_result_source": "jt808_0x0200_additional_0x13",
			}
		case id == 0x25 && size == 4:
			rawStatus := binary.BigEndian.Uint32(value)
			parsed = map[string]any{
				"name":  "extended_vehicle_signal_status",
				"value": rawStatus,
				"bits":  parseExtendedVehicleSignal(rawStatus),
			}
		case id == 0x2a && size == 2:
			rawStatus := binary.BigEndian.Uint16(value)
			parsed = map[string]any{
				"name":  "io_status",
				"value": rawStatus,
				"bits":  parseIOStatus(rawStatus),
			}
		case id == 0x2b && size == 4:
			analog := binary.BigEndian.Uint32(value)
			parsed = map[string]any{
				"name":  "analog",
				"ad0":   analog & 0xffff,
				"ad1":   analog >> 16,
				"value": analog,
			}
		case id == 0x30 && size == 1:
			parsed = map[string]any{
				"name":  "network_signal_strength",
				"value": value[0],
			}
		case id == 0x31 && size == 1:
			parsed = map[string]any{
				"name":  "gnss_satellite_count",
				"value": value[0],
			}
		}
		addParsedAdditional(out, parsed)
		data = data[size:]
	}
	return out
}

func locationToMap(location Location) map[string]any {
	out := map[string]any{
		"alarm_flag":    location.AlarmFlag,
		"status_flag":   location.StatusFlag,
		"latitude":      location.Latitude,
		"longitude":     location.Longitude,
		"altitude_m":    location.AltitudeM,
		"speed_kmh":     location.SpeedKMH,
		"direction_deg": location.DirectionDeg,
	}
	if !location.Time.IsZero() {
		out["device_time"] = location.Time.Format(time.RFC3339)
	}
	if location.TotalMileageKM != nil {
		out[envelope.FieldTotalMileageKM] = *location.TotalMileageKM
	}
	for key, value := range location.Additional {
		out[key] = value
	}
	return out
}

func addParsedAdditional(out map[string]any, parsed map[string]any) {
	if parsed == nil {
		return
	}
	name, ok := parsed["name"].(string)
	if !ok || name == "" {
		return
	}
	if value, ok := parsed["value"]; ok && len(parsed) <= 2 {
		out[name] = value
		return
	}
	value := make(map[string]any, len(parsed)-1)
	for key, item := range parsed {
		if key == "name" {
			continue
		}
		value[key] = item
	}
	if len(value) == 1 {
		if singleValue, ok := value["value"]; ok {
			out[name] = singleValue
			return
		}
	}
	out[name] = value
}

func parseTirePressure(data []byte) []map[string]any {
	out := make([]map[string]any, 0, len(data))
	for index, value := range data {
		out = append(out, map[string]any{
			"position": index,
			"value":    value,
			"valid":    value != 0xff,
		})
	}
	return out
}

func parseExtendedVehicleSignal(value uint32) map[string]bool {
	return map[string]bool{
		"low_beam":        value&0x0001 != 0,
		"high_beam":       value&0x0002 != 0,
		"right_turn":      value&0x0004 != 0,
		"left_turn":       value&0x0008 != 0,
		"brake":           value&0x0010 != 0,
		"reverse_gear":    value&0x0020 != 0,
		"fog_light":       value&0x0040 != 0,
		"clearance_light": value&0x0080 != 0,
		"horn":            value&0x0100 != 0,
		"air_conditioner": value&0x0200 != 0,
		"neutral":         value&0x0400 != 0,
		"retarder":        value&0x0800 != 0,
		"abs":             value&0x1000 != 0,
		"heater":          value&0x2000 != 0,
		"clutch":          value&0x4000 != 0,
	}
}

func parseIOStatus(value uint16) map[string]bool {
	return map[string]bool{
		"deep_sleep": value&0x0001 != 0,
		"sleep":      value&0x0002 != 0,
	}
}

func roadSectionDrivingTimeResult(value byte) string {
	if value == 0 {
		return "insufficient"
	}
	if value == 1 {
		return "too_long"
	}
	return "unknown"
}

func bcdString(data []byte) string {
	out := make([]byte, 0, len(data)*2)
	for _, value := range data {
		out = append(out, '0'+((value>>4)&0x0f), '0'+(value&0x0f))
	}
	return string(out)
}

func fixedText(data []byte) string {
	return strings.Trim(freeText(data), "\x00 ")
}

func freeText(data []byte) string {
	data = bytes.Trim(data, "\x00 ")
	if len(data) == 0 {
		return ""
	}
	if utf8.Valid(data) {
		return string(data)
	}
	decoded, err := simplifiedchinese.GBK.NewDecoder().Bytes(data)
	if err == nil && utf8.Valid(decoded) {
		return string(decoded)
	}
	return strings.ToValidUTF8(string(data), "")
}

func parseBCDTime(data []byte) time.Time {
	if len(data) != 6 {
		return time.Time{}
	}
	year := 2000 + bcdByte(data[0])
	month := time.Month(bcdByte(data[1]))
	day := bcdByte(data[2])
	hour := bcdByte(data[3])
	minute := bcdByte(data[4])
	second := bcdByte(data[5])
	if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 {
		return time.Time{}
	}
	return time.Date(year, month, day, hour, minute, second, 0, time.FixedZone("Asia/Shanghai", 8*3600))
}

func bcdByte(value byte) int {
	return int(value>>4)*10 + int(value&0x0f)
}

func unescape(data []byte) ([]byte, error) {
	out := make([]byte, 0, len(data))
	for i := 0; i < len(data); i++ {
		if data[i] != 0x7d {
			out = append(out, data[i])
			continue
		}
		if i+1 >= len(data) {
			return nil, ErrEscape
		}
		i++
		switch data[i] {
		case 0x01:
			out = append(out, 0x7d)
		case 0x02:
			out = append(out, 0x7e)
		default:
			return nil, ErrEscape
		}
	}
	return out, nil
}

func checksum(data []byte) byte {
	var out byte
	for _, value := range data {
		out ^= value
	}
	return out
}
