package vowifi

import (
	"context"
	"encoding/csv"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"vocat/internal/modem"
)

var (
	ErrEC20SIMNotReady       = errors.New("vocat: EC20 SIM is not ready")
	ErrEC20MNCUnavailable    = errors.New("vocat: EC20 SIM does not expose an explicit MNC length")
	ErrEC20ApplicationAbsent = errors.New("vocat: EC20 has no usable USIM or ISIM application")
	ErrEC20IdentityChanged   = errors.New("vocat: EC20 SIM identity changed during authentication")
	ErrEC20AKACommand        = errors.New("vocat: EC20 USIM AUTHENTICATE command failed")
	ErrEC20AKAResponse       = errors.New("vocat: EC20 returned an invalid USIM AUTHENTICATE response")
	ErrEC20AKAMACFailure     = errors.New("vocat: EC20 USIM rejected the network authentication token")
)

const (
	usimAIDPrefix         = "A0000000871002"
	isimAIDPrefix         = "A0000000871004"
	efADDecimal           = 28589 // 0x6FAD
	channelCleanupTimeout = 3 * time.Second
)

// EC20ATExecutor is deliberately the same narrow shape as
// device.Manager.ExecuteAT. Production code passes the device manager directly;
// tests can use an evidence transcript without opening any serial device.
type EC20ATExecutor interface {
	ExecuteAT(context.Context, string, string) (modem.Response, error)
}

// EC20SensitiveATExecutor is implemented by device.Manager so an APDU carrying
// RAND and AUTN is not retained as a device error. The fallback exists only for
// small deterministic test executors.
type EC20SensitiveATExecutor interface {
	ExecuteSensitiveAT(context.Context, string, string) (modem.Response, error)
}

type EC20AdapterOptions struct {
	// PureAirplanePolicy reports the independent user policy. The adapter only
	// changes the transactional CFUN projection used by VoWiFi and never
	// changes this policy.
	PureAirplanePolicy func(deviceID string) bool

	// HomePLMN supplies an explicit operator configuration when EF_AD omits
	// the MNC length. The returned MCC/MNC must exactly prefix the live IMSI.
	HomePLMN func(deviceID, iccid, imsi string) (mcc, mnc string, ok bool)

	// RestoreCellularData permits reactivating PDP contexts that were active
	// before the VoWiFi transaction. It is deliberately false by default:
	// VoWiFi must never start billable cellular data unless an operator has
	// explicitly opted in to that separate behavior.
	RestoreCellularData bool
}

// EC20Adapter implements SIMIdentityReader, AKAProvider, and RadioController
// using standardized AT commands on the AT port selected by device.Manager.
// It never discovers or opens serial ports itself, so ttyUSB0 diagnostic cannot
// be selected accidentally.
type EC20Adapter struct {
	executor EC20ATExecutor
	options  EC20AdapterOptions

	apduMu      sync.Mutex
	mu          sync.Mutex
	bindings    map[string]ec20SIMBinding
	checkpoints map[string]ec20RadioCheckpoint
}

type ec20SIMBinding struct {
	deviceID     string
	iccid        string
	imsi         string
	aid          string
	application  string
	basicChannel bool
}

type ec20RadioCheckpoint struct {
	activeCIDs []int
}

var (
	_ SIMIdentityReader = (*EC20Adapter)(nil)
	_ AKAProvider       = (*EC20Adapter)(nil)
	_ RadioController   = (*EC20Adapter)(nil)
)

func NewEC20Adapter(
	executor EC20ATExecutor,
	options EC20AdapterOptions,
) (*EC20Adapter, error) {
	if executor == nil {
		return nil, errors.New("vocat: EC20 AT executor is required")
	}
	return &EC20Adapter{
		executor:    executor,
		options:     options,
		bindings:    make(map[string]ec20SIMBinding),
		checkpoints: make(map[string]ec20RadioCheckpoint),
	}, nil
}

func (adapter *EC20Adapter) ReadIdentity(
	ctx context.Context,
	deviceID string,
) (SIMIdentity, error) {
	deviceID = strings.TrimSpace(deviceID)
	if deviceID == "" {
		return SIMIdentity{}, errors.New("vocat: EC20 device ID is required")
	}

	pin, err := adapter.execute(ctx, deviceID, "AT+CPIN?")
	if err != nil {
		return SIMIdentity{}, fmt.Errorf("read EC20 SIM state: %w", err)
	}
	if !responseContainsValue(pin, "+CPIN:", "READY") {
		return SIMIdentity{}, ErrEC20SIMNotReady
	}

	imsiResponse, err := adapter.execute(ctx, deviceID, "AT+CIMI")
	if err != nil {
		return SIMIdentity{}, fmt.Errorf("read EC20 IMSI: %w", err)
	}
	imsi := digitIdentifier(imsiResponse, []string{"+CIMI:"}, 10, 18)
	if imsi == "" {
		return SIMIdentity{}, errors.New("vocat: EC20 returned no valid IMSI")
	}
	iccid, err := adapter.readICCID(ctx, deviceID)
	if err != nil {
		return SIMIdentity{}, err
	}

	imeiResponse, err := adapter.execute(ctx, deviceID, "AT+CGSN")
	if err != nil {
		return SIMIdentity{}, fmt.Errorf("read EC20 IMEI: %w", err)
	}
	imei := digitIdentifier(imeiResponse, []string{"+CGSN:", "+GSN:"}, 14, 17)
	if imei == "" {
		return SIMIdentity{}, errors.New("vocat: EC20 returned no valid IMEI")
	}

	homeMCC, homeMNC, err := adapter.readHomePLMN(
		ctx,
		deviceID,
		iccid,
		imsi,
	)
	if err != nil {
		return SIMIdentity{}, err
	}
	identity := SIMIdentity{
		ICCID:   iccid,
		IMSI:    imsi,
		IMEI:    imei,
		HomeMCC: homeMCC,
		HomeMNC: homeMNC,
	}
	adapter.mu.Lock()
	adapter.bindings[iccid] = ec20SIMBinding{
		deviceID: deviceID,
		iccid:    iccid,
		imsi:     imsi,
	}
	adapter.mu.Unlock()
	return identity, nil
}

// ReadSMSCenter returns the service-centre address configured by the SIM.
// AT+CSCA? is read-only and remains available while cellular RF is disabled
// for a Wi-Fi Calling session.
func (adapter *EC20Adapter) ReadSMSCenter(ctx context.Context, deviceID string) (string, error) {
	response, err := adapter.execute(ctx, strings.TrimSpace(deviceID), "AT+CSCA?")
	if err != nil {
		return "", fmt.Errorf("read EC20 SMS service centre: %w", err)
	}
	fields := parseCSV(valueAfterATPrefix(response, "+CSCA:"))
	if len(fields) == 0 {
		return "", errors.New("vocat: EC20 returned no SMS service-centre address")
	}
	value := strings.Trim(strings.TrimSpace(fields[0]), `"`)
	digits := strings.TrimPrefix(value, "+")
	if !validDigits(digits, 3, 20) {
		return "", errors.New("vocat: EC20 returned an invalid SMS service-centre address")
	}
	return value, nil
}

func (adapter *EC20Adapter) readHomePLMN(
	ctx context.Context,
	deviceID string,
	iccid string,
	imsi string,
) (string, string, error) {
	mncLength, efErr := adapter.readExplicitMNCLength(ctx, deviceID)
	if efErr == nil {
		if len(imsi) < 3+mncLength {
			return "", "", errors.New(
				"vocat: IMSI is shorter than the EF_AD home PLMN",
			)
		}
		return imsi[:3], imsi[3 : 3+mncLength], nil
	}
	if adapter.options.HomePLMN != nil {
		mcc, mnc, ok := adapter.options.HomePLMN(deviceID, iccid, imsi)
		mcc = strings.TrimSpace(mcc)
		mnc = strings.TrimSpace(mnc)
		if ok && validConfiguredHomePLMN(imsi, mcc, mnc) {
			return mcc, mnc, nil
		}
	}
	// Exact assigned HPLMN prefixes are data, not an MNC-length heuristic. The
	// target Vodafone UK SIM is 234/15. Unknown assignments remain fail-closed.
	for prefix, mncLength := range map[string]int{"23415": 2} {
		if strings.HasPrefix(imsi, prefix) {
			return imsi[:3], imsi[3 : 3+mncLength], nil
		}
	}
	return "", "", efErr
}

func validConfiguredHomePLMN(imsi, mcc, mnc string) bool {
	if !validDigits(mcc, 3, 3) || !validDigits(mnc, 2, 3) {
		return false
	}
	return strings.HasPrefix(imsi, mcc+mnc)
}

func (adapter *EC20Adapter) readExplicitMNCLength(
	ctx context.Context,
	deviceID string,
) (int, error) {
	commands := []string{
		fmt.Sprintf("AT+CRSM=176,%d,0,0,4", efADDecimal),
		fmt.Sprintf("AT+CRSM=176,%d,0,0,0", efADDecimal),
	}
	var lastErr error
	for _, command := range commands {
		response, err := adapter.execute(ctx, deviceID, command)
		if err != nil {
			lastErr = err
			continue
		}
		data, err := parseCRSMData(response)
		if err != nil {
			lastErr = err
			continue
		}
		if len(data) < 4 {
			lastErr = ErrEC20MNCUnavailable
			continue
		}
		length := int(data[3] & 0x0f)
		if length == 2 || length == 3 {
			return length, nil
		}
		lastErr = ErrEC20MNCUnavailable
	}
	if lastErr == nil {
		lastErr = ErrEC20MNCUnavailable
	}
	return 0, fmt.Errorf("%w: %v", ErrEC20MNCUnavailable, lastErr)
}

func (adapter *EC20Adapter) readICCID(
	ctx context.Context,
	deviceID string,
) (string, error) {
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		for _, command := range []string{"AT+CCID", "AT+QCCID"} {
			response, err := adapter.execute(ctx, deviceID, command)
			if err != nil {
				lastErr = err
				continue
			}
			value := iccidIdentifier(
				response,
				[]string{"+CCID:", "+QCCID:"},
				18,
				22,
			)
			if value != "" {
				return value, nil
			}
			lastErr = errors.New("response contained no valid ICCID")
		}
		if attempt < 2 {
			select {
			case <-ctx.Done():
				return "", ctx.Err()
			case <-time.After(100 * time.Millisecond):
			}
		}
	}
	return "", fmt.Errorf("read EC20 ICCID: %w", lastErr)
}

func (adapter *EC20Adapter) CheckReady(
	ctx context.Context,
	identity SIMIdentity,
) (AKAEvidence, error) {
	binding, err := adapter.bindingFor(identity)
	if err != nil {
		return AKAEvidence{}, err
	}
	if err := adapter.verifyLiveICCID(ctx, binding); err != nil {
		return AKAEvidence{}, err
	}

	// CCHO/CGLA/GET RESPONSE/CCHC is one UICC transaction. Serialize it
	// across the adapter so periodic device refreshes or another AKA exchange
	// cannot insert an APDU between a 61xx response and GET RESPONSE.
	adapter.apduMu.Lock()
	defer adapter.apduMu.Unlock()

	aid, application, err := adapter.discoverAKAApplication(ctx, binding.deviceID)
	if err != nil {
		return AKAEvidence{}, err
	}
	channel, err := adapter.openLogicalChannel(ctx, binding.deviceID, aid)
	basicChannel := false
	if err == nil {
		if err := adapter.closeLogicalChannelWithCleanup(
			binding.deviceID,
			channel,
		); err != nil {
			return AKAEvidence{}, err
		}
	} else {
		// Several EC20 firmware branches reject CCHO/CGLA even though their
		// basic-channel CSIM implementation is standards compliant.
		if err := adapter.selectBasicApplication(
			ctx,
			binding.deviceID,
			aid,
		); err != nil {
			return AKAEvidence{}, errors.Join(
				ErrEC20ApplicationAbsent,
				err,
			)
		}
		basicChannel = true
	}

	binding.aid = aid
	binding.application = application
	binding.basicChannel = basicChannel
	adapter.mu.Lock()
	adapter.bindings[binding.iccid] = binding
	adapter.mu.Unlock()
	return AKAEvidence{Ready: true, Application: application}, nil
}

func (adapter *EC20Adapter) Authenticate(
	ctx context.Context,
	identity SIMIdentity,
	challenge AKAChallenge,
) (AKAResult, error) {
	binding, err := adapter.bindingFor(identity)
	if err != nil {
		return AKAResult{}, err
	}
	if binding.aid == "" {
		if _, err := adapter.CheckReady(ctx, identity); err != nil {
			return AKAResult{}, err
		}
		binding, err = adapter.bindingFor(identity)
		if err != nil {
			return AKAResult{}, err
		}
	}
	if err := adapter.verifyLiveICCID(ctx, binding); err != nil {
		return AKAResult{}, err
	}

	adapter.apduMu.Lock()
	defer adapter.apduMu.Unlock()

	apdu := buildUSIMAuthenticateAPDU(challenge)
	var raw []byte
	if binding.basicChannel {
		if err := adapter.selectBasicApplication(
			ctx,
			binding.deviceID,
			binding.aid,
		); err != nil {
			return AKAResult{}, err
		}
		raw, err = adapter.transmitBasicAPDU(
			ctx,
			binding.deviceID,
			apdu,
			true,
		)
		if err != nil {
			return AKAResult{}, ErrEC20AKACommand
		}
	} else {
		channel, err := adapter.openLogicalChannel(
			ctx,
			binding.deviceID,
			binding.aid,
		)
		if err != nil {
			return AKAResult{}, err
		}
		var commandErr error
		raw, commandErr = adapter.transmitLogicalAPDU(
			ctx,
			binding.deviceID,
			channel,
			apdu,
			true,
		)
		closeErr := adapter.closeLogicalChannelWithCleanup(
			binding.deviceID,
			channel,
		)
		if commandErr != nil {
			if closeErr != nil {
				return AKAResult{}, errors.Join(commandErr, closeErr)
			}
			return AKAResult{}, commandErr
		}
		if closeErr != nil {
			return AKAResult{}, closeErr
		}
	}
	return parseUSIMAuthenticateResponse(raw)
}

func buildUSIMAuthenticateAPDU(challenge AKAChallenge) []byte {
	// TS 31.102 AUTHENTICATE, 3G security context (P2=0x81):
	// Lc=34, then LV(RAND) and LV(AUTN), followed by Le.
	apdu := make([]byte, 0, 40)
	apdu = append(apdu, 0x00, 0x88, 0x00, 0x81, 0x22, 0x10)
	apdu = append(apdu, challenge.RAND[:]...)
	apdu = append(apdu, 0x10)
	apdu = append(apdu, challenge.AUTN[:]...)
	apdu = append(apdu, 0x00)
	return apdu
}

func parseUSIMAuthenticateResponse(raw []byte) (AKAResult, error) {
	if len(raw) < 2 {
		return AKAResult{}, fmt.Errorf(
			"%w: response length %d has no status word",
			ErrEC20AKAResponse,
			len(raw),
		)
	}
	status := uint16(raw[len(raw)-2])<<8 | uint16(raw[len(raw)-1])
	body := raw[:len(raw)-2]
	if status != 0x9000 {
		switch status {
		case 0x9862:
			return AKAResult{}, ErrEC20AKAMACFailure
		default:
			return AKAResult{}, fmt.Errorf(
				"%w: status word %04X",
				ErrEC20AKAResponse,
				status,
			)
		}
	}
	if len(body) < 2 {
		return AKAResult{}, fmt.Errorf(
			"%w: response body length %d is too short",
			ErrEC20AKAResponse,
			len(body),
		)
	}
	tag := body[0]
	value := body[1:]

	switch tag {
	case 0xdb:
		res, rest, ok := takeLV(value)
		if !ok {
			return AKAResult{}, fmt.Errorf(
				"%w: malformed RES field in %d-byte success value",
				ErrEC20AKAResponse,
				len(value),
			)
		}
		if len(res) < 4 || len(res) > 16 {
			return AKAResult{}, fmt.Errorf(
				"%w: invalid RES length %d",
				ErrEC20AKAResponse,
				len(res),
			)
		}
		ck, rest, ok := takeLV(rest)
		if !ok || len(ck) != 16 {
			return AKAResult{}, fmt.Errorf(
				"%w: invalid CK length %d",
				ErrEC20AKAResponse,
				len(ck),
			)
		}
		ik, rest, ok := takeLV(rest)
		if !ok || len(ik) != 16 {
			return AKAResult{}, fmt.Errorf(
				"%w: invalid IK length %d",
				ErrEC20AKAResponse,
				len(ik),
			)
		}
		// Kc is present on many USIMs. It is not needed by EAP-AKA, but when
		// present its LV still has to be structurally valid.
		if len(rest) > 0 {
			kc, tail, ok := takeLV(rest)
			if !ok || len(kc) != 8 || len(tail) != 0 {
				return AKAResult{}, fmt.Errorf(
					"%w: invalid optional Kc length %d with %d trailing bytes",
					ErrEC20AKAResponse,
					len(kc),
					len(tail),
				)
			}
		}
		return AKAResult{
			RES: append([]byte(nil), res...),
			CK:  append([]byte(nil), ck...),
			IK:  append([]byte(nil), ik...),
		}, nil
	case 0xdc:
		auts, tail, ok := takeLV(value)
		if !ok || len(auts) != 14 || len(tail) != 0 {
			return AKAResult{}, fmt.Errorf(
				"%w: invalid AUTS length %d with %d trailing bytes",
				ErrEC20AKAResponse,
				len(auts),
				len(tail),
			)
		}
		return AKAResult{
			AUTS:                   append([]byte(nil), auts...),
			SynchronizationFailure: true,
		}, nil
	default:
		return AKAResult{}, fmt.Errorf(
			"%w: unsupported response tag %02X with %d value bytes",
			ErrEC20AKAResponse,
			tag,
			len(value),
		)
	}
}

func takeLV(value []byte) (field, rest []byte, ok bool) {
	if len(value) == 0 {
		return nil, value, false
	}
	length := int(value[0])
	if length > len(value)-1 {
		return nil, value, false
	}
	return value[1 : 1+length], value[1+length:], true
}

func (adapter *EC20Adapter) Snapshot(
	ctx context.Context,
	deviceID string,
) (RadioSnapshot, error) {
	mode, err := adapter.readOperatingMode(ctx, deviceID)
	if err != nil {
		return RadioSnapshot{}, err
	}
	active, err := adapter.readActiveCIDs(ctx, deviceID)
	if err != nil {
		return RadioSnapshot{}, err
	}
	adapter.mu.Lock()
	adapter.checkpoints[deviceID] = ec20RadioCheckpoint{
		activeCIDs: append([]int(nil), active...),
	}
	adapter.mu.Unlock()

	purePolicy := false
	if adapter.options.PureAirplanePolicy != nil {
		purePolicy = adapter.options.PureAirplanePolicy(deviceID)
	}
	return RadioSnapshot{
		CellularDataEnabled: len(active) > 0,
		OperatingMode:       mode,
		PureAirplanePolicy:  purePolicy,
	}, nil
}

func (adapter *EC20Adapter) StopCellularData(
	ctx context.Context,
	deviceID string,
) error {
	active, err := adapter.readActiveCIDs(ctx, deviceID)
	if err != nil {
		return err
	}
	for _, cid := range active {
		if _, err := adapter.execute(
			ctx,
			deviceID,
			fmt.Sprintf("AT+CGACT=0,%d", cid),
		); err != nil {
			return fmt.Errorf("deactivate EC20 PDP context %d: %w", cid, err)
		}
	}
	remaining, err := adapter.readActiveCIDs(ctx, deviceID)
	if err != nil {
		return err
	}
	if len(remaining) != 0 {
		return errors.New("vocat: EC20 cellular data remained active")
	}
	return nil
}

func (adapter *EC20Adapter) EnterVoWiFiRFOff(
	ctx context.Context,
	deviceID string,
) error {
	mode, err := adapter.readOperatingMode(ctx, deviceID)
	if err != nil {
		return err
	}
	if mode != 4 {
		if _, err := adapter.execute(ctx, deviceID, "AT+CFUN=4"); err != nil {
			return fmt.Errorf("enter EC20 RF-off mode: %w", err)
		}
	}
	mode, err = adapter.readOperatingMode(ctx, deviceID)
	if err != nil {
		return err
	}
	if mode != 4 {
		return fmt.Errorf("vocat: EC20 reported CFUN=%d after RF-off request", mode)
	}
	return nil
}

func (adapter *EC20Adapter) Restore(
	ctx context.Context,
	deviceID string,
	snapshot RadioSnapshot,
) error {
	if snapshot.OperatingMode < 0 {
		return errors.New("vocat: invalid EC20 radio snapshot")
	}
	currentMode, err := adapter.readOperatingMode(ctx, deviceID)
	if err != nil {
		return err
	}
	if currentMode != snapshot.OperatingMode {
		if _, err := adapter.execute(
			ctx,
			deviceID,
			fmt.Sprintf("AT+CFUN=%d", snapshot.OperatingMode),
		); err != nil {
			return fmt.Errorf("restore EC20 operating mode: %w", err)
		}
	}
	currentMode, err = adapter.readOperatingMode(ctx, deviceID)
	if err != nil {
		return err
	}
	if currentMode != snapshot.OperatingMode {
		return fmt.Errorf(
			"vocat: EC20 restore reported CFUN=%d, expected %d",
			currentMode,
			snapshot.OperatingMode,
		)
	}

	adapter.mu.Lock()
	checkpoint, found := adapter.checkpoints[deviceID]
	adapter.mu.Unlock()
	if !found {
		if snapshot.CellularDataEnabled {
			return errors.New("vocat: EC20 PDP restore evidence is unavailable")
		}
		checkpoint.activeCIDs = nil
	}
	if (snapshot.OperatingMode == 0 || snapshot.OperatingMode == 4) &&
		len(checkpoint.activeCIDs) > 0 {
		return errors.New("vocat: EC20 snapshot has active data in RF-off mode")
	}
	desiredCIDs := checkpoint.activeCIDs
	if !adapter.options.RestoreCellularData {
		desiredCIDs = nil
	}
	if err := adapter.reconcileActiveCIDs(
		ctx,
		deviceID,
		desiredCIDs,
	); err != nil {
		return err
	}
	adapter.mu.Lock()
	delete(adapter.checkpoints, deviceID)
	adapter.mu.Unlock()
	return nil
}

func (adapter *EC20Adapter) reconcileActiveCIDs(
	ctx context.Context,
	deviceID string,
	desired []int,
) error {
	current, err := adapter.readActiveCIDs(ctx, deviceID)
	if err != nil {
		return err
	}
	desiredSet := integerSet(desired)
	currentSet := integerSet(current)
	for _, cid := range current {
		if _, wanted := desiredSet[cid]; wanted {
			continue
		}
		if _, err := adapter.execute(
			ctx,
			deviceID,
			fmt.Sprintf("AT+CGACT=0,%d", cid),
		); err != nil {
			return fmt.Errorf("restore EC20 PDP context %d: %w", cid, err)
		}
	}
	for _, cid := range desired {
		if _, active := currentSet[cid]; active {
			continue
		}
		if _, err := adapter.execute(
			ctx,
			deviceID,
			fmt.Sprintf("AT+CGACT=1,%d", cid),
		); err != nil {
			return fmt.Errorf("restore EC20 PDP context %d: %w", cid, err)
		}
	}
	verified, err := adapter.readActiveCIDs(ctx, deviceID)
	if err != nil {
		return err
	}
	if !sameIntegers(verified, desired) {
		return fmt.Errorf(
			"vocat: EC20 PDP restore mismatch: active contexts %v",
			verified,
		)
	}
	return nil
}

func (adapter *EC20Adapter) readOperatingMode(
	ctx context.Context,
	deviceID string,
) (int, error) {
	response, err := adapter.execute(ctx, deviceID, "AT+CFUN?")
	if err != nil {
		return 0, fmt.Errorf("read EC20 operating mode: %w", err)
	}
	value := valueAfterATPrefix(response, "+CFUN:")
	fields := parseCSV(value)
	if len(fields) == 0 {
		return 0, errors.New("vocat: EC20 returned no CFUN mode")
	}
	mode, err := strconv.Atoi(fields[0])
	if err != nil || mode < 0 {
		return 0, errors.New("vocat: EC20 returned an invalid CFUN mode")
	}
	return mode, nil
}

func (adapter *EC20Adapter) readActiveCIDs(
	ctx context.Context,
	deviceID string,
) ([]int, error) {
	response, err := adapter.execute(ctx, deviceID, "AT+CGACT?")
	if err != nil {
		return nil, fmt.Errorf("read EC20 PDP contexts: %w", err)
	}
	var active []int
	for _, line := range response.Lines {
		line = strings.TrimSpace(line)
		if !strings.HasPrefix(strings.ToUpper(line), "+CGACT:") {
			continue
		}
		fields := parseCSV(strings.TrimSpace(line[len("+CGACT:"):]))
		if len(fields) < 2 {
			return nil, errors.New("vocat: EC20 returned an invalid CGACT record")
		}
		cid, cidErr := strconv.Atoi(fields[0])
		state, stateErr := strconv.Atoi(fields[1])
		if cidErr != nil || stateErr != nil || cid <= 0 || (state != 0 && state != 1) {
			return nil, errors.New("vocat: EC20 returned an invalid CGACT record")
		}
		if state == 1 {
			active = append(active, cid)
		}
	}
	sort.Ints(active)
	return uniqueIntegers(active), nil
}

func (adapter *EC20Adapter) bindingFor(
	identity SIMIdentity,
) (ec20SIMBinding, error) {
	iccid := strings.TrimSpace(identity.ICCID)
	adapter.mu.Lock()
	binding, ok := adapter.bindings[iccid]
	adapter.mu.Unlock()
	if !ok || iccid == "" {
		return ec20SIMBinding{}, errors.New("vocat: EC20 SIM identity is not bound to a device")
	}
	if strings.TrimSpace(identity.IMSI) != binding.imsi {
		return ec20SIMBinding{}, ErrEC20IdentityChanged
	}
	return binding, nil
}

func (adapter *EC20Adapter) verifyLiveICCID(
	ctx context.Context,
	binding ec20SIMBinding,
) error {
	iccid, err := adapter.readICCID(ctx, binding.deviceID)
	if err != nil {
		return err
	}
	if iccid != binding.iccid {
		return ErrEC20IdentityChanged
	}
	return nil
}

func (adapter *EC20Adapter) discoverAKAApplication(
	ctx context.Context,
	deviceID string,
) (aid string, application string, err error) {
	response, cuadErr := adapter.execute(ctx, deviceID, "AT+CUAD")
	if cuadErr == nil {
		data, parseErr := parseCUADData(response)
		if parseErr == nil {
			aids := collectApplicationAIDs(data)
			for _, candidate := range aids {
				if strings.HasPrefix(candidate, usimAIDPrefix) {
					return candidate, "USIM", nil
				}
			}
			for _, candidate := range aids {
				if strings.HasPrefix(candidate, isimAIDPrefix) {
					return candidate, "ISIM", nil
				}
			}
			if len(aids) > 0 {
				return "", "", ErrEC20ApplicationAbsent
			}
		}
	}

	// AT+CUAD is optional on older EC20 firmware. CCHO still provides a
	// standards-based, evidence-bearing probe of the assigned USIM AID.
	return usimAIDPrefix, "USIM", nil
}

func (adapter *EC20Adapter) openLogicalChannel(
	ctx context.Context,
	deviceID string,
	aid string,
) (int, error) {
	response, err := adapter.execute(
		ctx,
		deviceID,
		fmt.Sprintf(`AT+CCHO="%s"`, aid),
	)
	if err != nil {
		return 0, fmt.Errorf("%w: open application", ErrEC20ApplicationAbsent)
	}
	value := valueAfterATPrefix(response, "+CCHO:")
	if value == "" {
		for _, line := range response.Lines {
			line = strings.TrimSpace(line)
			if _, parseErr := strconv.Atoi(line); parseErr == nil {
				value = line
				break
			}
		}
	}
	channel, err := strconv.Atoi(strings.TrimSpace(value))
	if err != nil || channel < 1 || channel > 19 {
		return 0, errors.New("vocat: EC20 returned an invalid logical channel")
	}
	return channel, nil
}

func (adapter *EC20Adapter) closeLogicalChannel(
	ctx context.Context,
	deviceID string,
	channel int,
) error {
	if _, err := adapter.execute(
		ctx,
		deviceID,
		fmt.Sprintf("AT+CCHC=%d", channel),
	); err != nil {
		return fmt.Errorf("close EC20 logical channel: %w", err)
	}
	return nil
}

func (adapter *EC20Adapter) closeLogicalChannelWithCleanup(
	deviceID string,
	channel int,
) error {
	ctx, cancel := context.WithTimeout(
		context.Background(),
		channelCleanupTimeout,
	)
	defer cancel()
	return adapter.closeLogicalChannel(ctx, deviceID, channel)
}

func (adapter *EC20Adapter) selectBasicApplication(
	ctx context.Context,
	deviceID string,
	aid string,
) error {
	aidBytes, err := hex.DecodeString(aid)
	if err != nil || len(aidBytes) == 0 || len(aidBytes) > 255 {
		return errors.New("vocat: invalid USIM application identifier")
	}
	// SELECT by DF name, request the first/only matching application and FCP.
	apdu := []byte{0x00, 0xa4, 0x04, 0x04, byte(len(aidBytes))}
	apdu = append(apdu, aidBytes...)
	raw, err := adapter.transmitBasicAPDU(ctx, deviceID, apdu, false)
	if err != nil {
		return fmt.Errorf("select EC20 basic-channel application: %w", err)
	}
	_, status, err := splitAPDUStatus(raw)
	if err != nil {
		return err
	}
	if status != 0x9000 {
		return fmt.Errorf(
			"vocat: EC20 basic-channel SELECT returned %04X",
			status,
		)
	}
	return nil
}

func (adapter *EC20Adapter) transmitBasicAPDU(
	ctx context.Context,
	deviceID string,
	apdu []byte,
	sensitive bool,
) ([]byte, error) {
	if len(apdu) == 0 || len(apdu) > 261 {
		return nil, errors.New("vocat: invalid EC20 APDU length")
	}
	var collected []byte
	current := append([]byte(nil), apdu...)
	for exchange := 0; exchange < 4; exchange++ {
		command := fmt.Sprintf(
			`AT+CSIM=%d,"%s"`,
			len(current)*2,
			strings.ToUpper(hex.EncodeToString(current)),
		)
		var response modem.Response
		var err error
		if sensitive {
			response, err = adapter.executeSensitive(ctx, deviceID, command)
		} else {
			response, err = adapter.execute(ctx, deviceID, command)
		}
		if err != nil {
			return nil, errors.New("vocat: EC20 CSIM exchange failed")
		}
		raw, err := parseCSIMData(response)
		if err != nil {
			return nil, err
		}
		body, status, err := splitAPDUStatus(raw)
		if err != nil {
			return nil, err
		}
		collected = append(collected, body...)
		sw1 := byte(status >> 8)
		if sw1 != 0x61 && sw1 != 0x9f {
			collected = append(collected, byte(status>>8), byte(status))
			return collected, nil
		}
		// GET RESPONSE on the same basic channel. Le=0 means 256 bytes.
		current = []byte{0x00, 0xc0, 0x00, 0x00, byte(status)}
	}
	return nil, errors.New("vocat: EC20 APDU response chaining exceeded limit")
}

func (adapter *EC20Adapter) transmitLogicalAPDU(
	ctx context.Context,
	deviceID string,
	channel int,
	apdu []byte,
	sensitive bool,
) ([]byte, error) {
	if channel < 1 || channel > 19 || len(apdu) == 0 || len(apdu) > 261 {
		return nil, ErrEC20AKACommand
	}
	var collected []byte
	current := append([]byte(nil), apdu...)
	for exchange := 0; exchange < 4; exchange++ {
		command := fmt.Sprintf(
			`AT+CGLA=%d,%d,"%s"`,
			channel,
			len(current)*2,
			strings.ToUpper(hex.EncodeToString(current)),
		)
		var response modem.Response
		var err error
		if sensitive {
			response, err = adapter.executeSensitive(ctx, deviceID, command)
		} else {
			response, err = adapter.execute(ctx, deviceID, command)
		}
		if err != nil {
			return nil, errors.Join(ErrEC20AKACommand, err)
		}
		raw, err := parseCGLAData(response)
		if err != nil {
			return nil, fmt.Errorf("%w: %v", ErrEC20AKAResponse, err)
		}
		body, status, err := splitAPDUStatus(raw)
		if err != nil {
			return nil, fmt.Errorf("%w: %v", ErrEC20AKAResponse, err)
		}
		collected = append(collected, body...)
		sw1 := byte(status >> 8)
		if sw1 != 0x61 && sw1 != 0x9f {
			collected = append(collected, byte(status>>8), byte(status))
			return collected, nil
		}
		// CGLA carries the logical-channel identifier separately, so GET
		// RESPONSE retains the same interindustry CLA used by AUTHENTICATE.
		// Le=0 means 256 bytes when SW2 is zero.
		current = []byte{0x00, 0xc0, 0x00, 0x00, byte(status)}
	}
	return nil, fmt.Errorf(
		"%w: logical-channel response chaining exceeded limit",
		ErrEC20AKAResponse,
	)
}

func splitAPDUStatus(raw []byte) ([]byte, uint16, error) {
	if len(raw) < 2 {
		return nil, 0, errors.New("vocat: EC20 APDU has no status word")
	}
	status := uint16(raw[len(raw)-2])<<8 | uint16(raw[len(raw)-1])
	return raw[:len(raw)-2], status, nil
}

func (adapter *EC20Adapter) execute(
	ctx context.Context,
	deviceID string,
	command string,
) (modem.Response, error) {
	return adapter.executor.ExecuteAT(ctx, deviceID, command)
}

func (adapter *EC20Adapter) executeSensitive(
	ctx context.Context,
	deviceID string,
	command string,
) (modem.Response, error) {
	if executor, ok := adapter.executor.(EC20SensitiveATExecutor); ok {
		return executor.ExecuteSensitiveAT(ctx, deviceID, command)
	}
	return adapter.executor.ExecuteAT(ctx, deviceID, command)
}

func parseCRSMData(response modem.Response) ([]byte, error) {
	value := valueAfterATPrefix(response, "+CRSM:")
	fields := parseCSV(value)
	if len(fields) < 2 {
		return nil, errors.New("invalid CRSM response")
	}
	sw1, err1 := strconv.Atoi(fields[0])
	sw2, err2 := strconv.Atoi(fields[1])
	if err1 != nil || err2 != nil {
		return nil, errors.New("invalid CRSM status")
	}
	if sw1 != 0x90 && sw1 != 0x91 && sw1 != 0x9f {
		return nil, fmt.Errorf("CRSM status %02X%02X", sw1, sw2)
	}
	if len(fields) < 3 {
		return nil, errors.New("CRSM response has no data")
	}
	data, err := hex.DecodeString(strings.Trim(fields[2], `"`))
	if err != nil {
		return nil, errors.New("CRSM response data is not hexadecimal")
	}
	return data, nil
}

func parseCUADData(response modem.Response) ([]byte, error) {
	fields := parseCSV(valueAfterATPrefix(response, "+CUAD:"))
	if len(fields) == 0 {
		return nil, errors.New("CUAD response has no data")
	}
	value := fields[len(fields)-1]
	data, err := hex.DecodeString(strings.Trim(value, `"`))
	if err != nil || len(data) == 0 {
		return nil, errors.New("CUAD response data is invalid")
	}
	if len(data) >= 2 && data[len(data)-2] == 0x90 && data[len(data)-1] == 0x00 {
		data = data[:len(data)-2]
	}
	return data, nil
}

func collectApplicationAIDs(data []byte) []string {
	var result []string
	var walk func([]byte)
	walk = func(value []byte) {
		for len(value) > 0 {
			tag, constructed, body, consumed, err := decodeBERTLV(value)
			if err != nil || consumed == 0 {
				return
			}
			if len(tag) == 1 && tag[0] == 0x4f && len(body) > 0 {
				result = append(result, strings.ToUpper(hex.EncodeToString(body)))
			}
			if constructed {
				walk(body)
			}
			value = value[consumed:]
		}
	}
	walk(data)
	return result
}

func decodeBERTLV(data []byte) (
	tag []byte,
	constructed bool,
	value []byte,
	consumed int,
	err error,
) {
	if len(data) < 2 {
		return nil, false, nil, 0, errors.New("short BER-TLV")
	}
	tagLength := 1
	if data[0]&0x1f == 0x1f {
		for {
			if tagLength >= len(data) {
				return nil, false, nil, 0, errors.New("short BER tag")
			}
			last := data[tagLength]&0x80 == 0
			tagLength++
			if last {
				break
			}
			if tagLength > 4 {
				return nil, false, nil, 0, errors.New("oversized BER tag")
			}
		}
	}
	body, bodyConsumed, err := decodeBERTLVValue(data[tagLength:])
	if err != nil {
		return nil, false, nil, 0, err
	}
	return data[:tagLength],
		data[0]&0x20 != 0,
		body,
		tagLength + bodyConsumed,
		nil
}

func decodeBERTLVValue(data []byte) ([]byte, int, error) {
	if len(data) == 0 {
		return nil, 0, errors.New("missing BER length")
	}
	length := int(data[0])
	lengthOctets := 1
	if data[0]&0x80 != 0 {
		count := int(data[0] & 0x7f)
		if count == 0 || count > 2 || len(data) < 1+count {
			return nil, 0, errors.New("invalid BER length")
		}
		length = 0
		for _, octet := range data[1 : 1+count] {
			length = length<<8 | int(octet)
		}
		lengthOctets += count
	}
	if length > len(data)-lengthOctets {
		return nil, 0, errors.New("short BER value")
	}
	return data[lengthOctets : lengthOctets+length],
		lengthOctets + length,
		nil
}

func parseCGLAData(response modem.Response) ([]byte, error) {
	return parseATAPDUData(response, "+CGLA:")
}

func parseCSIMData(response modem.Response) ([]byte, error) {
	return parseATAPDUData(response, "+CSIM:")
}

func parseATAPDUData(response modem.Response, prefix string) ([]byte, error) {
	fields := parseCSV(valueAfterATPrefix(response, prefix))
	if len(fields) < 2 {
		return nil, errors.New("EC20 response has no APDU")
	}
	declared, err := strconv.Atoi(fields[0])
	if err != nil || declared < 0 {
		return nil, errors.New("EC20 response has invalid APDU length")
	}
	encoded := strings.Trim(fields[1], `" `)
	data, err := hex.DecodeString(encoded)
	if err != nil {
		return nil, errors.New("EC20 APDU response is not hexadecimal")
	}
	if declared != len(encoded) && declared != len(data) {
		return nil, errors.New("EC20 APDU response length mismatch")
	}
	return data, nil
}

func responseContainsValue(
	response modem.Response,
	prefix string,
	expected string,
) bool {
	return strings.EqualFold(
		strings.Trim(strings.TrimSpace(valueAfterATPrefix(response, prefix)), `"`),
		expected,
	)
}

func valueAfterATPrefix(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 digitIdentifier(
	response modem.Response,
	prefixes []string,
	minimum int,
	maximum int,
) string {
	for _, prefix := range prefixes {
		value := strings.Trim(valueAfterATPrefix(response, prefix), `" `)
		if validDigits(value, minimum, maximum) {
			return value
		}
	}
	for _, line := range response.Lines {
		value := strings.TrimSpace(line)
		if validDigits(value, minimum, maximum) {
			return value
		}
	}
	return ""
}

func iccidIdentifier(
	response modem.Response,
	prefixes []string,
	minimum int,
	maximum int,
) string {
	normalize := func(value string) string {
		value = strings.Trim(value, `" `)
		value = strings.TrimRight(value, "Ff")
		if validDigits(value, minimum, maximum) {
			return value
		}
		return ""
	}
	for _, prefix := range prefixes {
		if value := normalize(valueAfterATPrefix(response, prefix)); value != "" {
			return value
		}
	}
	for _, line := range response.Lines {
		if value := normalize(strings.TrimSpace(line)); value != "" {
			return value
		}
	}
	return ""
}

func validDigits(value string, minimum int, maximum int) bool {
	if len(value) < minimum || len(value) > maximum {
		return false
	}
	for _, character := range value {
		if character < '0' || character > '9' {
			return false
		}
	}
	return true
}

func parseCSV(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 integerSet(values []int) map[int]struct{} {
	result := make(map[int]struct{}, len(values))
	for _, value := range values {
		result[value] = struct{}{}
	}
	return result
}

func uniqueIntegers(values []int) []int {
	if len(values) < 2 {
		return values
	}
	result := values[:1]
	for _, value := range values[1:] {
		if value != result[len(result)-1] {
			result = append(result, value)
		}
	}
	return result
}

func sameIntegers(left, right []int) bool {
	left = append([]int(nil), left...)
	right = append([]int(nil), right...)
	sort.Ints(left)
	sort.Ints(right)
	left = uniqueIntegers(left)
	right = uniqueIntegers(right)
	if len(left) != len(right) {
		return false
	}
	for index := range left {
		if left[index] != right[index] {
			return false
		}
	}
	return true
}
