// Minimal OIDC Authorization Code flow against Authentik.
//
// Setup:
//   1. In Authentik create a provider + app (slug "go-example", redirect URI http://localhost:8080/callback).
//   2. Fill in ~/.authentik/.env (see .env.example).
//   3. go mod tidy && go run .
//   4. Open http://localhost:8080/login
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/joho/godotenv"
	"golang.org/x/oauth2"
)

// oauthState is a fixed random value for this process — good enough for a local demo.
// In production, generate a per-request random state and store it in a cookie.
var oauthState = "homelab-oidc-example"

func main() {
	// Load ~/.authentik/.env; shell env vars already set take precedence.
	home, _ := os.UserHomeDir()
	godotenv.Load(filepath.Join(home, ".authentik", ".env"))

	ctx := context.Background()

	// go-oidc discovers the token endpoint, auth endpoint, and JWKS URI automatically
	// from Authentik's /.well-known/openid-configuration.
	issuer := os.Getenv("AUTHENTIK_BASE_URL") + "/application/o/" + os.Getenv("APP_SLUG")
	provider, err := oidc.NewProvider(ctx, issuer)
	if err != nil {
		log.Fatalf("OIDC discovery failed (%s): %v", issuer, err)
	}

	cfg := &oauth2.Config{
		ClientID:     os.Getenv("OIDC_CLIENT_ID"),
		ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
		RedirectURL:  os.Getenv("REDIRECT_URL"),
		Endpoint:     provider.Endpoint(),
		Scopes:       []string{oidc.ScopeOpenID, "email", "profile"},
	}
	verifier := provider.Verifier(&oidc.Config{ClientID: cfg.ClientID})

	// /login — redirect the browser to Authentik's authorization endpoint
	http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, cfg.AuthCodeURL(oauthState), http.StatusFound)
	})

	// /callback — Authentik redirects here with ?code=...&state=...
	http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("state") != oauthState {
			http.Error(w, "state mismatch", http.StatusBadRequest)
			return
		}

		// Exchange the authorization code for tokens
		token, err := cfg.Exchange(ctx, r.URL.Query().Get("code"))
		if err != nil {
			http.Error(w, "token exchange: "+err.Error(), http.StatusInternalServerError)
			return
		}

		// Verify the ID token signature against Authentik's JWKS, then extract claims
		rawID, _ := token.Extra("id_token").(string)
		idToken, err := verifier.Verify(ctx, rawID)
		if err != nil {
			http.Error(w, "id_token verify: "+err.Error(), http.StatusInternalServerError)
			return
		}

		var claims map[string]any
		idToken.Claims(&claims)

		w.Header().Set("Content-Type", "application/json")
		enc := json.NewEncoder(w)
		enc.SetIndent("", "  ")
		enc.Encode(claims)
	})

	fmt.Println("open http://localhost:8080/login")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
