package main

import (
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/subtle"
	_ "embed"
	"errors"
	"fmt"
	"github.com/casbin/casbin/v2"
	"github.com/casbin/casbin/v2/model"
	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/golang-jwt/jwt/v5"
	"golang.org/x/crypto/argon2"
	"time"
)

//go:embed model.conf
var modelText string
var now = time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)

const issuer = "https://issuer.example"
const publicNonce = "public-test-nonce-bound-to-one-transaction"

func token(key *rsa.PrivateKey, aud, nonce string, expires time.Time) string {
	c := jwt.MapClaims{"iss": issuer, "sub": "alice", "aud": aud, "iat": now.Add(-time.Second).Unix(), "nbf": now.Add(-time.Second).Unix(), "exp": expires.Unix(), "jti": "public-test-id"}
	typ := "demo-access+jwt"
	if nonce != "" {
		c["nonce"] = nonce
		typ = "JWT"
	}
	t := jwt.NewWithClaims(jwt.SigningMethodRS256, c)
	t.Header["typ"] = typ
	raw, err := t.SignedString(key)
	must(err)
	return raw
}
func checkJWT(raw string, pub *rsa.PublicKey) error {
	c := new(jwt.RegisteredClaims)
	parsed, err := jwt.ParseWithClaims(raw, c, func(t *jwt.Token) (any, error) {
		if t.Header["typ"] != "demo-access+jwt" {
			return nil, errors.New("profile")
		}
		return pub, nil
	}, jwt.WithValidMethods([]string{"RS256"}), jwt.WithIssuer(issuer), jwt.WithAudience("orders-api"), jwt.WithExpirationRequired(), jwt.WithIssuedAt(), jwt.WithTimeFunc(func() time.Time { return now }))
	if err != nil {
		return err
	}
	if !parsed.Valid || c.Subject == "" || c.ID == "" || c.IssuedAt == nil || c.NotBefore == nil {
		return errors.New("claims")
	}
	return nil
}
func verifier(pub *rsa.PublicKey) *oidc.IDTokenVerifier {
	return oidc.NewVerifier(issuer, &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{pub}}, &oidc.Config{ClientID: "web-client", SupportedSigningAlgs: []string{"RS256"}, Now: func() time.Time { return now }})
}
func checkOIDC(v *oidc.IDTokenVerifier, raw, wantNonce string) error {
	id, err := v.Verify(context.Background(), raw)
	if err != nil {
		return err
	}
	if wantNonce == "" || subtle.ConstantTimeCompare([]byte(id.Nonce), []byte(wantNonce)) != 1 {
		return errors.New("nonce")
	}
	if id.Subject == "" || id.IssuedAt.IsZero() || id.IssuedAt.After(now.Add(time.Minute)) || !now.Before(id.Expiry) {
		return errors.New("application time/subject")
	}
	var extra struct {
		AZP string `json:"azp"`
	}
	if err = id.Claims(&extra); err != nil {
		return err
	}
	if len(id.Audience) > 1 && extra.AZP != "web-client" || extra.AZP != "" && extra.AZP != "web-client" {
		return errors.New("azp")
	}
	// A production callback must also atomically consume its transaction state.
	return nil
}

type Resource struct{ Name, Tenant string }

func enforcer() (*casbin.Enforcer, error) {
	m, err := model.NewModelFromString(modelText)
	if err != nil {
		return nil, err
	}
	e, err := casbin.NewEnforcer(m)
	if err != nil {
		return nil, err
	}
	if _, err = e.AddPolicy("reader", "team-a", "doc-1", "read"); err != nil {
		return nil, err
	}
	if _, err = e.AddGroupingPolicy("alice", "reader", "team-a"); err != nil {
		return nil, err
	}
	return e, nil
}
func main() {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	must(err)
	pub := &key.PublicKey
	fmt.Println("jwt-valid:", checkJWT(token(key, "orders-api", "", now.Add(time.Minute)), pub) == nil)
	fmt.Println("jwt-wrong-audience:", checkJWT(token(key, "other-api", "", now.Add(time.Minute)), pub) == nil)
	v := verifier(pub)
	raw := token(key, "web-client", publicNonce, now.Add(time.Minute))
	fmt.Println("oidc-valid:", checkOIDC(v, raw, publicNonce) == nil)
	fmt.Println("oidc-wrong-nonce:", checkOIDC(v, raw, "other-transaction") == nil)
	e, err := enforcer()
	must(err)
	ok, err := e.Enforce("alice", "team-a", Resource{"doc-1", "team-a"}, "read")
	must(err)
	fmt.Println("casbin-read:", ok)
	ok, err = e.Enforce("alice", "team-a", Resource{"doc-1", "team-b"}, "read")
	must(err)
	fmt.Println("casbin-cross-tenant:", ok)
	ok, err = e.Enforce("alice", "team-a", Resource{"doc-1", "team-a"}, "delete")
	must(err)
	fmt.Println("casbin-delete:", ok)
	salt := make([]byte, 16)
	_, err = rand.Read(salt)
	must(err)
	dk := argon2.IDKey([]byte("public test password"), salt, 2, 19*1024, 1, 32)
	fmt.Println("argon2-output-bytes:", len(dk))
}
func must(err error) {
	if err != nil {
		panic(err)
	}
}
