// Chore Network door — Go port. Standard library only (crypto/ed25519).
//
//	mux.Handle("/docs/", chore.Door(chore.Config{Site: "site:…", NetworkKey: "<x from https://chore.network/.well-known/chore.json>"}, docsHandler))
//
// Humans (no X-Chore-Key) always pass. Nothing here calls the network.
package chore

import (
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

type Config struct {
	Site       string
	NetworkKey string
	Network    string // default https://chore.network
	NowMs      func() int64
	WindowMs   int64
}

func (c Config) now() int64 {
	if c.NowMs != nil {
		return c.NowMs()
	}
	return time.Now().UnixMilli()
}

func unb64u(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(strings.TrimRight(s, "=")) }

// VerifyRequest checks the chore-v1 signature. Returns the agent id, or a reason.
func VerifyRequest(c Config, method, host, target string, body []byte, h http.Header) (string, string) {
	key, ts, nonce, sig := h.Get("X-Chore-Key"), h.Get("X-Chore-Ts"), h.Get("X-Chore-Nonce"), h.Get("X-Chore-Sig")
	if key == "" || ts == "" || nonce == "" || sig == "" {
		return "", "missing_signature_headers"
	}
	pub, e1 := unb64u(key)
	sg, e2 := unb64u(sig)
	if e1 != nil || e2 != nil || len(pub) != 32 || len(sg) != 64 {
		return "", "bad_key_or_sig_encoding"
	}
	t, err := strconv.ParseInt(ts, 10, 64)
	w := c.WindowMs
	if w == 0 {
		w = 300000
	}
	if err != nil || abs(c.now()-t) > w {
		return "", "timestamp_out_of_window"
	}
	host = strings.ToLower(host)
	host = strings.TrimSuffix(strings.TrimSuffix(host, ":443"), ":80")
	bh := sha256.Sum256(body)
	msg := strings.Join([]string{"chore-v1", strings.ToUpper(method), host, target, ts, nonce, hex.EncodeToString(bh[:])}, "\n")
	if !ed25519.Verify(ed25519.PublicKey(pub), []byte(msg), sg) {
		return "", "bad_signature"
	}
	id := sha256.Sum256(pub)
	return hex.EncodeToString(id[:])[:32], ""
}

// VerifyTicket checks a network-signed ticket for (agent, site).
func VerifyTicket(c Config, token, agent string) bool {
	p := strings.Split(token, ".")
	if token == "" || len(p) != 3 || p[0] != "v1" {
		return false
	}
	payload, e1 := unb64u(p[1])
	sig, e2 := unb64u(p[2])
	pub, e3 := unb64u(c.NetworkKey)
	if e1 != nil || e2 != nil || e3 != nil || len(sig) != 64 || len(pub) != 32 || !ed25519.Verify(ed25519.PublicKey(pub), payload, sig) {
		return false
	}
	var t struct {
		A   string `json:"a"`
		S   string `json:"s"`
		Exp int64  `json:"exp"`
	}
	if json.Unmarshal(payload, &t) != nil {
		return false
	}
	return t.A == agent && t.S == c.Site && t.Exp > c.now()
}

// Check returns (0, nil) to let the request through, else the status and JSON body to send.
func Check(c Config, method, host, target string, body []byte, h http.Header) (int, map[string]string) {
	if h.Get("X-Chore-Key") == "" {
		return 0, nil
	}
	agent, reason := VerifyRequest(c, method, host, target, body, h)
	if agent == "" {
		return 401, map[string]string{"error": "bad_signature", "reason": reason}
	}
	if VerifyTicket(c, h.Get("X-Chore-Ticket"), agent) {
		return 0, nil
	}
	net := strings.TrimRight(c.Network, "/")
	if net == "" {
		net = "https://chore.network"
	}
	return 402, map[string]string{"error": "ticket_required", "site": c.Site, "network": net,
		"challenge": net + "/chore/challenge?site=" + url.QueryEscape(c.Site), "submit": net + "/chore/submit", "pay": net + "/chore/pay"}
}

// Door wraps an http.Handler.
func Door(c Config, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body []byte
		if r.Body != nil && r.Header.Get("X-Chore-Key") != "" {
			body, _ = io.ReadAll(r.Body)
			r.Body = io.NopCloser(strings.NewReader(string(body)))
		}
		status, resp := Check(c, r.Method, r.Host, r.URL.RequestURI(), body, r.Header)
		if status == 0 {
			next.ServeHTTP(w, r)
			return
		}
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		w.WriteHeader(status)
		_ = json.NewEncoder(w).Encode(resp)
	})
}

func abs(x int64) int64 {
	if x < 0 {
		return -x
	}
	return x
}
