package device

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

	"vocat/internal/modem"
)

func (manager *Manager) readSnapshot(
	ctx context.Context,
	id string,
	candidate modem.Candidate,
	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)
	}
	if response, ok := optional("AT+CSQ"); ok {
		snapshot.SignalRaw, snapshot.SignalPercent, snapshot.RSSIDBm = parseCSQ(response)
	}
	if response, ok := optional(`AT+QENG="servingcell"`); ok {
		metrics := parseQENG(response)
		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)
		snapshot.OperatorName = operator.Name
		snapshot.OperatorCode = operator.Code
		if snapshot.AccessTech == "" {
			snapshot.AccessTech = operator.AccessTech
		}
	}
	if response, ok := optional("AT+CGSN"); ok {
		snapshot.IMEI = parseIdentifier(
			response,
			[]string{"+CGSN:", "+GSN:"},
			14,
			17,
		)
	}

	ccid, ccidErr := manager.command(ctx, client, "AT+CCID")
	if ccidErr != nil {
		ccid, ccidErr = manager.command(ctx, client, "AT+QCCID")
	}
	if ccidErr != nil {
		snapshot.Warnings = append(snapshot.Warnings, "read ICCID: "+ccidErr.Error())
	} else {
		snapshot.ICCID = parseICCIDIdentifier(ccid, []string{"+CCID:", "+QCCID:"}, 18, 22)
	}
	if response, ok := optional("AT+CIMI"); ok {
		snapshot.IMSI = parseIdentifier(response, []string{"+CIMI:"}, 10, 18)
	}
	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 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 {
	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 {
			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{}
}

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
}
