// Package robokassa builds and verifies Robokassa direct-rail (RUB) payments: it forms the signed
// hosted-payment URL a client is sent to, and verifies the Result-URL server callback that credits
// an order. It is pure provider glue — no database, no payments-domain coupling — so the payments
// domain stays provider-agnostic and this layer is unit-testable in isolation.
//
// This rail is RETIRED: YooKassa settles the direct rail now, and no deployment sets these
// credentials, so the shop set is empty and the rail is dormant. It stays wired as the fallback the
// direct rail resolves to when YooKassa has no shop, which makes reviving it a credentials change
// rather than a code change. README.md in this directory records the retired environment variables,
// the cabinet configuration and how to bring the rail back.
//
// The order is threaded through Robokassa's custom-parameter channel as Shp_order=<order id> (echoed
// back in the callback and bound into the signature), not the numeric InvId, because an order id is
// a uuid; InvId is sent as 0. Idempotency is therefore keyed on the order id at the credit site.
// Signatures use SHA-256 (configured to match the shop's technical settings); the shop's test mode
// is carried by IsTest.
package robokassa

import (
	"crypto/sha256"
	"encoding/hex"
	"net/url"
	"sort"
	"strings"

	"github.com/google/uuid"
)

// payEndpoint is Robokassa's hosted payment page; the signed query sends the client there.
const payEndpoint = "https://auth.robokassa.ru/Merchant/Index.aspx"

// Config is a Robokassa shop's credentials. Password1 signs the outgoing payment request;
// Password2 signs (and so verifies) the incoming Result callback. IsTest adds IsTest=1 so the shop's
// test mode simulates payments without money movement.
type Config struct {
	MerchantLogin string
	Password1     string
	Password2     string
	IsTest        bool
}

// PaymentURL builds the signed hosted-payment URL for an order: amount is the OutSum decimal string
// (roubles, e.g. "149.00"), description is the human payment purpose. The order id rides as
// Shp_order and is bound into the SHA-256 signature; InvId is 0 (unused).
func (c Config) PaymentURL(orderID uuid.UUID, amount, description string) string {
	q := url.Values{}
	q.Set("Shp_order", orderID.String())
	// Signature base: MerchantLogin:OutSum:InvId:Password1[:Shp_key=value...sorted].
	base := c.MerchantLogin + ":" + amount + ":0:" + c.Password1 + shpSuffix(q)

	q.Set("MerchantLogin", c.MerchantLogin)
	q.Set("OutSum", amount)
	q.Set("InvId", "0")
	q.Set("Description", description)
	q.Set("SignatureValue", sign(base))
	if c.IsTest {
		q.Set("IsTest", "1")
	}
	return payEndpoint + "?" + q.Encode()
}

// VerifyResult verifies a Result-URL callback and extracts the order it credits. It recomputes the
// SHA-256 signature OutSum:InvId:Password2[:Shp_...sorted] over the callback's own fields and
// compares it (case-insensitively) with SignatureValue. On success it returns the order id (from
// Shp_order) and the raw OutSum string the caller re-checks against the order amount; on any
// missing field, a signature mismatch or an unparseable order id it returns ok=false.
func (c Config) VerifyResult(v url.Values) (orderID uuid.UUID, outSum string, ok bool) {
	outSum = v.Get("OutSum")
	sig := v.Get("SignatureValue")
	order := v.Get("Shp_order")
	if outSum == "" || sig == "" || order == "" {
		return uuid.Nil, "", false
	}
	// Robokassa signs with the InvId it returns in the callback; recompute with that same value.
	base := outSum + ":" + v.Get("InvId") + ":" + c.Password2 + shpSuffix(v)
	if !strings.EqualFold(sign(base), sig) {
		return uuid.Nil, "", false
	}
	id, err := uuid.Parse(order)
	if err != nil {
		return uuid.Nil, "", false
	}
	return id, outSum, true
}

// shpSuffix renders the Shp_ custom parameters of v as Robokassa binds them into a signature:
// every Shp_-prefixed key, sorted alphabetically, appended as ":key=value".
func shpSuffix(v url.Values) string {
	var keys []string
	for k := range v {
		if strings.HasPrefix(k, "Shp_") {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)
	var b strings.Builder
	for _, k := range keys {
		b.WriteString(":")
		b.WriteString(k)
		b.WriteString("=")
		b.WriteString(v.Get(k))
	}
	return b.String()
}

// sign returns the lowercase hex SHA-256 of s, the hash Robokassa compares against SignatureValue.
func sign(s string) string {
	sum := sha256.Sum256([]byte(s))
	return hex.EncodeToString(sum[:])
}
