package ike

import (
	"context"
	"crypto/hmac"
	"crypto/sha1"
	"crypto/subtle"
	"encoding/binary"
	"errors"
	"fmt"
	"math/big"
	"strings"

	"vocat/internal/vowifi"
)

const (
	eapRequest  = 1
	eapResponse = 2
	eapSuccess  = 3
	eapFailure  = 4

	eapTypeIdentity = 1
	eapTypeAKA      = 23

	akaSubtypeChallenge    = 1
	akaSubtypeAuthReject   = 2
	akaSubtypeSyncFailure  = 4
	akaSubtypeIdentity     = 5
	akaSubtypeNotification = 12
	akaSubtypeReauth       = 13
	akaSubtypeClientError  = 14

	akaAttrRAND           = 1
	akaAttrAUTN           = 2
	akaAttrRES            = 3
	akaAttrAUTS           = 4
	akaAttrPermanentIDReq = 10
	akaAttrMAC            = 11
	akaAttrNotification   = 12
	akaAttrAnyIDReq       = 13
	akaAttrIdentity       = 14
	akaAttrFullAuthIDReq  = 17
	akaAttrClientError    = 22
	akaAttrResultInd      = 135
)

var errAKAProvider = errors.New("ike: SIM AKA provider failure")

type eapPacket struct {
	Code       uint8
	Identifier uint8
	Type       uint8
	Data       []byte
}

func parseEAPPacket(encoded []byte) (eapPacket, error) {
	if len(encoded) < 4 {
		return eapPacket{}, errors.New("ike: truncated EAP header")
	}
	length := int(binary.BigEndian.Uint16(encoded[2:4]))
	if length != len(encoded) {
		return eapPacket{}, fmt.Errorf("ike: EAP length %d does not match payload length %d", length, len(encoded))
	}
	packet := eapPacket{Code: encoded[0], Identifier: encoded[1]}
	switch packet.Code {
	case eapRequest, eapResponse:
		if len(encoded) < 5 {
			return eapPacket{}, errors.New("ike: typed EAP packet is truncated")
		}
		packet.Type = encoded[4]
		packet.Data = append([]byte(nil), encoded[5:]...)
	case eapSuccess, eapFailure:
		if len(encoded) != 4 {
			return eapPacket{}, errors.New("ike: EAP success/failure has trailing data")
		}
	default:
		return eapPacket{}, fmt.Errorf("ike: unsupported EAP code %d", packet.Code)
	}
	return packet, nil
}

func marshalEAPPacket(packet eapPacket) ([]byte, error) {
	length := 4
	if packet.Code == eapRequest || packet.Code == eapResponse {
		if packet.Type == 0 {
			return nil, errors.New("ike: typed EAP packet has no type")
		}
		length += 1 + len(packet.Data)
	} else if len(packet.Data) != 0 || packet.Type != 0 {
		return nil, errors.New("ike: EAP success/failure cannot carry type data")
	}
	if length > 65535 {
		return nil, errors.New("ike: EAP packet exceeds 65535 bytes")
	}
	encoded := make([]byte, length)
	encoded[0] = packet.Code
	encoded[1] = packet.Identifier
	binary.BigEndian.PutUint16(encoded[2:4], uint16(length))
	if length > 4 {
		encoded[4] = packet.Type
		copy(encoded[5:], packet.Data)
	}
	return encoded, nil
}

type akaAttribute struct {
	Type   uint8
	Raw    []byte
	Offset int
}

func parseAKAAttributes(encoded []byte) ([]akaAttribute, error) {
	var result []akaAttribute
	for offset := 0; offset < len(encoded); {
		if len(result) >= 64 || offset+2 > len(encoded) {
			return nil, errors.New("ike: malformed EAP-AKA attribute list")
		}
		length := int(encoded[offset+1]) * 4
		if length < 4 || offset+length > len(encoded) {
			return nil, fmt.Errorf("ike: EAP-AKA attribute %d has invalid length %d", encoded[offset], length)
		}
		result = append(result, akaAttribute{
			Type:   encoded[offset],
			Raw:    append([]byte(nil), encoded[offset:offset+length]...),
			Offset: offset,
		})
		offset += length
	}
	return result, nil
}

func oneAKAAttribute(attributes []akaAttribute, kind uint8) (akaAttribute, error) {
	var result akaAttribute
	count := 0
	for _, attribute := range attributes {
		if attribute.Type == kind {
			result = attribute
			count++
		}
	}
	if count != 1 {
		return akaAttribute{}, fmt.Errorf("ike: EAP-AKA expected one attribute %d, got %d", kind, count)
	}
	return result, nil
}

func marshalAKAAttribute(kind uint8, value []byte) ([]byte, error) {
	length := 2 + len(value)
	padded := (length + 3) &^ 3
	if padded/4 > 255 {
		return nil, errors.New("ike: EAP-AKA attribute is too long")
	}
	encoded := make([]byte, padded)
	encoded[0] = kind
	encoded[1] = uint8(padded / 4)
	copy(encoded[2:], value)
	return encoded, nil
}

type akaKeys struct {
	KEncr []byte
	KAut  []byte
	MSK   []byte
	EMSK  []byte
}

func deriveAKAKeys(identity, ik, ck []byte) (akaKeys, error) {
	if len(identity) == 0 {
		return akaKeys{}, errors.New("ike: EAP-AKA identity is empty")
	}
	if len(ik) != 16 || len(ck) != 16 {
		return akaKeys{}, fmt.Errorf("ike: EAP-AKA requires 16-byte IK and CK, got %d and %d", len(ik), len(ck))
	}
	material := make([]byte, 0, len(identity)+32)
	material = append(material, identity...)
	material = append(material, ik...)
	material = append(material, ck...)
	masterKey := sha1.Sum(material)
	stream := fips1862PRF(masterKey[:], 160)
	return akaKeys{
		KEncr: append([]byte(nil), stream[0:16]...),
		KAut:  append([]byte(nil), stream[16:32]...),
		MSK:   append([]byte(nil), stream[32:96]...),
		EMSK:  append([]byte(nil), stream[96:160]...),
	}, nil
}

func fips1862PRF(seed []byte, length int) []byte {
	xkey := new(big.Int).SetBytes(seed)
	modulus := new(big.Int).Lsh(big.NewInt(1), 160)
	result := make([]byte, 0, length)
	for len(result) < length {
		xval := xkey.FillBytes(make([]byte, 20))
		word := fipsSHA1G(xval)
		result = append(result, word[:]...)
		increment := new(big.Int).SetBytes(word[:])
		xkey.Add(xkey, increment)
		xkey.Add(xkey, big.NewInt(1))
		xkey.Mod(xkey, modulus)
	}
	return result[:length]
}

// fipsSHA1G is the SHA-1 compression function G(t, XVAL) from FIPS 186-2.
// Unlike ordinary SHA-1, the 160-bit XVAL is zero-filled to one compression
// block and is not followed by SHA-1 message padding.
func fipsSHA1G(xval []byte) [20]byte {
	var words [80]uint32
	var block [64]byte
	copy(block[:20], xval)
	for index := 0; index < 16; index++ {
		words[index] = binary.BigEndian.Uint32(block[index*4 : index*4+4])
	}
	for index := 16; index < 80; index++ {
		value := words[index-3] ^ words[index-8] ^ words[index-14] ^ words[index-16]
		words[index] = value<<1 | value>>31
	}
	a := uint32(0x67452301)
	b := uint32(0xEFCDAB89)
	c := uint32(0x98BADCFE)
	d := uint32(0x10325476)
	e := uint32(0xC3D2E1F0)
	initialA, initialB, initialC, initialD, initialE := a, b, c, d, e
	for index := 0; index < 80; index++ {
		var function, constant uint32
		switch {
		case index < 20:
			function = (b & c) | (^b & d)
			constant = 0x5A827999
		case index < 40:
			function = b ^ c ^ d
			constant = 0x6ED9EBA1
		case index < 60:
			function = (b & c) | (b & d) | (c & d)
			constant = 0x8F1BBCDC
		default:
			function = b ^ c ^ d
			constant = 0xCA62C1D6
		}
		rotatedA := a<<5 | a>>27
		next := rotatedA + function + e + constant + words[index]
		e = d
		d = c
		c = b<<30 | b>>2
		b = a
		a = next
	}
	values := [5]uint32{initialA + a, initialB + b, initialC + c, initialD + d, initialE + e}
	var result [20]byte
	for index, value := range values {
		binary.BigEndian.PutUint32(result[index*4:index*4+4], value)
	}
	return result
}

func permanentAKAIdentity(identity vowifi.SIMIdentity) ([]byte, error) {
	imsi := strings.TrimSpace(identity.IMSI)
	if len(imsi) < 5 || len(imsi) > 16 {
		return nil, errors.New("ike: IMSI length is invalid for EAP-AKA")
	}
	for _, digit := range imsi {
		if digit < '0' || digit > '9' {
			return nil, errors.New("ike: IMSI contains a non-digit")
		}
	}
	mcc := strings.TrimSpace(identity.HomeMCC)
	mnc := strings.TrimSpace(identity.HomeMNC)
	if len(mcc) != 3 || (len(mnc) != 2 && len(mnc) != 3) {
		return nil, errors.New("ike: explicit home MCC/MNC is required for EAP-AKA")
	}
	for len(mnc) < 3 {
		mnc = "0" + mnc
	}
	return []byte(fmt.Sprintf("0%s@nai.epc.mnc%s.mcc%s.3gppnetwork.org", imsi, mnc, mcc)), nil
}

type eapAction struct {
	Response []byte
	Success  bool
}

type akaClient struct {
	identity          []byte
	simIdentity       vowifi.SIMIdentity
	provider          vowifi.AKAProvider
	keys              akaKeys
	challengeComplete bool
	resultIndication  bool
	protectedSuccess  bool
}

func newAKAClient(identity vowifi.SIMIdentity, provider vowifi.AKAProvider) (*akaClient, error) {
	if provider == nil {
		return nil, errors.New("ike: AKA provider is required")
	}
	nai, err := permanentAKAIdentity(identity)
	if err != nil {
		return nil, err
	}
	return &akaClient{identity: nai, simIdentity: identity, provider: provider}, nil
}

func (client *akaClient) handle(ctx context.Context, encoded []byte) (eapAction, error) {
	packet, err := parseEAPPacket(encoded)
	if err != nil {
		return eapAction{}, err
	}
	switch packet.Code {
	case eapFailure:
		stage := "before the SIM AKA challenge (identity or subscription rejected)"
		if client.challengeComplete {
			stage = "after the SIM AKA response (AKA result or subscription rejected)"
		}
		return eapAction{}, fmt.Errorf("%w %s", vowifi.ErrEAPAuthenticationRejected, stage)
	case eapSuccess:
		if !client.challengeComplete {
			return eapAction{}, errors.New("ike: EAP success arrived before an authenticated AKA challenge")
		}
		if client.resultIndication && !client.protectedSuccess {
			return eapAction{}, errors.New("ike: unprotected EAP success received after AT_RESULT_IND")
		}
		return eapAction{Success: true}, nil
	case eapRequest:
	default:
		return eapAction{}, fmt.Errorf("ike: unexpected EAP code %d from responder", packet.Code)
	}
	switch packet.Type {
	case eapTypeIdentity:
		response, err := marshalEAPPacket(eapPacket{
			Code:       eapResponse,
			Identifier: packet.Identifier,
			Type:       eapTypeIdentity,
			Data:       client.identity,
		})
		return eapAction{Response: response}, err
	case eapTypeAKA:
		return client.handleAKARequest(ctx, packet)
	default:
		return eapAction{}, fmt.Errorf("ike: responder requested unsupported EAP type %d", packet.Type)
	}
}

func (client *akaClient) handleAKARequest(ctx context.Context, packet eapPacket) (eapAction, error) {
	if len(packet.Data) < 3 {
		return akaClientErrorResponse(packet.Identifier)
	}
	subtype := packet.Data[0]
	if packet.Data[1] != 0 || packet.Data[2] != 0 {
		return akaClientErrorResponse(packet.Identifier)
	}
	attributes, err := parseAKAAttributes(packet.Data[3:])
	if err != nil {
		return akaClientErrorResponse(packet.Identifier)
	}
	switch subtype {
	case akaSubtypeIdentity:
		action, err := client.respondAKAIdentity(packet.Identifier, attributes)
		if err != nil {
			return akaClientErrorResponse(packet.Identifier)
		}
		return action, nil
	case akaSubtypeChallenge:
		action, err := client.respondAKAChallenge(ctx, packet.Identifier, attributes, packet)
		if err != nil && !errors.Is(err, errAKAProvider) &&
			!errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
			return akaClientErrorResponse(packet.Identifier)
		}
		return action, err
	case akaSubtypeNotification:
		return client.respondAKANotification(packet.Identifier, attributes, packet)
	case akaSubtypeReauth:
		return akaClientErrorResponse(packet.Identifier)
	default:
		return akaClientErrorResponse(packet.Identifier)
	}
}

func (client *akaClient) respondAKAIdentity(identifier uint8, attributes []akaAttribute) (eapAction, error) {
	requests := 0
	for _, attribute := range attributes {
		switch attribute.Type {
		case akaAttrPermanentIDReq, akaAttrAnyIDReq, akaAttrFullAuthIDReq:
			if len(attribute.Raw) != 4 {
				return eapAction{}, errors.New("ike: malformed EAP-AKA identity request attribute")
			}
			requests++
		default:
			if attribute.Type < 128 {
				return eapAction{}, fmt.Errorf("ike: unsupported mandatory EAP-AKA identity attribute %d", attribute.Type)
			}
		}
	}
	if requests != 1 {
		return eapAction{}, errors.New("ike: EAP-AKA identity request must contain exactly one request attribute")
	}
	identityAttribute, err := marshalAKAAttribute(akaAttrIdentity, append([]byte{byte(len(client.identity) >> 8), byte(len(client.identity))}, client.identity...))
	if err != nil {
		return eapAction{}, err
	}
	data := append([]byte{akaSubtypeIdentity, 0, 0}, identityAttribute...)
	response, err := marshalEAPPacket(eapPacket{
		Code:       eapResponse,
		Identifier: identifier,
		Type:       eapTypeAKA,
		Data:       data,
	})
	return eapAction{Response: response}, err
}

func (client *akaClient) respondAKAChallenge(
	ctx context.Context,
	identifier uint8,
	attributes []akaAttribute,
	request eapPacket,
) (eapAction, error) {
	for _, attribute := range attributes {
		switch attribute.Type {
		case akaAttrRAND, akaAttrAUTN, akaAttrMAC, akaAttrResultInd:
		default:
			if attribute.Type < 128 {
				return eapAction{}, fmt.Errorf("ike: unknown mandatory EAP-AKA challenge attribute %d", attribute.Type)
			}
		}
	}
	randAttribute, err := oneAKAAttribute(attributes, akaAttrRAND)
	if err != nil {
		return eapAction{}, err
	}
	autnAttribute, err := oneAKAAttribute(attributes, akaAttrAUTN)
	if err != nil {
		return eapAction{}, err
	}
	macAttribute, err := oneAKAAttribute(attributes, akaAttrMAC)
	if err != nil {
		return eapAction{}, err
	}
	if len(randAttribute.Raw) != 20 || len(autnAttribute.Raw) != 20 || len(macAttribute.Raw) != 20 {
		return eapAction{}, errors.New("ike: EAP-AKA RAND, AUTN, or MAC has an invalid length")
	}
	var challenge vowifi.AKAChallenge
	copy(challenge.RAND[:], randAttribute.Raw[4:20])
	copy(challenge.AUTN[:], autnAttribute.Raw[4:20])
	result, err := client.provider.Authenticate(ctx, client.simIdentity, challenge)
	if err != nil {
		if errors.Is(err, vowifi.ErrEC20AKAMACFailure) {
			return akaAuthenticationRejectResponse(identifier)
		}
		return eapAction{}, errors.Join(errAKAProvider, fmt.Errorf("ike: SIM AKA authentication: %w", err))
	}
	if result.SynchronizationFailure {
		if len(result.AUTS) != 14 {
			return eapAction{}, errors.New("ike: SIM reported synchronization failure without a 14-byte AUTS")
		}
		autsAttribute, err := marshalAKAAttribute(akaAttrAUTS, result.AUTS)
		if err != nil {
			return eapAction{}, err
		}
		data := append([]byte{akaSubtypeSyncFailure, 0, 0}, autsAttribute...)
		response, err := marshalEAPPacket(eapPacket{Code: eapResponse, Identifier: identifier, Type: eapTypeAKA, Data: data})
		return eapAction{Response: response}, err
	}
	if len(result.RES) < 4 || len(result.RES) > 16 {
		return eapAction{}, fmt.Errorf("ike: SIM returned invalid RES length %d", len(result.RES))
	}
	keys, err := deriveAKAKeys(client.identity, result.IK, result.CK)
	if err != nil {
		return eapAction{}, err
	}
	requestBytes, err := marshalEAPPacket(request)
	if err != nil {
		return eapAction{}, err
	}
	zeroed := append([]byte(nil), requestBytes...)
	macOffset := 5 + 3 + macAttribute.Offset
	if macOffset+20 > len(zeroed) {
		return eapAction{}, errors.New("ike: EAP-AKA MAC offset is invalid")
	}
	for index := macOffset + 4; index < macOffset+20; index++ {
		zeroed[index] = 0
	}
	expectedMAC := akaMAC(keys.KAut, zeroed)
	if subtle.ConstantTimeCompare(expectedMAC, macAttribute.Raw[4:20]) != 1 {
		return eapAction{}, errors.New("ike: EAP-AKA server MAC is invalid")
	}

	resValue := make([]byte, 2+len(result.RES))
	binary.BigEndian.PutUint16(resValue[0:2], uint16(len(result.RES)*8))
	copy(resValue[2:], result.RES)
	resAttribute, err := marshalAKAAttribute(akaAttrRES, resValue)
	if err != nil {
		return eapAction{}, err
	}
	macResponse, _ := marshalAKAAttribute(akaAttrMAC, make([]byte, 18))
	responseData := append([]byte{akaSubtypeChallenge, 0, 0}, resAttribute...)
	resultIndication := false
	for _, attribute := range attributes {
		if attribute.Type == akaAttrResultInd {
			if len(attribute.Raw) != 4 {
				return eapAction{}, errors.New("ike: malformed AT_RESULT_IND")
			}
			responseData = append(responseData, attribute.Raw...)
			resultIndication = true
		}
	}
	responseData = append(responseData, macResponse...)
	responseBytes, err := marshalEAPPacket(eapPacket{
		Code:       eapResponse,
		Identifier: identifier,
		Type:       eapTypeAKA,
		Data:       responseData,
	})
	if err != nil {
		return eapAction{}, err
	}
	responseAttributes, _ := parseAKAAttributes(responseData[3:])
	responseMAC, err := oneAKAAttribute(responseAttributes, akaAttrMAC)
	if err != nil {
		return eapAction{}, err
	}
	responseMACOffset := 5 + 3 + responseMAC.Offset
	computed := akaMAC(keys.KAut, responseBytes)
	copy(responseBytes[responseMACOffset+4:responseMACOffset+20], computed)
	client.keys = keys
	client.challengeComplete = true
	client.resultIndication = resultIndication
	return eapAction{Response: responseBytes}, nil
}

func (client *akaClient) respondAKANotification(
	identifier uint8,
	attributes []akaAttribute,
	request eapPacket,
) (eapAction, error) {
	notification, err := oneAKAAttribute(attributes, akaAttrNotification)
	if err != nil {
		return eapAction{}, err
	}
	if len(notification.Raw) != 4 {
		return eapAction{}, errors.New("ike: malformed EAP-AKA notification")
	}
	code := binary.BigEndian.Uint16(notification.Raw[2:4])
	if code != 32768 {
		responseData := []byte{akaSubtypeNotification, 0, 0}
		if code&0x4000 == 0 {
			if !client.challengeComplete {
				return akaClientErrorResponse(identifier)
			}
			macAttribute, err := oneAKAAttribute(attributes, akaAttrMAC)
			if err != nil || len(macAttribute.Raw) != 20 {
				return akaClientErrorResponse(identifier)
			}
			requestBytes, err := marshalEAPPacket(request)
			if err != nil {
				return eapAction{}, err
			}
			zeroed := append([]byte(nil), requestBytes...)
			macOffset := 5 + 3 + macAttribute.Offset
			for index := macOffset + 4; index < macOffset+20; index++ {
				zeroed[index] = 0
			}
			if subtle.ConstantTimeCompare(akaMAC(client.keys.KAut, zeroed), macAttribute.Raw[4:20]) != 1 {
				return akaClientErrorResponse(identifier)
			}
			responseMAC, _ := marshalAKAAttribute(akaAttrMAC, make([]byte, 18))
			responseData = append(responseData, responseMAC...)
		}
		responseBytes, err := marshalEAPPacket(eapPacket{
			Code: eapResponse, Identifier: identifier, Type: eapTypeAKA, Data: responseData,
		})
		if err != nil {
			return eapAction{}, err
		}
		if code&0x4000 == 0 {
			responseAttributes, _ := parseAKAAttributes(responseData[3:])
			responseMAC, _ := oneAKAAttribute(responseAttributes, akaAttrMAC)
			offset := 5 + 3 + responseMAC.Offset
			copy(responseBytes[offset+4:offset+20], akaMAC(client.keys.KAut, responseBytes))
		}
		return eapAction{Response: responseBytes}, nil
	}
	if !client.challengeComplete || !client.resultIndication {
		return eapAction{}, errors.New("ike: unexpected protected EAP-AKA success notification")
	}
	macAttribute, err := oneAKAAttribute(attributes, akaAttrMAC)
	if err != nil {
		return eapAction{}, err
	}
	if len(macAttribute.Raw) != 20 {
		return eapAction{}, errors.New("ike: malformed notification AT_MAC")
	}
	requestBytes, err := marshalEAPPacket(request)
	if err != nil {
		return eapAction{}, err
	}
	zeroed := append([]byte(nil), requestBytes...)
	macOffset := 5 + 3 + macAttribute.Offset
	for index := macOffset + 4; index < macOffset+20; index++ {
		zeroed[index] = 0
	}
	if subtle.ConstantTimeCompare(akaMAC(client.keys.KAut, zeroed), macAttribute.Raw[4:20]) != 1 {
		return eapAction{}, errors.New("ike: EAP-AKA protected success MAC is invalid")
	}
	responseMAC, _ := marshalAKAAttribute(akaAttrMAC, make([]byte, 18))
	responseData := append([]byte{akaSubtypeNotification, 0, 0}, responseMAC...)
	responseBytes, err := marshalEAPPacket(eapPacket{
		Code:       eapResponse,
		Identifier: identifier,
		Type:       eapTypeAKA,
		Data:       responseData,
	})
	if err != nil {
		return eapAction{}, err
	}
	responseAttributes, _ := parseAKAAttributes(responseData[3:])
	responseMACAttribute, _ := oneAKAAttribute(responseAttributes, akaAttrMAC)
	responseOffset := 5 + 3 + responseMACAttribute.Offset
	copy(responseBytes[responseOffset+4:responseOffset+20], akaMAC(client.keys.KAut, responseBytes))
	client.protectedSuccess = true
	return eapAction{Response: responseBytes}, nil
}

func akaMAC(key, packet []byte) []byte {
	mac := hmac.New(sha1.New, key)
	_, _ = mac.Write(packet)
	return mac.Sum(nil)[:16]
}

func akaClientErrorResponse(identifier uint8) (eapAction, error) {
	attribute, err := marshalAKAAttribute(akaAttrClientError, []byte{0, 0})
	if err != nil {
		return eapAction{}, err
	}
	response, err := marshalEAPPacket(eapPacket{
		Code:       eapResponse,
		Identifier: identifier,
		Type:       eapTypeAKA,
		Data:       append([]byte{akaSubtypeClientError, 0, 0}, attribute...),
	})
	return eapAction{Response: response}, err
}

func akaAuthenticationRejectResponse(identifier uint8) (eapAction, error) {
	response, err := marshalEAPPacket(eapPacket{
		Code:       eapResponse,
		Identifier: identifier,
		Type:       eapTypeAKA,
		Data:       []byte{akaSubtypeAuthReject, 0, 0},
	})
	return eapAction{Response: response}, err
}
