// iCryptoNow Gateway — request signing and webhook verification (Go 1.20+, standard library only).
//
//	X-Signature = hex(HMAC-SHA256(secret, timestamp + "\n" + METHOD + "\n" + pathWithQuery + "\n" + rawBody))
//	timestamp   = unix milliseconds, within ±5 minutes of our clock
//
// Self-test against the published vectors:  go run gateway.go ../vectors.json
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
)

func Sign(secret, timestamp, method, pathWithQuery, body string) string {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "\n" + strings.ToUpper(method) + "\n" + pathWithQuery + "\n" + body))
	return hex.EncodeToString(mac.Sum(nil))
}

// Request calls the gateway. body is any JSON-serialisable value, or nil.
func Request(baseURL, keyID, secret, method, pathWithQuery string, body any) (int, []byte, error) {
	raw := []byte{}
	if body != nil {
		var err error
		if raw, err = json.Marshal(body); err != nil { // sign exactly the bytes you send
			return 0, nil, err
		}
	}
	timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
	req, err := http.NewRequest(method, baseURL+pathWithQuery, bytes.NewReader(raw))
	if err != nil {
		return 0, nil, err
	}
	req.Header.Set("X-Key-Id", keyID)
	req.Header.Set("X-Timestamp", timestamp)
	req.Header.Set("X-Signature", Sign(secret, timestamp, method, pathWithQuery, string(raw)))
	if len(raw) > 0 {
		req.Header.Set("Content-Type", "application/json")
	}
	res, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
	if err != nil {
		return 0, nil, err
	}
	defer res.Body.Close()
	out, err := io.ReadAll(res.Body)
	return res.StatusCode, out, err
}

var tsRe = regexp.MustCompile(`^\d{13}$`)
var sigRe = regexp.MustCompile(`^[0-9a-f]{64}$`)

// VerifyWebhook checks a callback. rawBody is the exact request body (verify BEFORE unmarshalling);
// pathWithQuery is the path of your callback URL as configured with us.
func VerifyWebhook(secret, pathWithQuery, timestamp, signature, rawBody string, nowMs int64) bool {
	if !tsRe.MatchString(timestamp) || !sigRe.MatchString(signature) {
		return false
	}
	ts, _ := strconv.ParseInt(timestamp, 10, 64)
	if d := nowMs - ts; d > 5*60*1000 || d < -5*60*1000 {
		return false
	}
	return hmac.Equal([]byte(Sign(secret, timestamp, "POST", pathWithQuery, rawBody)), []byte(signature))
}

// ── self-test ──
type vector struct{ Secret, Timestamp, Method, Path, Body, Signature string }

func main() {
	path := "../vectors.json"
	if len(os.Args) > 1 {
		path = os.Args[1]
	}
	data, err := os.ReadFile(path)
	if err != nil {
		panic(err)
	}
	var v map[string]vector
	if err := json.Unmarshal(data, &v); err != nil {
		panic(err)
	}
	failed := false
	check := func(name string, ok bool) {
		if ok {
			fmt.Println("PASS", name)
		} else {
			fmt.Println("FAIL", name)
			failed = true
		}
	}
	for _, k := range []string{"request_post", "request_get"} {
		x := v[k]
		check(k, Sign(x.Secret, x.Timestamp, x.Method, x.Path, x.Body) == x.Signature)
	}
	w := v["webhook"]
	ts, _ := strconv.ParseInt(w.Timestamp, 10, 64)
	check("webhook valid", VerifyWebhook(w.Secret, w.Path, w.Timestamp, w.Signature, w.Body, ts))
	check("webhook tampered body rejected", !VerifyWebhook(w.Secret, w.Path, w.Timestamp, w.Signature, strings.Replace(w.Body, "100000000", "900000000", 1), ts))
	check("webhook stale timestamp rejected", !VerifyWebhook(w.Secret, w.Path, w.Timestamp, w.Signature, w.Body, ts+301000))
	if failed {
		os.Exit(1)
	}
}
