// Package initdata validates Telegram Mini App launch data (initData). It lives in
// the connector because the HMAC secret is the bot token, which is held only here
// (ARCHITECTURE.md §12); the gateway calls the connector's ValidateInitData RPC
// instead of validating the launch data itself.
package initdata

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"
)

// ErrInvalidInitData is returned when initData fails HMAC validation, is missing
// the hash, is malformed, is older than the freshness window, or identifies a bot
// user (is_bot), which is denied.
var ErrInvalidInitData = errors.New("initdata: invalid telegram init data")

// defaultMaxAge bounds how old a validated initData payload may be.
const defaultMaxAge = 24 * time.Hour

// User is the identity extracted from a validated initData payload. ExternalID is
// the Telegram user id used as the identities external_id; LanguageCode seeds a
// new account's preferred language.
type User struct {
	ExternalID   string
	Username     string
	FirstName    string
	LanguageCode string
}

// Validator validates Telegram Web App launch data and returns the authenticated
// user. It is an interface so the connector can be tested with a fixture.
type Validator interface {
	Validate(initData string) (User, error)
}

// HMACValidator validates initData against a bot token per Telegram's documented
// algorithm: the data-check string is HMAC-SHA256'd under a secret derived from
// the bot token, and the result is compared with the supplied hash.
type HMACValidator struct {
	botToken string
	maxAge   time.Duration
	now      func() time.Time
}

// NewHMACValidator constructs a validator for botToken.
func NewHMACValidator(botToken string) *HMACValidator {
	return &HMACValidator{botToken: botToken, maxAge: defaultMaxAge, now: time.Now}
}

// Validate parses and verifies initData, returning the authenticated user.
func (v *HMACValidator) Validate(initData string) (User, error) {
	values, err := url.ParseQuery(initData)
	if err != nil {
		return User{}, ErrInvalidInitData
	}
	hash := values.Get("hash")
	if hash == "" {
		return User{}, ErrInvalidInitData
	}
	values.Del("hash")

	if !v.checkSignature(values, hash) {
		return User{}, ErrInvalidInitData
	}
	if err := v.checkFreshness(values.Get("auth_date")); err != nil {
		return User{}, err
	}
	return parseUser(values.Get("user"))
}

// checkSignature recomputes the HMAC over the sorted data-check string and
// compares it with hash in constant time.
func (v *HMACValidator) checkSignature(values url.Values, hash string) bool {
	keys := make([]string, 0, len(values))
	for k := range values {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	lines := make([]string, 0, len(keys))
	for _, k := range keys {
		lines = append(lines, k+"="+values.Get(k))
	}
	dataCheck := strings.Join(lines, "\n")

	secret := hmacSHA256([]byte("WebAppData"), []byte(v.botToken))
	want := hmacSHA256(secret, []byte(dataCheck))
	got, err := hex.DecodeString(hash)
	if err != nil {
		return false
	}
	return hmac.Equal(want, got)
}

// checkFreshness rejects an auth_date older than the validator's window.
func (v *HMACValidator) checkFreshness(authDate string) error {
	if authDate == "" {
		return ErrInvalidInitData
	}
	secs, err := strconv.ParseInt(authDate, 10, 64)
	if err != nil {
		return ErrInvalidInitData
	}
	if v.now().Sub(time.Unix(secs, 0)) > v.maxAge {
		return ErrInvalidInitData
	}
	return nil
}

// parseUser extracts the user id, names and language from the user JSON field.
func parseUser(userJSON string) (User, error) {
	if userJSON == "" {
		return User{}, ErrInvalidInitData
	}
	var u struct {
		ID           int64  `json:"id"`
		IsBot        bool   `json:"is_bot"`
		Username     string `json:"username"`
		FirstName    string `json:"first_name"`
		LanguageCode string `json:"language_code"`
	}
	if err := json.Unmarshal([]byte(userJSON), &u); err != nil || u.ID == 0 {
		return User{}, ErrInvalidInitData
	}
	// Deny bot principals: the HMAC has already proved Telegram signed this payload, so is_bot==true
	// is Telegram itself attesting the launching user is a bot. A real user opening the Mini App
	// never carries it, so reject defensively rather than provision an account for a bot.
	if u.IsBot {
		return User{}, ErrInvalidInitData
	}
	return User{
		ExternalID:   strconv.FormatInt(u.ID, 10),
		Username:     u.Username,
		FirstName:    u.FirstName,
		LanguageCode: u.LanguageCode,
	}, nil
}

// hmacSHA256 returns HMAC-SHA256(message) under key.
func hmacSHA256(key, message []byte) []byte {
	h := hmac.New(sha256.New, key)
	h.Write(message)
	return h.Sum(nil)
}
