package feichibridge

import (
	"encoding/binary"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

const (
	CommandRealtime = byte(0x02)
	CommandReissue  = byte(0x03)
	CommandLogin    = byte(0x05)
)

type Encoder struct{}

func (Encoder) DataFrame(command byte, vin string, at time.Time, record Record) ([]byte, error) {
	if command != CommandRealtime && command != CommandReissue {
		return nil, fmt.Errorf("unsupported data command 0x%02X", command)
	}
	body := encodeTime(at)
	units := 0
	if unit, ok := wholeVehicleUnit(record); ok {
		body = append(body, 0x01)
		body = append(body, unit...)
		units++
	}
	if unit, ok := motorUnit(record); ok {
		body = append(body, 0x02)
		body = append(body, unit...)
		units++
	}
	if unit, ok := fuelCellUnit(record); ok {
		body = append(body, 0x03)
		body = append(body, unit...)
		units++
	}
	if unit, ok := positionUnit(record); ok {
		body = append(body, 0x05)
		body = append(body, unit...)
		units++
	}
	if unit, ok := extremeUnit(record); ok {
		body = append(body, 0x06)
		body = append(body, unit...)
		units++
	}
	if unit, ok := alarmUnit(record); ok {
		body = append(body, 0x07)
		body = append(body, unit...)
		units++
	}
	for _, unit := range voltageUnits(record) {
		body = append(body, 0x08)
		body = append(body, unit...)
		units++
	}
	for _, unit := range temperatureUnits(record) {
		body = append(body, 0x09)
		body = append(body, unit...)
		units++
	}
	if units == 0 {
		return nil, errors.New("source record has no mappable GB/T 32960 units")
	}
	return buildFrame('#', command, 0xFE, vin, body)
}

func LoginFrame(platformID, username, password string, serial uint16, now time.Time) ([]byte, error) {
	if len(username) > 12 {
		return nil, errors.New("GB/T 32960 platform username exceeds 12 bytes")
	}
	if len(password) > 20 {
		return nil, errors.New("GB/T 32960 platform password exceeds 20 bytes")
	}
	body := encodeTime(now)
	body = binary.BigEndian.AppendUint16(body, serial)
	body = appendPaddedASCII(body, username, 12)
	body = appendPaddedASCII(body, password, 20)
	body = append(body, 0x01)
	return buildFrame('#', CommandLogin, 0xFE, platformID, body)
}

func buildFrame(start, command, response byte, vin string, body []byte) ([]byte, error) {
	if len(vin) != 17 {
		return nil, fmt.Errorf("GB/T 32960 identifier must be exactly 17 bytes, got %d", len(vin))
	}
	if len(body) > math.MaxUint16 {
		return nil, errors.New("GB/T 32960 body exceeds 65535 bytes")
	}
	frame := make([]byte, 24, 25+len(body))
	frame[0], frame[1] = start, start
	frame[2], frame[3] = command, response
	copy(frame[4:21], vin)
	frame[21] = 0x01
	binary.BigEndian.PutUint16(frame[22:24], uint16(len(body)))
	frame = append(frame, body...)
	frame = append(frame, bcc(frame[2:]))
	return frame, nil
}

func wholeVehicleUnit(record Record) ([]byte, bool) {
	if !hasAny(record, "2201", "2202", "3201", "7615") {
		return nil, false
	}
	out := make([]byte, 20)
	out[0] = enum(recordValue(record, "3201"), map[string]byte{
		"启动": 1, "启动状态": 1, "行驶": 1, "熄火": 2, "熄火状态": 2, "其他": 3,
	}, 0xFE)
	out[1] = enum(recordValue(record, "2301"), map[string]byte{
		"停车充电": 1, "行驶充电": 2, "未充电": 3, "未充电状态": 3, "充电完成": 4,
	}, 0xFE)
	out[2] = enum(recordValue(record, "2213"), map[string]byte{
		"纯电": 1, "纯电动": 1, "混动": 2, "混合动力": 2, "燃油": 3,
	}, 0xFE)
	putScaledU16(out[3:5], recordValue(record, "2201"), 10, 0)
	putScaledU32(out[5:9], recordValue(record, "2202"), 10)
	putScaledU16(out[9:11], recordValue(record, "2613"), 10, 0)
	putScaledU16(out[11:13], recordValue(record, "2614"), 10, 1000)
	out[13] = byteValue(recordValue(record, "7615"))
	out[14] = enum(recordValue(record, "2214"), map[string]byte{"工作": 1, "断开": 2}, 0xFE)
	out[15] = gearValue(firstField(recordValue(record, "2203")))
	putU16(out[16:18], recordValue(record, "2617"))
	out[18] = byteValue(recordValue(record, "2208"))
	out[19] = byteValue(recordValue(record, "2209"))
	return out, true
}

func motorUnit(record Record) ([]byte, bool) {
	// The source carries every motor as a key:value composite separated by "|".
	// Fields are the GB/T 32960 sequence numbers exposed by the platform metadata.
	composite := recordValue(record, "2308")
	if composite == "" {
		return nil, false
	}
	groups := parseCompositeGroups(composite)
	if len(groups) == 0 || len(groups) > 253 {
		return nil, false
	}
	out := []byte{byte(len(groups))}
	for index, group := range groups {
		out = append(out, byteValue(firstNonempty(group["2302"], strconv.Itoa(index+1))))
		out = append(out, enum(group["2303"], map[string]byte{
			"耗电": 1, "发电": 2, "关闭": 3, "准备": 4, "准备状态": 4,
		}, 0xFE))
		out = append(out, tempByte(group["2304"]))
		out = binary.BigEndian.AppendUint16(out, offsetU16(group["2305"], 1, 20000))
		out = binary.BigEndian.AppendUint16(out, offsetU16(group["2306"], 10, 2000))
		out = append(out, tempByte(group["2309"]))
		out = binary.BigEndian.AppendUint16(out, scaledU16(group["2311"], 10, 0))
		out = binary.BigEndian.AppendUint16(out, scaledU16(group["2312"], 10, 1000))
	}
	return out, true
}

func fuelCellUnit(record Record) ([]byte, bool) {
	if !hasAny(record, "2110", "2111", "2112", "2117", "2119") {
		return nil, false
	}
	temperatureGroups := parseSeriesGroups(recordValue(record, "2103"))
	var temperatures []string
	for _, group := range temperatureGroups {
		temperatures = append(temperatures, group.values...)
	}
	if len(temperatures) > math.MaxUint16 {
		temperatures = temperatures[:math.MaxUint16]
	}
	out := binary.BigEndian.AppendUint16(nil, scaledU16(recordValue(record, "2110"), 10, 0))
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2111"), 10, 0))
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2112"), 100, 0))
	out = binary.BigEndian.AppendUint16(out, uint16(len(temperatures)))
	for _, value := range temperatures {
		out = append(out, tempByte(value))
	}
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2115"), 10, 40))
	out = append(out, byteValue(recordValue(record, "2116")))
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2117"), 1, 0))
	out = append(out, byteValue(recordValue(record, "2118")))
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2119"), 10, 0))
	out = append(out, byteValue(recordValue(record, "2120")))
	out = append(out, enum(recordValue(record, "2121"), map[string]byte{"工作": 1, "断开": 2}, 0xFE))
	return out, true
}

func positionUnit(record Record) ([]byte, bool) {
	longitude, lonOK := floatValue(recordValue(record, "2502"))
	latitude, latOK := floatValue(recordValue(record, "2503"))
	if !lonOK || !latOK || longitude < 0 || latitude < 0 {
		return nil, false
	}
	status := byte(0)
	text := strings.TrimSpace(recordValue(record, "2501"))
	if text != "" && (strings.Contains(text, "无效") || text == "1") {
		status = 1
	}
	out := []byte{status}
	out = binary.BigEndian.AppendUint32(out, uint32(math.Round(longitude*1_000_000)))
	out = binary.BigEndian.AppendUint32(out, uint32(math.Round(latitude*1_000_000)))
	return out, true
}

func extremeUnit(record Record) ([]byte, bool) {
	if !hasAny(record, "2601", "2602", "2603", "2604", "2605", "2606", "2607", "2608", "2609", "2610", "2611", "2612") {
		return nil, false
	}
	out := []byte{
		byteValue(recordValue(record, "2601")),
		byteValue(recordValue(record, "2602")),
	}
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2603"), 1000, 0))
	out = append(out, byteValue(recordValue(record, "2604")), byteValue(recordValue(record, "2605")))
	out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2606"), 1000, 0))
	out = append(out,
		byteValue(recordValue(record, "2607")),
		byteValue(recordValue(record, "2608")),
		tempByte(recordValue(record, "2609")),
		byteValue(recordValue(record, "2610")),
		byteValue(recordValue(record, "2611")),
		tempByte(recordValue(record, "2612")),
	)
	return out, true
}

func alarmUnit(record Record) ([]byte, bool) {
	levelRaw := recordValue(record, "2900", "2901")
	var general uint32
	for bit := 0; bit < 32; bit++ {
		value := recordValue(record, strconv.Itoa(2901+bit))
		if truthy(value) {
			general |= 1 << bit
		}
	}
	if strings.TrimSpace(levelRaw) == "" && general == 0 {
		return nil, false
	}
	out := []byte{byteValue(levelRaw)}
	out = binary.BigEndian.AppendUint32(out, general)
	out = append(out, 0, 0, 0, 0)
	return out, true
}

func voltageUnits(record Record) [][]byte {
	raw := recordValue(record, "2003", "batteryVoltages")
	groups := parseSeriesGroups(raw)
	var units [][]byte
	for _, group := range groups {
		for start := 0; start < len(group.values); start += 255 {
			end := start + 255
			if end > len(group.values) {
				end = len(group.values)
			}
			out := []byte{1, byte(group.id)}
			out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2613"), 10, 0))
			out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2614"), 10, 1000))
			out = binary.BigEndian.AppendUint16(out, uint16(len(group.values)))
			out = binary.BigEndian.AppendUint16(out, uint16(start+1))
			out = append(out, byte(end-start))
			for _, value := range group.values[start:end] {
				out = binary.BigEndian.AppendUint16(out, scaledU16(value, 1000, 0))
			}
			units = append(units, out)
		}
	}
	return units
}

func temperatureUnits(record Record) [][]byte {
	groups := parseSeriesGroups(recordValue(record, "2103", "batteryTemperatures"))
	var units [][]byte
	for _, group := range groups {
		for start := 0; start < len(group.values); start += math.MaxUint16 {
			end := start + math.MaxUint16
			if end > len(group.values) {
				end = len(group.values)
			}
			out := []byte{1, byte(group.id)}
			out = binary.BigEndian.AppendUint16(out, uint16(end-start))
			for _, value := range group.values[start:end] {
				out = append(out, tempByte(value))
			}
			units = append(units, out)
		}
	}
	return units
}

func parseCompositeGroups(raw string) []map[string]string {
	var groups []map[string]string
	for _, encoded := range strings.Split(raw, "|") {
		group := map[string]string{}
		for _, field := range strings.Split(encoded, ",") {
			key, value, found := strings.Cut(field, ":")
			if found {
				group[strings.TrimSpace(key)] = strings.TrimSpace(value)
			}
		}
		if len(group) > 0 {
			groups = append(groups, group)
		}
	}
	return groups
}

type seriesGroup struct {
	id     int
	values []string
}

func parseSeriesGroups(raw string) []seriesGroup {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	var groups []seriesGroup
	for index, part := range strings.FieldsFunc(raw, func(r rune) bool { return r == ';' || r == '|' }) {
		group := seriesGroup{id: index + 1}
		if before, after, found := strings.Cut(part, ":"); found {
			if parsed, err := strconv.Atoi(strings.TrimSpace(before)); err == nil && parsed > 0 && parsed <= 255 {
				group.id = parsed
			}
			part = after
		}
		for _, value := range strings.FieldsFunc(part, func(r rune) bool {
			return r == '_' || r == ',' || r == ' ' || r == '[' || r == ']'
		}) {
			if strings.TrimSpace(value) != "" {
				group.values = append(group.values, strings.TrimSpace(value))
			}
		}
		if len(group.values) > 0 {
			groups = append(groups, group)
		}
	}
	return groups
}

func recordValue(record Record, keys ...string) string {
	for _, key := range keys {
		if value := strings.TrimSpace(record[key]); value != "" && value != "--" && value != "null" {
			return value
		}
	}
	return ""
}

func firstField(value string) string {
	fields := strings.Fields(value)
	if len(fields) == 0 {
		return ""
	}
	return fields[0]
}

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

func hasAny(record Record, keys ...string) bool { return recordValue(record, keys...) != "" }

func floatValue(raw string) (float64, bool) {
	raw = strings.TrimSpace(strings.TrimSuffix(raw, "%"))
	if raw == "" {
		return 0, false
	}
	value, err := strconv.ParseFloat(raw, 64)
	return value, err == nil && !math.IsNaN(value) && !math.IsInf(value, 0)
}

func byteValue(raw string) byte {
	value, ok := floatValue(raw)
	if !ok || value < 0 || value > 253 {
		return 0xFE
	}
	return byte(math.Round(value))
}

func tempByte(raw string) byte {
	value, ok := floatValue(raw)
	if !ok || value < -40 || value > 210 {
		return 0xFE
	}
	return byte(math.Round(value + 40))
}

func putU16(out []byte, raw string) { binary.BigEndian.PutUint16(out, scaledU16(raw, 1, 0)) }

func putScaledU16(out []byte, raw string, scale, offset float64) {
	binary.BigEndian.PutUint16(out, scaledU16(raw, scale, offset))
}

func putScaledU32(out []byte, raw string, scale float64) {
	value, ok := floatValue(raw)
	if !ok || value < 0 || value*scale > math.MaxUint32-2 {
		binary.BigEndian.PutUint32(out, 0xFFFFFFFE)
		return
	}
	binary.BigEndian.PutUint32(out, uint32(math.Round(value*scale)))
}

func scaledU16(raw string, scale, offset float64) uint16 {
	value, ok := floatValue(raw)
	encoded := (value + offset) * scale
	if !ok || encoded < 0 || encoded > math.MaxUint16-2 {
		return 0xFFFE
	}
	return uint16(math.Round(encoded))
}

func offsetU16(raw string, scale, offset float64) uint16 {
	return scaledU16(raw, scale, offset)
}

func enum(raw string, values map[string]byte, missing byte) byte {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return missing
	}
	if numeric, err := strconv.ParseUint(raw, 10, 8); err == nil {
		return byte(numeric)
	}
	for label, value := range values {
		if raw == label || strings.Contains(raw, label) {
			return value
		}
	}
	return missing
}

func gearValue(raw string) byte {
	raw = strings.ToUpper(strings.TrimSpace(raw))
	switch raw {
	case "P", "P档":
		return 15
	case "R", "R档":
		return 13
	case "N", "N档":
		return 0
	case "D", "D档":
		return 14
	}
	raw = strings.TrimSuffix(raw, "档")
	raw = strings.TrimPrefix(raw, "D")
	return byteValue(raw)
}

func truthy(raw string) bool {
	raw = strings.ToLower(strings.TrimSpace(raw))
	return raw != "" && raw != "0" && raw != "false" && raw != "无" && raw != "正常"
}

func appendPaddedASCII(out []byte, value string, width int) []byte {
	start := len(out)
	out = append(out, make([]byte, width)...)
	copy(out[start:start+width], value)
	return out
}

func encodeTime(value time.Time) []byte {
	local := value.In(shanghai)
	return []byte{
		byte(local.Year() - 2000), byte(local.Month()), byte(local.Day()),
		byte(local.Hour()), byte(local.Minute()), byte(local.Second()),
	}
}

func bcc(value []byte) byte {
	var result byte
	for _, current := range value {
		result ^= current
	}
	return result
}
