// Package vkauth verifies VK Mini App launch parameters in-process. VK signs the
// launch query string with the app's protected ("secure") key; the gateway holds that
// secret (GATEWAY_VK_APP_SECRET) and validates the `sign` itself rather than calling a
// side-service, because the check is a pure offline HMAC with no VK API round-trip
// (unlike the Telegram validator, which lives in a separate process to isolate the bot
// token). See docs/ARCHITECTURE.md §12.
package vkauth

import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"net/url"
	"strings"
)

// ErrInvalid is returned when launch params fail signature verification, are
// malformed, carry no signed vk_* parameters, or lack the vk_user_id.
var ErrInvalid = errors.New("vkauth: invalid vk launch params")

// Identity is the user extracted from verified VK launch params. ExternalID is the
// vk_user_id used as the identities external_id; Language is the vk_language hint that
// seeds a brand-new account's preferred language; Platform is the raw signed
// vk_platform value (e.g. "mobile_iphone", "desktop_web"), from which Subtype derives
// the trusted device family.
type Identity struct {
	ExternalID string
	Language   string
	Platform   string
}

// Subtype maps the signed vk_platform to the trusted device family recorded on the
// session — ios (iPhone/iPad, the store-frozen case), android, or web (desktop/mobile
// web and anything unrecognised). Because vk_platform rides inside the verified
// signature, this subtype is trustworthy, unlike a client-reported one.
func (i Identity) Subtype() string {
	switch {
	case strings.Contains(i.Platform, "android"):
		return "android"
	case strings.Contains(i.Platform, "iphone"), strings.Contains(i.Platform, "ipad"):
		return "ios"
	default:
		return "web"
	}
}

// Verify checks the `sign` of a VK Mini App launch query string against secret and
// returns the launching user's identity. Per VK's documented algorithm the signature
// is HMAC-SHA256 over the vk_*-prefixed parameters — sorted by key and serialized as a
// URL-encoded query string (url.Values.Encode mirrors VK's reference serialization for
// the constrained launch-parameter charset) — under the app secret, then base64url
// without padding; the comparison is constant-time.
//
// VK launch params carry no built-in expiry (unlike Telegram's auth_date), so freshness
// is deliberately not enforced here: the gateway mints its own short-lived session, and
// a replay only re-authenticates the same vk_user_id.
func Verify(params, secret string) (Identity, error) {
	values, err := url.ParseQuery(params)
	if err != nil {
		return Identity{}, ErrInvalid
	}
	sign := values.Get("sign")
	if sign == "" {
		return Identity{}, ErrInvalid
	}
	// Only vk_*-prefixed parameters are signed; any other query parameter the client
	// appended is outside the signature and must be excluded from the check.
	signed := url.Values{}
	for k, v := range values {
		if strings.HasPrefix(k, "vk_") {
			signed[k] = v
		}
	}
	if len(signed) == 0 {
		return Identity{}, ErrInvalid
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(signed.Encode()))
	want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
	if subtle.ConstantTimeCompare([]byte(want), []byte(sign)) != 1 {
		return Identity{}, ErrInvalid
	}
	externalID := values.Get("vk_user_id")
	if externalID == "" {
		return Identity{}, ErrInvalid
	}
	return Identity{ExternalID: externalID, Language: values.Get("vk_language"), Platform: values.Get("vk_platform")}, nil
}
