package pcsc

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"sync"
)

const usimAIDPrefix = "A0000000871002"

type Service struct {
	mu      sync.Mutex
	backend Backend
}

// Session is an exclusive connection to one smart card. It is used by eUICC
// operations which must keep the same PC/SC transaction and logical channel
// alive across a sequence of APDUs.
type Session struct {
	service *Service
	card    Card
	closed  bool
}

func New() *Service {
	return &Service{backend: newNativeBackend()}
}

func NewWithBackend(backend Backend) *Service {
	return &Service{backend: backend}
}

func DeviceID(reader Reader) string {
	identity := strings.TrimSpace(reader.USBPath)
	if identity == "" {
		identity = strings.TrimSpace(reader.Name)
	}
	sum := sha256.Sum256([]byte(identity))
	return "reader-" + hex.EncodeToString(sum[:8])
}

func (service *Service) Readers(ctx context.Context) ([]Reader, error) {
	if service == nil || service.backend == nil {
		return nil, ErrUnavailable
	}
	service.mu.Lock()
	defer service.mu.Unlock()
	return service.backend.Readers(ctx)
}

// OpenSession opens one card and holds the service lock until Close. Callers
// must close the returned session; this prevents AKA/identity reads from
// interleaving with a stateful ES10 transaction.
func (service *Service) OpenSession(ctx context.Context, selector Selector) (*Session, error) {
	if service == nil || service.backend == nil {
		return nil, ErrUnavailable
	}
	if err := selector.validate(); err != nil {
		return nil, err
	}
	service.mu.Lock()
	card, err := service.backend.Open(ctx, selector)
	if err != nil {
		service.mu.Unlock()
		return nil, err
	}
	return &Session{service: service, card: card}, nil
}

// Transmit sends one raw APDU. Unlike the ordinary SIM helpers, it leaves
// 61xx continuation handling to the eUICC logical-channel implementation so
// GET RESPONSE uses the correct channel CLA.
func (session *Session) Transmit(ctx context.Context, command []byte) ([]byte, uint16, error) {
	if session == nil || session.card == nil || session.closed {
		return nil, 0, errors.New("pcsc: card session is closed")
	}
	if raw, ok := session.card.(interface {
		TransmitRaw(context.Context, []byte) ([]byte, uint16, error)
	}); ok {
		return raw.TransmitRaw(ctx, command)
	}
	return session.card.Transmit(ctx, command)
}

func (session *Session) Close() error {
	return session.close(false)
}

// CloseWithReset resets the card while releasing the PC/SC connection. eUICC
// EnableProfile requires this refresh boundary before the newly enabled USIM
// application and ICCID become visible to subsequent callers.
func (session *Session) CloseWithReset() error {
	return session.close(true)
}

func (session *Session) close(reset bool) error {
	if session == nil || session.closed {
		return nil
	}
	session.closed = true
	var err error
	if resetter, ok := session.card.(interface{ CloseWithReset() error }); reset && ok {
		err = resetter.CloseWithReset()
	} else {
		err = session.card.Close()
	}
	session.service.mu.Unlock()
	return err
}

func (service *Service) Snapshot(ctx context.Context, selector Selector, pin string) (Snapshot, error) {
	readers, err := service.Readers(ctx)
	if err != nil {
		return Snapshot{}, err
	}
	reader, ok := matchReader(readers, selector)
	if !ok {
		return Snapshot{}, ErrReaderNotFound
	}
	result := Snapshot{Reader: reader}
	if reader.DiscoveryIssue != "" {
		return result, fmt.Errorf("%w: %s", ErrUnavailable, reader.DiscoveryIssue)
	}
	if !reader.CardPresent {
		return result, ErrNoCard
	}
	identity, err := service.ReadIdentity(ctx, selector, pin)
	result.Identity = identity
	return result, err
}

func (service *Service) ReadIdentity(ctx context.Context, selector Selector, pin string) (Identity, error) {
	if service == nil || service.backend == nil {
		return Identity{}, ErrUnavailable
	}
	if err := selector.validate(); err != nil {
		return Identity{}, err
	}
	service.mu.Lock()
	defer service.mu.Unlock()
	card, err := service.backend.Open(ctx, selector)
	if err != nil {
		return Identity{}, err
	}
	defer card.Close()
	return readIdentity(ctx, card, pin)
}

func (service *Service) CheckReady(
	ctx context.Context,
	selector Selector,
	expectedICCID string,
	pin string,
) (string, error) {
	if service == nil || service.backend == nil {
		return "", ErrUnavailable
	}
	service.mu.Lock()
	defer service.mu.Unlock()
	card, err := service.backend.Open(ctx, selector)
	if err != nil {
		return "", err
	}
	defer card.Close()
	iccid, err := readICCID(ctx, card)
	if err != nil {
		return "", err
	}
	if expected := strings.TrimSpace(expectedICCID); expected != "" && !strings.EqualFold(expected, iccid) {
		return "", ErrCardChanged
	}
	aid, err := selectUSIM(ctx, card)
	if err != nil {
		return "", err
	}
	if err := verifyPIN(ctx, card, pin); err != nil {
		return "", err
	}
	return strings.ToUpper(hex.EncodeToString(aid)), nil
}

func (service *Service) Authenticate(
	ctx context.Context,
	selector Selector,
	expectedICCID string,
	pin string,
	challenge AKAChallenge,
) (AKAResult, error) {
	if service == nil || service.backend == nil {
		return AKAResult{}, ErrUnavailable
	}
	service.mu.Lock()
	defer service.mu.Unlock()
	card, err := service.backend.Open(ctx, selector)
	if err != nil {
		return AKAResult{}, err
	}
	defer card.Close()
	iccid, err := readICCID(ctx, card)
	if err != nil {
		return AKAResult{}, err
	}
	if expected := strings.TrimSpace(expectedICCID); expected != "" && !strings.EqualFold(expected, iccid) {
		return AKAResult{}, ErrCardChanged
	}
	if _, err := selectUSIM(ctx, card); err != nil {
		return AKAResult{}, err
	}
	if err := verifyPIN(ctx, card, pin); err != nil {
		return AKAResult{}, err
	}
	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)
	data, sw, err := card.Transmit(ctx, apdu)
	if err != nil {
		return AKAResult{}, errors.New("pcsc: USIM authentication transport failed")
	}
	if sw == 0x9862 {
		return AKAResult{}, ErrAKARejected
	}
	if sw != 0x9000 {
		return AKAResult{}, fmt.Errorf("pcsc: USIM authentication failed with status %04X", sw)
	}
	return parseAKAResponse(data)
}

func matchReader(readers []Reader, selector Selector) (Reader, bool) {
	path := strings.TrimSpace(selector.USBPath)
	name := strings.TrimSpace(selector.ReaderName)
	for _, reader := range readers {
		if path != "" && reader.USBPath == path {
			return reader, true
		}
	}
	for _, reader := range readers {
		if name != "" && reader.Name == name {
			return reader, true
		}
	}
	return Reader{}, false
}

func readIdentity(ctx context.Context, card Card, pin string) (Identity, error) {
	identity := Identity{PINTries: -1}
	iccid, err := readICCID(ctx, card)
	if err != nil {
		return identity, err
	}
	identity.ICCID = iccid
	aid, err := selectUSIM(ctx, card)
	if err != nil {
		return identity, err
	}
	identity.USIMAID = append([]byte(nil), aid...)
	if err := verifyPIN(ctx, card, pin); err != nil {
		identity.PINRequired = errors.Is(err, ErrPINRequired) || errors.Is(err, ErrPINTriesLow)
		var pinErr *PINError
		if errors.As(err, &pinErr) {
			identity.PINTries = pinErr.Tries
		}
		return identity, err
	}
	if err := selectFile(ctx, card, []byte{0x6F, 0x07}); err != nil {
		return identity, fmt.Errorf("pcsc: select EF_IMSI: %w", err)
	}
	imsiData, err := readBinary(ctx, card, 9)
	if err != nil {
		return identity, fmt.Errorf("pcsc: read EF_IMSI: %w", err)
	}
	identity.IMSI, err = decodeIMSI(imsiData)
	if err != nil {
		return identity, err
	}
	if _, selectErr := selectApplication(ctx, card, aid); selectErr == nil {
		if selectErr = selectFile(ctx, card, []byte{0x6F, 0xAD}); selectErr == nil {
			if data, readErr := readBinary(ctx, card, 4); readErr == nil && len(data) >= 4 {
				length := int(data[3] & 0x0f)
				if length == 2 || length == 3 {
					identity.MNCLength = length
				}
			}
		}
	}
	if _, selectErr := selectApplication(ctx, card, aid); selectErr == nil {
		identity.SPN = readSPN(ctx, card)
	}
	if _, selectErr := selectApplication(ctx, card, aid); selectErr == nil {
		identity.SMSC = readSMSC(ctx, card)
	}
	return identity, nil
}

func readICCID(ctx context.Context, card Card) (string, error) {
	if err := selectMF(ctx, card); err != nil {
		return "", err
	}
	if err := selectFile(ctx, card, []byte{0x2F, 0xE2}); err != nil {
		return "", fmt.Errorf("pcsc: select EF_ICCID: %w", err)
	}
	data, err := readBinary(ctx, card, 10)
	if err != nil {
		return "", fmt.Errorf("pcsc: read EF_ICCID: %w", err)
	}
	value := decodeSwappedBCD(data, false)
	if len(value) < 18 || len(value) > 22 {
		return "", errors.New("pcsc: card returned an invalid ICCID")
	}
	return value, nil
}

func selectUSIM(ctx context.Context, card Card) ([]byte, error) {
	if err := selectMF(ctx, card); err != nil {
		return nil, err
	}
	if err := selectFile(ctx, card, []byte{0x2F, 0x00}); err != nil {
		return nil, fmt.Errorf("pcsc: select EF_DIR: %w", err)
	}
	var usimAID []byte
	for record := 1; record <= 32; record++ {
		data, sw, err := card.Transmit(ctx, []byte{0x00, 0xB2, byte(record), 0x04, 0x00})
		if err != nil {
			return nil, err
		}
		if sw == 0x6A83 || sw == 0x9402 {
			break
		}
		if sw != 0x9000 {
			continue
		}
		aid := findTLV(data, 0x4F)
		if len(aid) == 0 {
			continue
		}
		if strings.HasPrefix(strings.ToUpper(hex.EncodeToString(aid)), usimAIDPrefix) {
			usimAID = append([]byte(nil), aid...)
			break
		}
	}
	if len(usimAID) == 0 {
		return nil, ErrUSIMUnavailable
	}
	if _, err := selectApplication(ctx, card, usimAID); err != nil {
		return nil, err
	}
	return usimAID, nil
}

func selectMF(ctx context.Context, card Card) error {
	_, sw, err := card.Transmit(ctx, []byte{0x00, 0xA4, 0x00, 0x04, 0x02, 0x3F, 0x00, 0x00})
	return requireStatus("select MF", sw, err)
}

func selectFile(ctx context.Context, card Card, fileID []byte) error {
	if len(fileID) != 2 {
		return errors.New("pcsc: invalid file identifier")
	}
	apdu := []byte{0x00, 0xA4, 0x00, 0x04, 0x02, fileID[0], fileID[1], 0x00}
	_, sw, err := card.Transmit(ctx, apdu)
	return requireStatus("select file", sw, err)
}

func selectApplication(ctx context.Context, card Card, aid []byte) ([]byte, error) {
	if len(aid) == 0 || len(aid) > 32 {
		return nil, errors.New("pcsc: invalid USIM AID")
	}
	apdu := []byte{0x00, 0xA4, 0x04, 0x04, byte(len(aid))}
	apdu = append(apdu, aid...)
	apdu = append(apdu, 0x00)
	data, sw, err := card.Transmit(ctx, apdu)
	if err := requireStatus("select USIM application", sw, err); err != nil {
		return nil, err
	}
	return data, nil
}

func readBinary(ctx context.Context, card Card, length int) ([]byte, error) {
	if length <= 0 || length > 256 {
		return nil, errors.New("pcsc: invalid binary read length")
	}
	le := byte(length)
	if length == 256 {
		le = 0
	}
	data, sw, err := card.Transmit(ctx, []byte{0x00, 0xB0, 0x00, 0x00, le})
	if err := requireStatus("read binary", sw, err); err != nil {
		return nil, err
	}
	return data, nil
}

func verifyPIN(ctx context.Context, card Card, pin string) error {
	pin = strings.TrimSpace(pin)
	if pin == "" {
		return nil
	}
	if len(pin) < 4 || len(pin) > 8 || !decimalDigits(pin) {
		return errors.New("pcsc: SIM PIN must contain 4 to 8 digits")
	}
	_, sw, err := card.Transmit(ctx, []byte{0x00, 0x20, 0x00, 0x01, 0x00})
	if err != nil {
		return errors.New("pcsc: SIM PIN status check failed")
	}
	if sw == 0x9000 {
		return nil
	}
	tries := -1
	if sw&0xFFF0 == 0x63C0 {
		tries = int(sw & 0x000F)
		if tries <= 2 {
			return &PINError{Kind: ErrPINTriesLow, Tries: tries}
		}
	}
	body := bytes.Repeat([]byte{0xFF}, 8)
	copy(body, []byte(pin))
	apdu := append([]byte{0x00, 0x20, 0x00, 0x01, 0x08}, body...)
	_, sw, err = card.Transmit(ctx, apdu)
	if err != nil {
		return errors.New("pcsc: SIM PIN verification transport failed")
	}
	if sw == 0x9000 {
		return nil
	}
	if sw&0xFFF0 == 0x63C0 {
		return &PINError{Kind: ErrPINRejected, Tries: int(sw & 0x000F)}
	}
	return ErrPINRejected
}

func requireStatus(operation string, sw uint16, err error) error {
	if err != nil {
		return fmt.Errorf("pcsc: %s transport failed", operation)
	}
	if sw == 0x9000 {
		return nil
	}
	if sw == 0x6982 || sw == 0x9804 {
		return &PINError{Kind: ErrPINRequired, Tries: -1}
	}
	return fmt.Errorf("pcsc: %s failed with status %04X", operation, sw)
}

func decodeSwappedBCD(value []byte, dropFirstNibble bool) string {
	var result strings.Builder
	for _, octet := range value {
		for _, nibble := range []byte{octet & 0x0F, octet >> 4} {
			if dropFirstNibble {
				dropFirstNibble = false
				continue
			}
			if nibble == 0x0F {
				return result.String()
			}
			if nibble > 9 {
				return ""
			}
			result.WriteByte('0' + nibble)
		}
	}
	return result.String()
}

func decodeIMSI(data []byte) (string, error) {
	if len(data) < 2 {
		return "", errors.New("pcsc: EF_IMSI is too short")
	}
	length := int(data[0])
	if length <= 0 || length > len(data)-1 {
		return "", errors.New("pcsc: EF_IMSI has an invalid length")
	}
	value := decodeSwappedBCD(data[1:1+length], true)
	if len(value) < 10 || len(value) > 18 || !decimalDigits(value) {
		return "", errors.New("pcsc: card returned an invalid IMSI")
	}
	return value, nil
}

func decimalDigits(value string) bool {
	if value == "" {
		return false
	}
	for _, character := range value {
		if character < '0' || character > '9' {
			return false
		}
	}
	return true
}

func findTLV(data []byte, wanted byte) []byte {
	for len(data) >= 2 {
		tag := data[0]
		data = data[1:]
		length, consumed, ok := decodeTLVLength(data)
		if !ok || consumed+length > len(data) {
			return nil
		}
		value := data[consumed : consumed+length]
		if tag == wanted {
			return append([]byte(nil), value...)
		}
		if tag&0x20 != 0 {
			if nested := findTLV(value, wanted); len(nested) > 0 {
				return nested
			}
		}
		data = data[consumed+length:]
	}
	return nil
}

func decodeTLVLength(data []byte) (length, consumed int, ok bool) {
	if len(data) == 0 {
		return 0, 0, false
	}
	if data[0]&0x80 == 0 {
		return int(data[0]), 1, true
	}
	count := int(data[0] & 0x7F)
	if count < 1 || count > 2 || len(data) < 1+count {
		return 0, 0, false
	}
	length = 0
	for _, octet := range data[1 : 1+count] {
		length = length<<8 | int(octet)
	}
	return length, 1 + count, true
}

func parseAKAResponse(data []byte) (AKAResult, error) {
	if len(data) < 2 {
		return AKAResult{}, errors.New("pcsc: USIM returned a short AKA response")
	}
	switch data[0] {
	case 0xDB:
		res, rest, ok := takeLV(data[1:])
		if !ok || len(res) < 4 || len(res) > 16 {
			return AKAResult{}, errors.New("pcsc: USIM returned an invalid AKA RES")
		}
		ck, rest, ok := takeLV(rest)
		if !ok || len(ck) != 16 {
			return AKAResult{}, errors.New("pcsc: USIM returned an invalid AKA CK")
		}
		ik, rest, ok := takeLV(rest)
		if !ok || len(ik) != 16 {
			return AKAResult{}, errors.New("pcsc: USIM returned an invalid AKA IK")
		}
		if len(rest) > 0 {
			kc, tail, valid := takeLV(rest)
			if !valid || len(kc) != 8 || len(tail) != 0 {
				return AKAResult{}, errors.New("pcsc: USIM returned invalid trailing AKA material")
			}
		}
		return AKAResult{RES: append([]byte(nil), res...), CK: append([]byte(nil), ck...), IK: append([]byte(nil), ik...)}, nil
	case 0xDC:
		auts, tail, ok := takeLV(data[1:])
		if !ok || len(auts) != 14 || len(tail) != 0 {
			return AKAResult{}, errors.New("pcsc: USIM returned invalid AKA synchronization evidence")
		}
		return AKAResult{AUTS: append([]byte(nil), auts...), SynchronizationFailure: true}, nil
	default:
		return AKAResult{}, errors.New("pcsc: USIM returned an unsupported AKA response")
	}
}

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

func readSPN(ctx context.Context, card Card) string {
	if err := selectFile(ctx, card, []byte{0x6F, 0x46}); err != nil {
		return ""
	}
	data, err := readBinary(ctx, card, 17)
	if err != nil || len(data) < 2 {
		return ""
	}
	return strings.TrimSpace(strings.TrimRight(string(data[1:]), "\x00\xFF"))
}

func readSMSC(ctx context.Context, card Card) string {
	if err := selectFile(ctx, card, []byte{0x6F, 0x42}); err != nil {
		return ""
	}
	data, sw, err := card.Transmit(ctx, []byte{0x00, 0xB2, 0x01, 0x04, 0x00})
	if err != nil || sw != 0x9000 || len(data) < 15 {
		return ""
	}
	sca := data[len(data)-15 : len(data)-3]
	if len(sca) < 2 || sca[0] < 2 || int(sca[0]) > len(sca)-1 {
		return ""
	}
	digits := decodeSwappedBCD(sca[2:1+int(sca[0])], false)
	if !decimalDigits(digits) {
		return ""
	}
	if sca[1]&0x70 == 0x10 {
		return "+" + digits
	}
	return digits
}
