// Package loginwidget validates Telegram Login Widget authorization data, the
// web (non-Mini-App) sign-in flow used to attach a Telegram identity to an existing
// account during linking. Like initdata it lives in the connector
// because the secret is derived from the bot token, held only here
// (ARCHITECTURE.md §12); the gateway calls the connector's ValidateLoginWidget RPC.
//
// The Login Widget algorithm differs from Mini App initData: the secret key is
// SHA-256(bot_token) (not HMAC(bot_token, "WebAppData")), the data-check string is
// the sorted key=value lines of the top-level fields (id, first_name, username,
// auth_date, ...), and there is no nested user JSON or language_code.
package loginwidget

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

// ErrInvalidLoginWidget is returned when the data fails HMAC validation, is
// missing the hash or id, is malformed, or is older than the freshness window.
var ErrInvalidLoginWidget = errors.New("loginwidget: invalid telegram login widget data")

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

// User is the identity extracted from validated Login Widget data. ExternalID is
// the Telegram user id used as the identities external_id.
type User struct {
	ExternalID string
	Username   string
	FirstName  string
}

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

// HMACValidator validates Login Widget data against a bot token.
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 the widget data (a URL-encoded key=value query
// string carrying the widget fields plus hash) and returns the authenticated user.
func (v *HMACValidator) Validate(data string) (User, error) {
	values, err := url.ParseQuery(data)
	if err != nil {
		return User{}, ErrInvalidLoginWidget
	}
	hash := values.Get("hash")
	if hash == "" {
		return User{}, ErrInvalidLoginWidget
	}
	values.Del("hash")

	if !v.checkSignature(values, hash) {
		return User{}, ErrInvalidLoginWidget
	}
	if err := v.checkFreshness(values.Get("auth_date")); err != nil {
		return User{}, err
	}
	id := values.Get("id")
	if id == "" {
		return User{}, ErrInvalidLoginWidget
	}
	return User{ExternalID: id, Username: values.Get("username"), FirstName: values.Get("first_name")}, nil
}

// checkSignature recomputes the HMAC over the sorted data-check string under the
// SHA-256(bot_token) secret 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 := sha256.Sum256([]byte(v.botToken))
	mac := hmac.New(sha256.New, secret[:])
	mac.Write([]byte(dataCheck))
	want := mac.Sum(nil)
	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 ErrInvalidLoginWidget
	}
	secs, err := strconv.ParseInt(authDate, 10, 64)
	if err != nil {
		return ErrInvalidLoginWidget
	}
	if v.now().Sub(time.Unix(secs, 0)) > v.maxAge {
		return ErrInvalidLoginWidget
	}
	return nil
}
