package webauthn

import (
	"fmt"
	"math"
)

// cborValue is one decoded CBOR item. Only the subset needed for WebAuthn
// COSE key + attestation object parsing is handled: integers, byte strings,
// text strings, arrays, maps.
type cborValue struct {
	typ   cborType
	u     uint64      // unsigned integer value
	n     int64       // negative integer value (-1 - u)
	b     []byte      // byte string
	t     string      // text string
	items []cborValue // array items or map key-value pairs (flattened)
}

type cborType int

const (
	cborUint   cborType = 0
	cborNegInt cborType = 1
	cborBytes  cborType = 2
	cborText   cborType = 3
	cborArray  cborType = 4
	cborMap    cborType = 5
	cborSimple cborType = 7
)

func (v cborValue) Int() (int, error) {
	switch v.typ {
	case cborUint:
		return int(v.u), nil
	case cborNegInt:
		return int(v.n), nil
	default:
		return 0, fmt.Errorf("cbor: expected int, got type %d", v.typ)
	}
}

func (v cborValue) Int64() (int64, error) {
	switch v.typ {
	case cborUint:
		return int64(v.u), nil
	case cborNegInt:
		return v.n, nil
	default:
		return 0, fmt.Errorf("cbor: expected int, got type %d", v.typ)
	}
}

func (v cborValue) Bytes() ([]byte, error) {
	if v.typ != cborBytes {
		return nil, fmt.Errorf("cbor: expected bytes, got type %d", v.typ)
	}
	return v.b, nil
}

func (v cborValue) Text() (string, error) {
	if v.typ != cborText {
		return "", fmt.Errorf("cbor: expected text, got type %d", v.typ)
	}
	return v.t, nil
}

func (v cborValue) Map() (map[int64]cborValue, error) {
	if v.typ != cborMap {
		return nil, fmt.Errorf("cbor: expected map, got type %d", v.typ)
	}
	m := make(map[int64]cborValue, len(v.items)/2)
	for i := 0; i+1 < len(v.items); i += 2 {
		k, err := v.items[i].Int64()
		if err != nil {
			return nil, fmt.Errorf("cbor: map key: %w", err)
		}
		m[k] = v.items[i+1]
	}
	return m, nil
}

func (v cborValue) MapText() (map[string]cborValue, error) {
	if v.typ != cborMap {
		return nil, fmt.Errorf("cbor: expected map, got type %d", v.typ)
	}
	m := make(map[string]cborValue, len(v.items)/2)
	for i := 0; i+1 < len(v.items); i += 2 {
		k, err := v.items[i].Text()
		if err != nil {
			return nil, fmt.Errorf("cbor: map text key: %w", err)
		}
		m[k] = v.items[i+1]
	}
	return m, nil
}

func (v cborValue) At(i int) (cborValue, error) {
	if v.typ != cborArray {
		return cborValue{}, fmt.Errorf("cbor: expected array, got type %d", v.typ)
	}
	if i < 0 || i >= len(v.items) {
		return cborValue{}, fmt.Errorf("cbor: index %d out of range (len %d)", i, len(v.items))
	}
	return v.items[i], nil
}

// decodeCBOR decodes a single CBOR item from data. It handles only the subset
// needed for WebAuthn COSE key + attestation parsing.
func decodeCBOR(data []byte) (cborValue, error) {
	v, _, err := decodeItem(data)
	return v, err
}

func decodeItem(data []byte) (cborValue, int, error) {
	if len(data) == 0 {
		return cborValue{}, 0, fmt.Errorf("cbor: empty data")
	}
	ib := data[0]
	mt := ib >> 5
	ai := ib & 0x1f
	off := 1

	arg, n, err := decodeArg(data, off, ai)
	if err != nil {
		return cborValue{}, 0, err
	}
	off = n

	switch mt {
	case 0: // unsigned integer
		return cborValue{typ: cborUint, u: arg}, off, nil

	case 1: // negative integer
		return cborValue{typ: cborNegInt, n: -1 - int64(arg)}, off, nil

	case 2: // byte string
		if off+int(arg) > len(data) {
			return cborValue{}, 0, fmt.Errorf("cbor: byte string length %d exceeds data", arg)
		}
		b := make([]byte, arg)
		copy(b, data[off:off+int(arg)])
		return cborValue{typ: cborBytes, b: b}, off + int(arg), nil

	case 3: // text string
		if off+int(arg) > len(data) {
			return cborValue{}, 0, fmt.Errorf("cbor: text string length %d exceeds data", arg)
		}
		return cborValue{typ: cborText, t: string(data[off : off+int(arg)])}, off + int(arg), nil

	case 4: // array
		items := make([]cborValue, 0, arg)
		pos := off
		for i := uint64(0); i < arg; i++ {
			item, n, err := decodeItem(data[pos:])
			if err != nil {
				return cborValue{}, 0, fmt.Errorf("cbor: array item %d: %w", i, err)
			}
			items = append(items, item)
			pos += n
		}
		return cborValue{typ: cborArray, items: items}, pos, nil

	case 5: // map
		items := make([]cborValue, 0, 2*arg)
		pos := off
		for i := uint64(0); i < arg; i++ {
			k, n, err := decodeItem(data[pos:])
			if err != nil {
				return cborValue{}, 0, fmt.Errorf("cbor: map key %d: %w", i, err)
			}
			pos += n
			v, n, err := decodeItem(data[pos:])
			if err != nil {
				return cborValue{}, 0, fmt.Errorf("cbor: map value %d: %w", i, err)
			}
			pos += n
			items = append(items, k, v)
		}
		return cborValue{typ: cborMap, items: items}, pos, nil

	case 7: // simple / float
		switch ai {
		case 20: // false
			return cborValue{typ: cborSimple, u: 20}, off, nil
		case 21: // true
			return cborValue{typ: cborSimple, u: 21}, off, nil
		case 22: // null
			return cborValue{typ: cborSimple, u: 22}, off, nil
		case 25: // half-precision float (not needed but avoid panic)
			return cborValue{typ: cborSimple, u: 25}, off + 2, nil
		case 26: // single-precision float
			if off+4 > len(data) {
				return cborValue{}, 0, fmt.Errorf("cbor: truncated float32")
			}
			_ = math.Float32frombits(readBE32(data[off:]))
			return cborValue{typ: cborSimple, u: 26}, off + 4, nil
		case 27: // double-precision float
			if off+8 > len(data) {
				return cborValue{}, 0, fmt.Errorf("cbor: truncated float64")
			}
			_ = math.Float64frombits(readBE64(data[off:]))
			return cborValue{typ: cborSimple, u: 27}, off + 8, nil
		default:
			return cborValue{typ: cborSimple, u: arg}, off, nil
		}

	default:
		return cborValue{}, 0, fmt.Errorf("cbor: unsupported major type %d", mt)
	}
}

func decodeArg(data []byte, off int, ai byte) (uint64, int, error) {
	switch {
	case ai <= 23:
		return uint64(ai), off, nil
	case ai == 24:
		if off >= len(data) {
			return 0, 0, fmt.Errorf("cbor: truncated additional info")
		}
		return uint64(data[off]), off + 1, nil
	case ai == 25:
		if off+2 > len(data) {
			return 0, 0, fmt.Errorf("cbor: truncated uint16")
		}
		return uint64(readBE16(data[off:])), off + 2, nil
	case ai == 26:
		if off+4 > len(data) {
			return 0, 0, fmt.Errorf("cbor: truncated uint32")
		}
		return uint64(readBE32(data[off:])), off + 4, nil
	case ai == 27:
		if off+8 > len(data) {
			return 0, 0, fmt.Errorf("cbor: truncated uint64")
		}
		return readBE64(data[off:]), off + 8, nil
	default:
		return 0, 0, fmt.Errorf("cbor: reserved additional info %d", ai)
	}
}

func readBE16(b []byte) uint16 { return uint16(b[0])<<8 | uint16(b[1]) }
func readBE32(b []byte) uint32 {
	return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
}
func readBE64(b []byte) uint64 { return uint64(readBE32(b))<<32 | uint64(readBE32(b[4:])) }
