// Copyright (c) 2025 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package whatsmeow

import (
	"crypto/hmac"
	"crypto/sha256"
	_ "embed"
	"encoding/binary"
	"encoding/json"
	"sort"
	"sync"

	"go.mau.fi/util/exerrors"
	"go.mau.fi/util/exstrings"

	waBinary "go.mau.fi/whatsmeow/binary"
	"go.mau.fi/whatsmeow/proto/waE2E"
	"go.mau.fi/whatsmeow/types"
)

//go:embed reportingfields.json
var reportingFieldsJSON string
var getReportingFields = sync.OnceValue(func() (output []reportingField) {
	exerrors.PanicIfNotNil(json.Unmarshal(exstrings.UnsafeBytes(reportingFieldsJSON), &output))
	return
})

type reportingField struct {
	FieldNumber int              `json:"f"`
	IsMessage   bool             `json:"m,omitempty"`
	Subfields   []reportingField `json:"s,omitempty"`
}

func (cli *Client) shouldIncludeReportingToken(message *waE2E.Message) bool {
	if !cli.SendReportingTokens {
		return false
	}
	return message.ReactionMessage == nil &&
		message.EncReactionMessage == nil &&
		message.EncEventResponseMessage == nil &&
		message.PollUpdateMessage == nil
}

func (cli *Client) getMessageReportingToken(
	msgProtobuf []byte,
	msg *waE2E.Message,
	senderJID, remoteJID types.JID,
	messageID types.MessageID,
) waBinary.Node {
	reportingSecret, _ := generateMsgSecretKey(
		EncSecretReportToken, senderJID, messageID, remoteJID,
		msg.GetMessageContextInfo().GetMessageSecret(),
	)
	hasher := hmac.New(sha256.New, reportingSecret)
	hasher.Write(getReportingToken(msgProtobuf))
	return waBinary.Node{
		Tag: "reporting",
		Content: []waBinary.Node{{
			Tag:     "reporting_token",
			Attrs:   waBinary.Attrs{"v": "2"},
			Content: hasher.Sum(nil)[:16],
		}},
	}
}

func getReportingToken(messageProtobuf []byte) []byte {
	return extractReportingTokenContent(messageProtobuf, getReportingFields())
}

// Helper to find config for a field number
func getConfigForField(fields []reportingField, fieldNum int) *reportingField {
	for i := range fields {
		if fields[i].FieldNumber == fieldNum {
			return &fields[i]
		}
	}
	return nil
}

// Protobuf wire types
const (
	wireVarint = 0
	wire64bit  = 1
	wireBytes  = 2
	wire32bit  = 5
)

// Extracts the reporting token content recursively
func extractReportingTokenContent(data []byte, config []reportingField) []byte {
	type field struct {
		Num   int
		Bytes []byte
	}
	var fields []field
	i := 0
	for i < len(data) {
		// Read tag (varint)
		tag, tagLen := binary.Uvarint(data[i:])
		if tagLen <= 0 {
			break // malformed
		}
		fieldNum := int(tag >> 3)
		wireType := int(tag & 0x7)
		fieldCfg := getConfigForField(config, fieldNum)
		fieldStart := i
		i += tagLen
		if fieldCfg == nil {
			// Skip field
			switch wireType {
			case wireVarint:
				_, n := binary.Uvarint(data[i:])
				i += n
			case wire64bit:
				i += 8
			case wireBytes:
				l, n := binary.Uvarint(data[i:])
				i += n + int(l)
			case wire32bit:
				i += 4
			default:
				return nil
			}
			continue
		}
		switch wireType {
		case wireVarint:
			_, n := binary.Uvarint(data[i:])
			i += n
			fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:i]})
		case wire64bit:
			i += 8
			fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:i]})
		case wireBytes:
			l, n := binary.Uvarint(data[i:])
			valStart := i + n
			valEnd := valStart + int(l)
			if fieldCfg.IsMessage || len(fieldCfg.Subfields) > 0 {
				// Recursively extract subfields
				sub := extractReportingTokenContent(data[valStart:valEnd], fieldCfg.Subfields)
				if len(sub) > 0 {
					// Re-encode tag and length
					buf := make([]byte, 0, tagLen+n+len(sub))
					tagBuf := make([]byte, binary.MaxVarintLen64)
					tagN := binary.PutUvarint(tagBuf, tag)
					lenBuf := make([]byte, binary.MaxVarintLen64)
					lenN := binary.PutUvarint(lenBuf, uint64(len(sub)))
					buf = append(buf, tagBuf[:tagN]...)
					buf = append(buf, lenBuf[:lenN]...)
					buf = append(buf, sub...)
					fields = append(fields, field{Num: fieldNum, Bytes: buf})
				}
			} else {
				fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:valEnd]})
			}
			i = valEnd
		case wire32bit:
			i += 4
			fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:i]})
		default:
			return nil
		}
	}
	// Sort by field number
	sort.Slice(fields, func(i, j int) bool { return fields[i].Num < fields[j].Num })
	// Concatenate
	var out []byte
	for _, f := range fields {
		out = append(out, f.Bytes...)
	}
	return out
}
