// Package webauthn: connector.go implements the WebAuthn ceremony
// handler for the bundled Dex (REQ-148, D-240, C-38). It serves
// registration + login endpoints at /orca/webauthn/{register,login}
// behind Traefik (R-017, step-ca cert, HTTPS secure context).
//
// The connector uses github.com/go-webauthn/webauthn for the
// cryptographic ceremony logic. Credential storage is in store.go
// (SQLite, 0600, public keys only).
package webauthn

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

// Connector is the WebAuthn ceremony handler. It is mounted behind
// Traefik and called by the bundled Dex.
type Connector struct {
	w       *webauthn.WebAuthn
	store   *Store
	rpID    string
	origin  string
}

// NewConnector builds a WebAuthn connector with the given RP ID
// (the cluster's Traefik-served domain, C-38) and origin (the full
// HTTPS URL).
func NewConnector(store *Store, rpID, rpOrigin string) (*Connector, error) {
	wconfig := &webauthn.Config{
		RPDisplayName: "Orca",
		RPID:          rpID,
		RPOrigins:     []string{rpOrigin},
	}
	w, err := webauthn.New(wconfig)
	if err != nil {
		return nil, fmt.Errorf("webauthn: new: %w", err)
	}
	return &Connector{
		w:      w,
		store:  store,
		rpID:   rpID,
		origin: rpOrigin,
	}, nil
}

// RegistrationSession holds the in-flight registration challenge.
type RegistrationSession struct {
	UserID    string
	Challenge *webauthn.SessionData
	CreatedAt time.Time
}

// sessionStore holds in-flight sessions (registration + login). In
// production this would be a Redis/shared cache; for the bundled
// single-lead Dex, an in-memory map with TTL is sufficient.
type sessionStore struct {
	sessions map[string]*RegistrationSession
}
var regSessions = &sessionStore{sessions: make(map[string]*RegistrationSession)}

// sessionTTL is the max time a registration/login session is valid.
const sessionTTL = 5 * time.Minute

// cleanSessions removes expired sessions.
func cleanSessions() {
	now := time.Now()
	for id, s := range regSessions.sessions {
		if now.Sub(s.CreatedAt) > sessionTTL {
			delete(regSessions.sessions, id)
		}
	}
}

// BeginRegistration starts the WebAuthn registration ceremony.
// GET /orca/webauthn/register?username=<name>
// Returns the creation options (challenge) for the browser.
func (c *Connector) BeginRegistration(w http.ResponseWriter, r *http.Request) {
	username := r.URL.Query().Get("username")
	if username == "" {
		http.Error(w, "username required", http.StatusBadRequest)
		return
	}
	userID := []byte(username)
	existing, _ := c.store.GetCredential(username)
	var creds []webauthn.Credential
	if existing != nil {
		creds = append(creds, webauthn.Credential{
			ID:              existing.CredentialID,
			PublicKey:       existing.PublicKey,
			AttestationType: "none",
		})
	}
	user := &webauthnUser{id: userID, name: username, credentials: creds}
	options, session, err := c.w.BeginRegistration(user)
	if err != nil {
		http.Error(w, fmt.Sprintf("begin registration: %v", err), http.StatusInternalServerError)
		return
	}
	sessionID := base64.RawURLEncoding.EncodeToString(userID)
	regSessions.sessions[sessionID] = &RegistrationSession{
		UserID:    username,
		Challenge: session,
		CreatedAt: time.Now(),
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(options)
}

// FinishRegistration completes the WebAuthn registration ceremony.
// POST /orca/webauthn/register/finish?username=<name>
// Body: the attestation response from the browser.
func (c *Connector) FinishRegistration(w http.ResponseWriter, r *http.Request) {
	username := r.URL.Query().Get("username")
	if username == "" {
		http.Error(w, "username required", http.StatusBadRequest)
		return
	}
	sessionID := base64.RawURLEncoding.EncodeToString([]byte(username))
	session, ok := regSessions.sessions[sessionID]
	if !ok {
		http.Error(w, "no registration session; call /register first", http.StatusBadRequest)
		return
	}
	if time.Since(session.CreatedAt) > sessionTTL {
		delete(regSessions.sessions, sessionID)
		http.Error(w, "session expired", http.StatusBadRequest)
		return
	}
	parsed, err := protocol.ParseCredentialCreationResponseBody(r.Body)
	if err != nil {
		http.Error(w, fmt.Sprintf("parse attestation: %v", err), http.StatusBadRequest)
		return
	}
	user := &webauthnUser{id: []byte(username), name: username}
	cred, err := c.w.CreateCredential(user, *session.Challenge, parsed)
	if err != nil {
		http.Error(w, fmt.Sprintf("create credential: %v", err), http.StatusInternalServerError)
		return
	}
	storeCred := &Credential{
		UserID:       username,
		CredentialID: cred.ID,
		PublicKey:    cred.PublicKey,
		SignCount:    0,
		AAGUID:       "",
		CreatedAt:    time.Now(),
	}
	if err := c.store.PutCredential(storeCred); err != nil {
		http.Error(w, fmt.Sprintf("store credential: %v", err), http.StatusInternalServerError)
		return
	}
	delete(regSessions.sessions, sessionID)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"status": "registered", "user_id": username})
}

// LoginSession holds the in-flight login challenge.
type LoginSession struct {
	UserID    string
	Challenge *webauthn.SessionData
	CreatedAt time.Time
}
var loginSessions = map[string]*LoginSession{}

// BeginLogin starts the WebAuthn login ceremony.
// GET /orca/webauthn/login?username=<name>
func (c *Connector) BeginLogin(w http.ResponseWriter, r *http.Request) {
	cleanSessions()
	username := r.URL.Query().Get("username")
	if username == "" {
		http.Error(w, "username required", http.StatusBadRequest)
		return
	}
	existing, _ := c.store.GetCredential(username)
	if existing == nil {
		http.Error(w, "user not registered", http.StatusNotFound)
		return
	}
	user := &webauthnUser{
		id:          []byte(username),
		name:        username,
		credentials: []webauthn.Credential{{ID: existing.CredentialID, PublicKey: existing.PublicKey}},
	}
	options, session, err := c.w.BeginLogin(user)
	if err != nil {
		http.Error(w, fmt.Sprintf("begin login: %v", err), http.StatusInternalServerError)
		return
	}
	loginSessions[username] = &LoginSession{
		UserID:    username,
		Challenge: session,
		CreatedAt: time.Now(),
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(options)
}

// FinishLogin completes the WebAuthn login ceremony.
// POST /orca/webauthn/login/finish?username=<name>
func (c *Connector) FinishLogin(w http.ResponseWriter, r *http.Request) {
	username := r.URL.Query().Get("username")
	if username == "" {
		http.Error(w, "username required", http.StatusBadRequest)
		return
	}
	session, ok := loginSessions[username]
	if !ok {
		http.Error(w, "no login session; call /login first", http.StatusBadRequest)
		return
	}
	if time.Since(session.CreatedAt) > sessionTTL {
		delete(loginSessions, username)
		http.Error(w, "session expired", http.StatusBadRequest)
		return
	}
	existing, _ := c.store.GetCredential(username)
	if existing == nil {
		http.Error(w, "user not registered", http.StatusNotFound)
		return
	}
	user := &webauthnUser{
		id:          []byte(username),
		name:        username,
		credentials: []webauthn.Credential{{ID: existing.CredentialID, PublicKey: existing.PublicKey}},
	}
	parsed, err := protocol.ParseCredentialRequestResponseBody(r.Body)
	if err != nil {
		http.Error(w, fmt.Sprintf("parse assertion: %v", err), http.StatusBadRequest)
		return
	}
	cred, err := c.w.ValidateLogin(user, *session.Challenge, parsed)
	if err != nil {
		http.Error(w, fmt.Sprintf("validate login: %v", err), http.StatusUnauthorized)
		return
	}
	_ = c.store.UpdateSignCount(username, cred.Authenticator.SignCount)
	delete(loginSessions, username)
	// The OIDC sub is the username (the connector maps credential ID
	// to sub). Dex uses this to issue the ID token.
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"status": "authenticated",
		"sub":    username,
	})
}

// Routes returns the HTTP handler mux for the WebAuthn connector.
// Mount under /orca/webauthn/ behind Traefik.
func (c *Connector) Routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/orca/webauthn/register", c.BeginRegistration)
	mux.HandleFunc("/orca/webauthn/register/finish", c.FinishRegistration)
	mux.HandleFunc("/orca/webauthn/login", c.BeginLogin)
	mux.HandleFunc("/orca/webauthn/login/finish", c.FinishLogin)
	mux.HandleFunc("/orca/webauthn/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"status":"ok","rp_id":"` + c.rpID + `"}`))
	})
	return mux
}

// Serve starts the WebAuthn HTTP handler on the given address. In
// production this runs behind Traefik (which provides TLS); the bind
// address is loopback only.
func (c *Connector) Serve(ctx context.Context, addr string) error {
	srv := &http.Server{
		Addr:              addr,
		Handler:           c.Routes(),
		ReadHeaderTimeout: 5 * time.Second,
	}
	go func() {
		<-ctx.Done()
		ctx2, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = srv.Shutdown(ctx2)
	}()
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		return fmt.Errorf("webauthn: serve: %w", err)
	}
	return nil
}

// webauthnUser implements webauthn.User.
type webauthnUser struct {
	id          []byte
	name        string
	credentials []webauthn.Credential
}

func (u *webauthnUser) WebAuthnID() []byte { return u.id }
func (u *webauthnUser) WebAuthnName() string { return u.name }
func (u *webauthnUser) WebAuthnDisplayName() string { return u.name }
func (u *webauthnUser) WebAuthnCredentials() []webauthn.Credential { return u.credentials }
func (u *webauthnUser) WebAuthnIcon() string { return "" }

// RPID returns the configured relying-party ID.
func (c *Connector) RPID() string { return c.rpID }

// Ensure strings import is used (for the healthz handler).
var _ = strings.Contains
