// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
//     https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2

package wallet

import (
	"encoding/binary"
	"fmt"

	coreerr "dappco.re/go/core/log"

	"dappco.re/go/core/blockchain/types"
	"dappco.re/go/core/blockchain/wire"
)

// Extra field tag constants from the CryptoNote variant vector encoding.
const (
	extraTagDerivationHint = 11 // string-encoded: varint(length) + bytes
	extraTagUnlockTime     = 14 // varint-encoded
	extraTagPublicKey      = 22 // fixed 32 bytes
)

// TxExtra holds wallet-critical fields parsed from a transaction's raw extra
// bytes. The Raw field preserves the original bytes for round-tripping.
type TxExtra struct {
	TxPublicKey    types.PublicKey
	UnlockTime     uint64
	DerivationHint uint16
	Raw            []byte
}

// ParseTxExtra decodes a CryptoNote variant vector (raw tx extra bytes) and
// extracts the wallet-critical fields: tx public key (tag 22), unlock time
// (tag 14), and derivation hint (tag 11). Unknown tags are skipped.
func ParseTxExtra(raw []byte) (*TxExtra, error) {
	extra := &TxExtra{Raw: make([]byte, len(raw))}
	copy(extra.Raw, raw)

	if len(raw) == 0 {
		return extra, nil
	}

	count, n, err := wire.DecodeVarint(raw)
	if err != nil {
		return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid varint count", err)
	}
	pos := n

	for i := uint64(0); i < count && pos < len(raw); i++ {
		tag := raw[pos]
		pos++

		switch tag {
		case extraTagPublicKey:
			if pos+32 > len(raw) {
				return extra, coreerr.E("ParseTxExtra", fmt.Sprintf("wallet: extra: truncated public key at offset %d", pos), nil)
			}
			copy(extra.TxPublicKey[:], raw[pos:pos+32])
			pos += 32

		case extraTagUnlockTime:
			val, vn, vErr := wire.DecodeVarint(raw[pos:])
			if vErr != nil {
				return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid unlock_time varint", vErr)
			}
			extra.UnlockTime = val
			pos += vn

		case extraTagDerivationHint:
			length, vn, vErr := wire.DecodeVarint(raw[pos:])
			if vErr != nil {
				return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid hint length varint", vErr)
			}
			pos += vn
			if length == 2 && pos+2 <= len(raw) {
				extra.DerivationHint = binary.LittleEndian.Uint16(raw[pos : pos+2])
			}
			pos += int(length)

		default:
			skip, skipErr := skipExtraElement(raw[pos:], tag)
			if skipErr != nil {
				// Unknown or malformed element; stop parsing but return what we have.
				return extra, nil
			}
			pos += skip
		}
	}

	return extra, nil
}

// BuildTxExtra constructs a minimal raw extra containing only the tx public
// key (tag 22). This is the minimum required for a valid transaction.
func BuildTxExtra(txPubKey types.PublicKey) []byte {
	raw := wire.EncodeVarint(1)
	raw = append(raw, extraTagPublicKey)
	raw = append(raw, txPubKey[:]...)
	return raw
}

// skipExtraElement returns the number of data bytes to skip for a given tag,
// based on the CryptoNote variant vector element sizes.
func skipExtraElement(data []byte, tag uint8) (int, error) {
	switch tag {
	// String types: varint(length) + length bytes.
	case 7, 9, 11, 19:
		if len(data) == 0 {
			return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: no data for string tag %d", tag), nil)
		}
		length, n, err := wire.DecodeVarint(data)
		if err != nil {
			return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: invalid string length for tag %d", tag), err)
		}
		return n + int(length), nil

	// Varint types: single varint value.
	case 14, 15, 16, 26, 27:
		_, n, err := wire.DecodeVarint(data)
		if err != nil {
			return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: invalid varint for tag %d", tag), err)
		}
		return n, nil

	// Fixed-size types.
	case 10:
		return 8, nil // 8 bytes
	case 17, 28:
		return 4, nil // 4 bytes
	case 23, 24:
		return 2, nil // 2 bytes
	case 22:
		return 32, nil // public key
	case 8, 29:
		return 64, nil // signature

	default:
		return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: unknown tag %d", tag), nil)
	}
}
