package device

import (
	"context"
	"encoding/csv"
	"encoding/hex"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf16"

	"vocat/internal/modem"
)

func (manager *Manager) readSnapshot(
	ctx context.Context,
	id string,
	candidate modem.Candidate,
	backend string,
	previousICCID string,
	previousSnapshot *Snapshot,
	client modem.Client,
) (Snapshot, error) {
	snapshot := Snapshot{
		DeviceID:      id,
		Port:          candidate.ATPort.OpenPath(),
		OperatingMode: -1,
		UpdatedAt:     time.Now().UTC(),
	}
	ati, err := manager.command(ctx, client, "ATI")
	if err != nil {
		return snapshot, fmt.Errorf("probe modem: %w", err)
	}
	snapshot.Responsive = true
	snapshot.Manufacturer, snapshot.Model, snapshot.Firmware = parseATI(ati.Lines)
	if snapshot.Model == "" && !strings.EqualFold(candidate.Product, "Android") {
		snapshot.Model = candidate.Product
	}

	optional := func(command string) (modem.Response, bool) {
		response, commandErr := manager.command(ctx, client, command)
		if commandErr != nil {
			snapshot.Warnings = append(snapshot.Warnings, commandErr.Error())
			return response, false
		}
		return response, true
	}

	if response, ok := optional("AT+CPIN?"); ok {
		snapshot.SIMStatus, snapshot.SIMReady = parseCPIN(response)
	}
	ccid, ccidErr := manager.command(ctx, client, "AT+CCID")
	if ccidErr != nil {
		ccid, ccidErr = manager.command(ctx, client, "AT+QCCID")
	}
	if ccidErr != nil && strings.EqualFold(strings.TrimSpace(backend), "qmi") && isNativeQMICandidate(candidate) {
		qmiContext, cancelQMI := manager.withTimeout(ctx, manager.commandTimeout*5)
		qmiICCID, qmiErr := manager.readNativeQMIICCID(qmiContext, candidate)
		cancelQMI()
		if qmiErr == nil {
			snapshot.ICCID = qmiICCID
			ccidErr = nil
		} else {
			snapshot.Warnings = append(snapshot.Warnings, "read ICCID via QMI UIM: "+qmiErr.Error())
		}
	}
	if ccidErr != nil {
		snapshot.Warnings = append(snapshot.Warnings, "read ICCID: "+ccidErr.Error())
	} else {
		if snapshot.ICCID == "" {
			snapshot.ICCID = parseICCIDIdentifier(ccid, []string{"+CCID:", "+QCCID:"}, 18, 22)
		}
	}
	previousICCID = strings.TrimSpace(previousICCID)
	if previousICCID != "" && snapshot.ICCID != "" && !strings.EqualFold(previousICCID, snapshot.ICCID) {
		// A different physical SIM must never inherit the previous card's
		// permission to use cellular RF. Disable RF before reading serving-cell
		// or operator state; policy reconciliation will then start VoWiFi.
		if _, err := manager.command(ctx, client, "AT+CFUN=4"); err != nil {
			return snapshot, fmt.Errorf("protect changed SIM with RF off: %w", err)
		}
		snapshot.SIMChanged = true
	}
	if response, ok := optional("AT+CIMI"); ok {
		snapshot.IMSI = parseIdentifier(response, []string{"+CIMI:"}, 10, 18)
	}
	// EF_SPN is the SIM-issued brand (for example "Lebara"), which is distinct
	// from the IMSI sponsor/core PLMN. A Lebara UK subscription may therefore
	// legitimately carry a Vodafone NL IMSI while still presenting Lebara as
	// its customer-facing operator. Failure is intentionally silent because
	// EF_SPN is optional and some physical SIMs deny CRSM access to it.
	if response, spnErr := manager.command(ctx, client, "AT+CRSM=176,28486,0,0,17"); spnErr == nil {
		snapshot.SPN = parseSPN(response)
	}
	if previousSnapshot != nil && previousSnapshot.IdentityFilesRead &&
		strings.EqualFold(strings.TrimSpace(previousSnapshot.ICCID), strings.TrimSpace(snapshot.ICCID)) {
		snapshot.MNCLength = previousSnapshot.MNCLength
		snapshot.GID1 = previousSnapshot.GID1
		snapshot.GID2 = previousSnapshot.GID2
		snapshot.IdentityFilesRead = true
	} else {
		// Android's carrier resolver does not identify MVNOs from MCC/MNC alone.
		// Read these files once per inserted ICCID and cache even an empty result;
		// repeatedly probing unsupported EFs would add avoidable modem traffic.
		if efAD := manager.readTransparentSIMFile(ctx, client, 28589); len(efAD) >= 4 {
			if length := int(efAD[3] & 0x0f); length == 2 || length == 3 {
				snapshot.MNCLength = length
			}
		}
		snapshot.GID1 = encodeSIMGroupID(manager.readTransparentSIMFile(ctx, client, 28478))
		snapshot.GID2 = encodeSIMGroupID(manager.readTransparentSIMFile(ctx, client, 28479))
		snapshot.IdentityFilesRead = true
	}
	if response, ok := optional("AT+CSQ"); ok {
		snapshot.SignalRaw, snapshot.SignalPercent, snapshot.RSSIDBm = parseCSQ(response)
	}
	servingPLMN := ""
	if response, ok := optional(`AT+QENG="servingcell"`); ok {
		metrics := parseQENG(response)
		servingPLMN = metrics.PLMN
		snapshot.AccessTech = metrics.AccessTech
		snapshot.Band = metrics.Band
		snapshot.Channel = metrics.Channel
		snapshot.RSRP = metrics.RSRP
		snapshot.RSRQ = metrics.RSRQ
		snapshot.SINR = metrics.SINR
		if metrics.RSSI != nil {
			snapshot.RSSIDBm = metrics.RSSI
		}
	}
	if response, ok := optional("AT+COPS?"); ok {
		operator := parseCOPS(response)
		if operator.Code != "" {
			snapshot.OperatorCode = operator.Code
		} else {
			snapshot.OperatorCode = servingPLMN
		}
		snapshot.OperatorName = carrierNameForPLMN(snapshot.OperatorCode, operator.Name)
		if snapshot.AccessTech == "" {
			snapshot.AccessTech = operator.AccessTech
		}
	}
	for _, command := range []string{"AT+CEREG?", "AT+CGREG?", "AT+CREG?"} {
		response, registrationErr := manager.command(ctx, client, command)
		if registrationErr != nil {
			continue
		}
		if status, found := parseRegistrationStatus(response); found {
			snapshot.RegistrationStatus = status
			snapshot.RegistrationSource = strings.TrimSuffix(strings.TrimPrefix(command, "AT+"), "?")
			break
		}
	}
	if strings.EqualFold(backend, "qmi") {
		registration, found := readPlatformRegistration(ctx, candidate)
		if found {
			snapshot.RegistrationStatus = registration.Status
			snapshot.RegistrationSource = "QMI NAS"
			snapshot.PSAttached = registration.PSAttached
			if registration.PLMN != "" {
				snapshot.OperatorCode = registration.PLMN
				snapshot.OperatorName = carrierNameForPLMN(registration.PLMN, registration.Name)
			}
		}
	}
	if snapshot.RegistrationSource == "" && (snapshot.OperatorName != "" || snapshot.OperatorCode != "") {
		// Older firmware can omit registration queries while COPS still proves
		// that an operator is selected.
		snapshot.RegistrationStatus = 1
		snapshot.RegistrationSource = "COPS"
	}
	if response, ok := optional("AT+CGSN"); ok {
		snapshot.IMEI = parseIdentifier(
			response,
			[]string{"+CGSN:", "+GSN:"},
			14,
			17,
		)
	}

	if response, ok := optional("AT+CFUN?"); ok {
		if mode, found := parseCFUN(response); found {
			snapshot.OperatingMode = mode
			snapshot.ModeKnown = true
			snapshot.FlightMode = isRadioOffMode(mode)
			snapshot.RadioOff = snapshot.FlightMode
		}
	}

	phone, warnings := manager.readPhoneNumber(ctx, client)
	snapshot.Phone = phone
	snapshot.Warnings = append(snapshot.Warnings, warnings...)
	snapshot.UpdatedAt = time.Now().UTC()
	return snapshot, nil
}

func (manager *Manager) readTransparentSIMFile(ctx context.Context, client modem.Client, fileID int) []byte {
	response, err := manager.command(ctx, client, fmt.Sprintf("AT+CRSM=192,%d,0,0,0", fileID))
	if err != nil {
		return nil
	}
	size := transparentSIMFileSize(crsmPayload(response))
	if size <= 0 || size > 64 {
		return nil
	}
	response, err = manager.command(ctx, client, fmt.Sprintf("AT+CRSM=176,%d,0,0,%d", fileID, size))
	if err != nil {
		return nil
	}
	return crsmPayload(response)
}

func transparentSIMFileSize(payload []byte) int {
	// USIM FCP templates contain file size in tag 0x80. Skip the outer 0x62
	// template and walk its immediate TLVs.
	content := payload
	if len(content) >= 2 && content[0] == 0x62 {
		length, header, ok := berLength(content[1:])
		if !ok || 1+header+length > len(content) {
			return 0
		}
		content = content[1+header : 1+header+length]
	}
	for offset := 0; offset+2 <= len(content); {
		tag := content[offset]
		length, header, ok := berLength(content[offset+1:])
		start := offset + 1 + header
		end := start + length
		if !ok || end > len(content) {
			break
		}
		if tag == 0x80 && (length == 1 || length == 2) {
			size := 0
			for _, value := range content[start:end] {
				size = size<<8 | int(value)
			}
			return size
		}
		offset = end
	}
	// Legacy GSM GET RESPONSE data stores file size in bytes 2 and 3.
	if len(payload) >= 4 && payload[0] != 0x62 {
		return int(payload[2])<<8 | int(payload[3])
	}
	return 0
}

func berLength(value []byte) (length, header int, ok bool) {
	if len(value) == 0 {
		return 0, 0, false
	}
	if value[0] < 0x80 {
		return int(value[0]), 1, true
	}
	count := int(value[0] & 0x7f)
	if count == 0 || count > 2 || len(value) < count+1 {
		return 0, 0, false
	}
	for _, item := range value[1 : count+1] {
		length = length<<8 | int(item)
	}
	return length, count + 1, true
}

func encodeSIMGroupID(value []byte) string {
	if len(value) == 0 {
		return ""
	}
	allPadding := true
	for _, item := range value {
		if item != 0xff {
			allPadding = false
			break
		}
	}
	if allPadding {
		return ""
	}
	return strings.ToUpper(hex.EncodeToString(value))
}

func parseSPN(response modem.Response) string {
	value := valueAfterPrefix(response, "+CRSM:")
	fields := csvValues(value)
	if len(fields) < 3 {
		return ""
	}
	sw1, sw1Err := strconv.Atoi(strings.TrimSpace(fields[0]))
	sw2, sw2Err := strconv.Atoi(strings.TrimSpace(fields[1]))
	if sw1Err != nil || sw2Err != nil || (sw1 != 0x90 && sw1 != 0x91 && sw1 != 0x9f) || sw2 < 0 || sw2 > 255 {
		return ""
	}
	raw, err := hex.DecodeString(strings.Trim(strings.TrimSpace(fields[2]), `"`))
	if err != nil || len(raw) < 2 {
		return ""
	}
	alpha := raw[1:] // byte 0 is the display-condition bit field.
	for len(alpha) > 0 && (alpha[len(alpha)-1] == 0xff || alpha[len(alpha)-1] == 0x00) {
		alpha = alpha[:len(alpha)-1]
	}
	if len(alpha) == 0 {
		return ""
	}
	if alpha[0] == 0x80 {
		ucs2 := alpha[1:]
		if len(ucs2)%2 != 0 {
			ucs2 = ucs2[:len(ucs2)-1]
		}
		units := make([]uint16, 0, len(ucs2)/2)
		for index := 0; index+1 < len(ucs2); index += 2 {
			unit := uint16(ucs2[index])<<8 | uint16(ucs2[index+1])
			if unit != 0xffff && unit != 0 {
				units = append(units, unit)
			}
		}
		return strings.TrimSpace(string(utf16.Decode(units)))
	}
	// EF_SPN uses the unpacked GSM default alphabet. Its printable Latin subset
	// is byte-compatible with UTF-8/ASCII and covers operator brands in practice.
	printable := make([]byte, 0, len(alpha))
	for _, value := range alpha {
		if value >= 0x20 && value <= 0x7e {
			printable = append(printable, value)
		}
	}
	return strings.TrimSpace(string(printable))
}

func parseRegistrationStatus(response modem.Response) (int, bool) {
	for _, prefix := range []string{"+CEREG:", "+CGREG:", "+CREG:"} {
		values := csvValues(valueAfterPrefix(response, prefix))
		if len(values) == 0 {
			continue
		}
		index := 0
		// Query responses are <n>,<stat>; unsolicited responses are <stat>.
		if len(values) >= 2 {
			index = 1
		}
		status, err := strconv.Atoi(strings.TrimSpace(values[index]))
		if err == nil && status >= 0 && status <= 10 {
			return status, true
		}
	}
	return 0, false
}

func parseATI(lines []string) (manufacturer, model, firmware string) {
	for _, line := range lines {
		line = strings.TrimSpace(line)
		upper := strings.ToUpper(line)
		switch {
		case strings.HasPrefix(upper, "REVISION:"):
			firmware = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
		case strings.Contains(upper, "QUECTEL"):
			manufacturer = line
		case strings.HasPrefix(upper, "EC20") || strings.HasPrefix(upper, "EC25"):
			model = line
		}
	}
	return
}

func parseCPIN(response modem.Response) (string, bool) {
	value := strings.ToUpper(valueAfterPrefix(response, "+CPIN:"))
	switch {
	case strings.Contains(value, "READY"):
		return "ready", true
	case strings.Contains(value, "SIM PIN"):
		return "pin_required", false
	case strings.Contains(value, "SIM PUK"):
		return "puk_required", false
	case strings.Contains(value, "NOT INSERTED"):
		return "not_inserted", false
	case value == "":
		return "unknown", false
	default:
		return strings.ToLower(strings.ReplaceAll(value, " ", "_")), false
	}
}

func parseCSQ(response modem.Response) (raw, percent, dbm *int) {
	values := csvValues(valueAfterPrefix(response, "+CSQ:"))
	if len(values) == 0 {
		return nil, nil, nil
	}
	value, err := strconv.Atoi(values[0])
	if err != nil || value < 0 || value > 31 {
		return nil, nil, nil
	}
	raw = intPointer(value)
	scaled := (value*100 + 15) / 31
	percent = intPointer(scaled)
	signalDBM := -113 + value*2
	dbm = intPointer(signalDBM)
	return
}

type qengMetrics struct {
	PLMN       string
	AccessTech string
	Band       string
	Channel    string
	RSSI       *int
	RSRP       *int
	RSRQ       *int
	SINR       *int
}

func parseQENG(response modem.Response) qengMetrics {
	for _, line := range response.Lines {
		if !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(line)), "+QENG:") {
			continue
		}
		values := csvValues(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]))
		if len(values) < 3 || !strings.EqualFold(values[0], "servingcell") {
			continue
		}
		result := qengMetrics{AccessTech: strings.ToUpper(values[2])}
		if strings.EqualFold(values[2], "LTE") && len(values) >= 17 {
			if decimalDigits(values[4], 3, 3) && decimalDigits(values[5], 2, 3) {
				result.PLMN = values[4] + values[5]
			}
			result.Channel = values[8]
			if values[9] != "" {
				result.Band = "B" + values[9]
			}
			result.RSRP = parseOptionalInt(values[13])
			result.RSRQ = parseOptionalInt(values[14])
			result.RSSI = parseOptionalInt(values[15])
			result.SINR = parseOptionalInt(values[16])
		}
		return result
	}
	return qengMetrics{}
}

func decimalDigits(value string, minimum, maximum int) bool {
	value = strings.TrimSpace(value)
	return len(value) >= minimum && len(value) <= maximum && strings.IndexFunc(value, func(character rune) bool {
		return character < '0' || character > '9'
	}) < 0
}

type operatorInfo struct {
	Name       string
	Code       string
	AccessTech string
}

func parseCOPS(response modem.Response) operatorInfo {
	values := csvValues(valueAfterPrefix(response, "+COPS:"))
	if len(values) < 3 {
		return operatorInfo{}
	}
	result := operatorInfo{Name: values[2]}
	format, _ := strconv.Atoi(values[1])
	if format == 2 {
		result.Code = values[2]
		result.Name = ""
	}
	if len(values) >= 4 {
		result.AccessTech = accessTechnology(values[3])
	}
	return result
}

func accessTechnology(value string) string {
	switch strings.TrimSpace(value) {
	case "0":
		return "GSM"
	case "2":
		return "UTRAN"
	case "3":
		return "EDGE"
	case "4":
		return "HSDPA"
	case "5":
		return "HSUPA"
	case "6":
		return "HSPA"
	case "7":
		return "LTE"
	case "9":
		return "NR5G"
	default:
		return ""
	}
}

func parseCFUN(response modem.Response) (int, bool) {
	values := csvValues(valueAfterPrefix(response, "+CFUN:"))
	if len(values) == 0 {
		return 0, false
	}
	mode, err := strconv.Atoi(values[0])
	return mode, err == nil
}

func isRadioOffMode(mode int) bool {
	return mode == 0 || mode == 4
}

func valueAfterPrefix(response modem.Response, prefix string) string {
	for _, line := range response.Lines {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(strings.ToUpper(line), strings.ToUpper(prefix)) {
			return strings.TrimSpace(line[len(prefix):])
		}
	}
	return ""
}

func csvValues(value string) []string {
	reader := csv.NewReader(strings.NewReader(value))
	reader.TrimLeadingSpace = true
	reader.LazyQuotes = true
	record, err := reader.Read()
	if err != nil && err != io.EOF {
		return nil
	}
	for index := range record {
		record[index] = strings.TrimSpace(record[index])
	}
	return record
}

func firstDigitLine(response modem.Response, minimum, maximum int) string {
	for _, line := range response.Lines {
		value := strings.TrimSpace(line)
		if len(value) < minimum || len(value) > maximum {
			continue
		}
		if strings.IndexFunc(value, func(character rune) bool {
			return !unicode.IsDigit(character)
		}) < 0 {
			return value
		}
	}
	return ""
}

func parseIdentifier(
	response modem.Response,
	prefixes []string,
	minimum, maximum int,
) string {
	for _, prefix := range prefixes {
		value := strings.Trim(valueAfterPrefix(response, prefix), `" `)
		if len(value) >= minimum && len(value) <= maximum &&
			strings.IndexFunc(value, func(character rune) bool {
				return !unicode.IsDigit(character)
			}) < 0 {
			return value
		}
	}
	return firstDigitLine(response, minimum, maximum)
}

// parseICCIDIdentifier accepts all trailing hexadecimal F nibbles exposed from
// the fixed 10-octet EF-ICCID representation. A 19-digit ICCID has one filler
// nibble while an 18-digit ICCID has two; neither is part of the identifier.
func parseICCIDIdentifier(
	response modem.Response,
	prefixes []string,
	minimum, maximum int,
) string {
	normalize := func(value string) string {
		value = strings.Trim(value, `" `)
		value = strings.TrimRight(value, "Ff")
		if len(value) >= minimum && len(value) <= maximum &&
			strings.IndexFunc(value, func(character rune) bool { return !unicode.IsDigit(character) }) < 0 {
			return value
		}
		return ""
	}
	for _, prefix := range prefixes {
		if value := normalize(valueAfterPrefix(response, prefix)); value != "" {
			return value
		}
	}
	for _, line := range response.Lines {
		if value := normalize(strings.TrimSpace(line)); value != "" {
			return value
		}
	}
	return ""
}

func parseOptionalInt(value string) *int {
	number, err := strconv.Atoi(strings.TrimSpace(value))
	if err != nil {
		return nil
	}
	return intPointer(number)
}

func intPointer(value int) *int {
	return &value
}
