// Package fastog — official signer SDK for FastOG (Go).
//
// Generates HMAC-SHA256 content-signed Open Graph image URLs for the FastOG API.
// Zero dependencies (stdlib only). Runs on the CLIENT'S SERVER only — never
// expose the hmac secret to a browser.
//
// Contract (matches the FastOG server, app/Support/HmacSignature):
//
//	canonical = every query param except `s` (including `key`), keys sorted,
//	            array values sorted, each pair `enc(key)=enc(value)` RFC-3986
//	            encoded (unreserved-only), joined with "&"
//	message   = "GET/api/v1/og" + "\n" + canonical
//	signature = hex(HMAC-SHA256(message, secret))
package fastog

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"
)

// DefaultBaseURL is the FastOG API endpoint for OG image URLs.
const DefaultBaseURL = "https://fastog.com/api/v1/og"

const hexDigits = "0123456789ABCDEF"

// enc percent-encodes s RFC-3986 style: only unreserved A-Za-z0-9-._~ stay
// literal; everything else becomes %XX (uppercase). Space -> %20 (NOT +),
// matching PHP rawurlencode byte-for-byte.
func enc(s string) string {
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
			c == '-' || c == '.' || c == '_' || c == '~' {
			b.WriteByte(c)
		} else {
			b.WriteByte('%')
			b.WriteByte(hexDigits[c>>4])
			b.WriteByte(hexDigits[c&0x0f])
		}
	}
	return b.String()
}

// asSlice returns v as a slice if it is one ([]string or []any), else nil, false.
func asSlice(v any) ([]any, bool) {
	switch t := v.(type) {
	case []string:
		out := make([]any, len(t))
		for i, e := range t {
			out[i] = e
		}
		return out, true
	case []any:
		return t, true
	}
	return nil, false
}

// values normalizes a param value into a sorted slice of strings (arrays sorted,
// scalars wrapped).
func values(v any) []string {
	if slice, ok := asSlice(v); ok {
		out := make([]string, len(slice))
		for i, e := range slice {
			out[i] = fmt.Sprint(e)
		}
		sort.Strings(out)
		return out
	}
	return []string{fmt.Sprint(v)}
}

// Canonicalize returns the canonical query string the server verifies.
func Canonicalize(params map[string]any, apiKey string) string {
	all := make(map[string]any, len(params)+1)
	for k, v := range params {
		all[k] = v
	}
	all["key"] = apiKey

	keys := make([]string, 0, len(all))
	for k := range all {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var pairs []string
	for _, k := range keys {
		for _, vv := range values(all[k]) {
			pairs = append(pairs, enc(k)+"="+enc(vv))
		}
	}
	sort.Strings(pairs)
	return strings.Join(pairs, "&")
}

// Sign returns the HMAC-SHA256 hex signature of "GET/api/v1/og\n" + canonical.
func Sign(params map[string]any, apiKey, hmacSecret string) string {
	mac := hmac.New(sha256.New, []byte(hmacSecret))
	mac.Write([]byte("GET/api/v1/og\n" + Canonicalize(params, apiKey)))
	return hex.EncodeToString(mac.Sum(nil))
}

// SignedURL builds a ready-to-use signed OG image URL.
//
// Array params are emitted as key[0]=/key[1]= bracket syntax so the FastOG
// backend (PHP) decodes them as arrays (key=a&key=b would decode to a scalar
// and fail the signature check).
func SignedURL(params map[string]any, apiKey, hmacSecret string, baseURL ...string) string {
	base := DefaultBaseURL
	if len(baseURL) > 0 && baseURL[0] != "" {
		base = baseURL[0]
	}

	all := make(map[string]any, len(params)+2)
	for k, v := range params {
		all[k] = v
	}
	all["key"] = apiKey
	all["s"] = Sign(params, apiKey, hmacSecret)

	keys := make([]string, 0, len(all))
	for k := range all {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var pairs []string
	for _, k := range keys {
		if slice, ok := asSlice(all[k]); ok {
			for i, e := range slice {
				pairs = append(pairs, enc(k+"["+fmt.Sprint(i)+"]")+"="+enc(fmt.Sprint(e)))
			}
		} else {
			pairs = append(pairs, enc(k)+"="+enc(fmt.Sprint(all[k])))
		}
	}
	return base + "?" + strings.Join(pairs, "&")
}
