package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/coreos/go-oidc/v3/oidc"
)

type Claims struct {
	Sub    string
	Email  string
	Name   string
	Groups []string
	Source string
	HD     string
	TID    string
	Org    string
}

type Verifier struct {
	verifier *oidc.IDTokenVerifier
}

// NewVerifier builds an ID token verifier. issuerURL is the URL ultid uses to reach
// the provider (e.g. http://nginx/auth/application/o/ulti/ in Docker).
// discoveryHost is sent as the HTTP Host header (e.g. localhost) so Authentik returns
// the same issuer claim as browser-issued tokens; JWKS is fetched via issuerURL.
func NewVerifier(ctx context.Context, issuerURL, clientID, discoveryHost string) (*Verifier, error) {
	issuerURL = strings.TrimSuffix(strings.TrimSpace(issuerURL), "/")
	if issuerURL == "" {
		return nil, fmt.Errorf("empty issuer URL")
	}

	discovery, err := fetchDiscovery(ctx, issuerURL, discoveryHost)
	if err != nil {
		return nil, err
	}

	keySet := oidc.NewRemoteKeySet(ctx, issuerURL+"/jwks/")
	idVerifier := oidc.NewVerifier(discovery.Issuer, keySet, &oidc.Config{ClientID: clientID})
	return &Verifier{verifier: idVerifier}, nil
}

type discoveryDocument struct {
	Issuer string `json:"issuer"`
}

func fetchDiscovery(ctx context.Context, issuerURL, discoveryHost string) (*discoveryDocument, error) {
	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		issuerURL+"/.well-known/openid-configuration",
		nil,
	)
	if err != nil {
		return nil, err
	}
	if discoveryHost != "" {
		req.Host = discoveryHost
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return nil, fmt.Errorf("oidc discovery %s: %s: %s", issuerURL, resp.Status, strings.TrimSpace(string(body)))
	}

	var doc discoveryDocument
	if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
		return nil, err
	}
	if doc.Issuer == "" {
		return nil, fmt.Errorf("oidc discovery %s: missing issuer", issuerURL)
	}
	return &doc, nil
}

func (v *Verifier) Verify(ctx context.Context, rawToken string) (*Claims, error) {
	if v == nil || v.verifier == nil {
		return nil, fmt.Errorf("verifier unavailable")
	}

	token, err := v.verifier.Verify(ctx, rawToken)
	if err != nil {
		return nil, err
	}

	var claims struct {
		Sub    string   `json:"sub"`
		Email  string   `json:"email"`
		Name   string   `json:"name"`
		Groups []string `json:"groups"`
		HD     string   `json:"hd"`
		TID    string   `json:"tid"`
		Org    string   `json:"org"`
		Source string   `json:"ak-source"`
	}
	if err := token.Claims(&claims); err != nil {
		return nil, err
	}

	return &Claims{
		Sub:    claims.Sub,
		Email:  claims.Email,
		Name:   claims.Name,
		Groups: claims.Groups,
		HD:     claims.HD,
		TID:    claims.TID,
		Org:    claims.Org,
		Source: claims.Source,
	}, nil
}
