package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	json "encoding/json/v2"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
)

// Public test fixture, NOT a deployable secret. The signature format is local
// teaching code, not an implementation of RFC 9421.
var testKey = []byte("PUBLIC-TEST-KEY-DO-NOT-DEPLOY-0001")

const testTime int64 = 1791892800

func canonical(r *http.Request, body []byte) []byte {
	sum := sha256.Sum256(body)
	fields := []string{"demo-v1", r.Method, r.Host, r.URL.EscapedPath(), r.URL.RawQuery, r.Header.Get("Content-Type"), r.Header.Get("X-Key-Id"), r.Header.Get("X-Time"), r.Header.Get("X-Nonce"), fmt.Sprintf("%x", sum)}
	var b strings.Builder
	for _, f := range fields {
		fmt.Fprintf(&b, "%d:%s", len(f), f)
	}
	return []byte(b.String())
}
func signature(r *http.Request, body []byte) string {
	h := hmac.New(sha256.New, testKey)
	h.Write(canonical(r, body))
	return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

type Verifier struct {
	mu   sync.Mutex
	seen map[string]int64
}

func (v *Verifier) Verify(r *http.Request, body []byte, now int64) error {
	for _, k := range []string{"X-Key-Id", "X-Time", "X-Nonce", "X-Signature", "Content-Type"} {
		if len(r.Header.Values(k)) != 1 {
			return errors.New("ambiguous header")
		}
	}
	if r.Header.Get("X-Key-Id") != "public-test-client" {
		return errors.New("key")
	}
	ts, err := strconv.ParseInt(r.Header.Get("X-Time"), 10, 64)
	if err != nil || ts < now-60 || ts > now+60 {
		return errors.New("time")
	}
	nonce := r.Header.Get("X-Nonce")
	if len(nonce) < 16 || len(nonce) > 64 {
		return errors.New("nonce")
	}
	for _, c := range nonce {
		if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9') {
			return errors.New("nonce")
		}
	}
	got, err := base64.RawURLEncoding.Strict().DecodeString(r.Header.Get("X-Signature"))
	if err != nil {
		return errors.New("signature")
	}
	h := hmac.New(sha256.New, testKey)
	h.Write(canonical(r, body))
	if !hmac.Equal(got, h.Sum(nil)) {
		return errors.New("signature")
	}
	v.mu.Lock()
	defer v.mu.Unlock()
	for key, expiry := range v.seen {
		if expiry < now {
			delete(v.seen, key)
		}
	}
	key := r.Header.Get("X-Key-Id") + ":" + nonce
	if _, ok := v.seen[key]; ok {
		return errors.New("replay")
	}
	if len(v.seen) >= 1024 {
		return errors.New("capacity")
	}
	v.seen[key] = ts + 60
	return nil
}
func (v *Verifier) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Add("Vary", "Origin")
	origin := r.Header.Get("Origin")
	if origin != "" && origin != "https://app.example" {
		http.Error(w, "origin", 403)
		return
	}
	if origin != "" {
		w.Header().Set("Access-Control-Allow-Origin", origin)
	}
	if r.Method == http.MethodOptions {
		w.Header().Add("Vary", "Access-Control-Request-Method")
		w.Header().Add("Vary", "Access-Control-Request-Headers")
		if origin == "" || r.Header.Get("Access-Control-Request-Method") != "POST" {
			http.Error(w, "preflight", 403)
			return
		}
		allowed := map[string]bool{"content-type": true, "x-key-id": true, "x-time": true, "x-nonce": true, "x-signature": true}
		for _, h := range strings.Split(r.Header.Get("Access-Control-Request-Headers"), ",") {
			if h = strings.ToLower(strings.TrimSpace(h)); h != "" && !allowed[h] {
				http.Error(w, "header", 403)
				return
			}
		}
		w.Header().Set("Access-Control-Allow-Methods", "POST")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Key-Id, X-Time, X-Nonce, X-Signature")
		w.WriteHeader(204)
		return
	}
	if r.Method != "POST" {
		w.Header().Set("Allow", "POST, OPTIONS")
		http.Error(w, "method", 405)
		return
	}
	if r.URL.Path != "/items" || r.URL.RawQuery != "" {
		http.Error(w, "route", 404)
		return
	}
	if r.Header.Get("Content-Type") != "application/json" {
		http.Error(w, "type", 415)
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, 128)
	defer r.Body.Close()
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "body", 413)
		return
	}
	if err = v.Verify(r, body, testTime); err != nil {
		http.Error(w, "authentication", 401)
		return
	}
	var in struct {
		Name string `json:"name"`
	}
	if err = json.Unmarshal(body, &in, json.RejectUnknownMembers(true)); err != nil || len(in.Name) == 0 || len(in.Name) > 20 {
		http.Error(w, "input", 400)
		return
	}
	// No actual mutation. A production handler must enforce object authorization
	// and combine replay/idempotency with durable business state.
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(201)
	fmt.Fprintln(w, `{"accepted":true}`)
}
func request(body, nonce string) *http.Request {
	r := httptest.NewRequest("POST", "https://api.example/items", strings.NewReader(body))
	r.Header.Set("Content-Type", "application/json")
	r.Header.Set("X-Key-Id", "public-test-client")
	r.Header.Set("X-Time", strconv.FormatInt(testTime, 10))
	r.Header.Set("X-Nonce", nonce)
	r.Header.Set("X-Signature", signature(r, []byte(body)))
	return r
}
func status(v *Verifier, r *http.Request) int {
	w := httptest.NewRecorder()
	v.ServeHTTP(w, r)
	return w.Code
}
func main() {
	v := &Verifier{seen: map[string]int64{}}
	body := `{"name":"ok"}`
	fmt.Println("first:", status(v, request(body, "aaaaaaaaaaaaaaaa")))
	fmt.Println("replay:", status(v, request(body, "aaaaaaaaaaaaaaaa")))
	r := request(body, "bbbbbbbbbbbbbbbb")
	r.Body = io.NopCloser(strings.NewReader(`{"name":"changed"}`))
	fmt.Println("tampered:", status(v, r))
	fmt.Println("duplicate-json:", status(v, request(`{"name":"a","name":"b"}`, "cccccccccccccccc")))
	fmt.Println("large-body:", status(v, request(strings.Repeat("x", 129), "dddddddddddddddd")))
	r = httptest.NewRequest("OPTIONS", "https://api.example/items", nil)
	r.Header.Set("Origin", "https://app.example")
	r.Header.Set("Access-Control-Request-Method", "POST")
	w := httptest.NewRecorder()
	v.ServeHTTP(w, r)
	fmt.Println("preflight:", w.Code, w.Header().Get("Access-Control-Allow-Origin"))
	r = request(body, "eeeeeeeeeeeeeeee")
	r.Header.Set("Origin", "https://app.example")
	r.Header.Set("X-Signature", "invalid")
	fmt.Println("allowed-origin-without-valid-signature:", status(v, r))
}
