package ui

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"sync"
	"time"

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

	"git.wntrmute.dev/mc/mcias/internal/audit"
	"git.wntrmute.dev/mc/mcias/internal/auth"
	"git.wntrmute.dev/mc/mcias/internal/crypto"
	"git.wntrmute.dev/mc/mcias/internal/model"
	"git.wntrmute.dev/mc/mcias/internal/token"
	mciaswebauthn "git.wntrmute.dev/mc/mcias/internal/webauthn"
)

const (
	webauthnCeremonyTTL   = 120 * time.Second
	webauthnCleanupPeriod = 5 * time.Minute
	webauthnNonceBytes    = 16
)

// webauthnCeremony holds a pending WebAuthn ceremony.
type webauthnCeremony struct { //nolint:govet // fieldalignment: field order matches logical grouping
	expiresAt time.Time
	session   *libwebauthn.SessionData
	accountID int64
	ssoNonce  string // non-empty when login is part of an SSO redirect flow
}

// pendingWebAuthnCeremonies stores in-flight WebAuthn ceremonies for the UI.
var pendingUIWebAuthnCeremonies sync.Map //nolint:gochecknoglobals

func init() {
	go cleanupUIWebAuthnCeremonies()
}

func cleanupUIWebAuthnCeremonies() {
	ticker := time.NewTicker(webauthnCleanupPeriod)
	defer ticker.Stop()
	for range ticker.C {
		now := time.Now()
		pendingUIWebAuthnCeremonies.Range(func(key, value any) bool {
			c, ok := value.(*webauthnCeremony)
			if !ok || now.After(c.expiresAt) {
				pendingUIWebAuthnCeremonies.Delete(key)
			}
			return true
		})
	}
}

func storeUICeremony(session *libwebauthn.SessionData, accountID int64, ssoNonce string) (string, error) {
	raw, err := crypto.RandomBytes(webauthnNonceBytes)
	if err != nil {
		return "", fmt.Errorf("webauthn: generate ceremony nonce: %w", err)
	}
	nonce := fmt.Sprintf("%x", raw)
	pendingUIWebAuthnCeremonies.Store(nonce, &webauthnCeremony{
		session:   session,
		accountID: accountID,
		ssoNonce:  ssoNonce,
		expiresAt: time.Now().Add(webauthnCeremonyTTL),
	})
	return nonce, nil
}

func consumeUICeremony(nonce string) (*webauthnCeremony, bool) {
	v, ok := pendingUIWebAuthnCeremonies.LoadAndDelete(nonce)
	if !ok {
		return nil, false
	}
	c, ok2 := v.(*webauthnCeremony)
	if !ok2 || time.Now().After(c.expiresAt) {
		return nil, false
	}
	return c, true
}

// ---- Profile: registration ----

// handleWebAuthnBegin starts a WebAuthn credential registration ceremony.
func (u *UIServer) handleWebAuthnBegin(w http.ResponseWriter, r *http.Request) {
	if !u.cfg.WebAuthnEnabled() {
		u.renderError(w, r, http.StatusNotFound, "WebAuthn not configured")
		return
	}

	claims := claimsFromContext(r.Context())
	if claims == nil {
		u.renderError(w, r, http.StatusUnauthorized, "unauthorized")
		return
	}

	acct, err := u.db.GetAccountByUUID(claims.Subject)
	if err != nil {
		u.renderError(w, r, http.StatusUnauthorized, "account not found")
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
	var req struct {
		Password string `json:"password"`
		Name     string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		u.renderError(w, r, http.StatusBadRequest, "invalid request")
		return
	}

	if req.Password == "" {
		writeJSONError(w, http.StatusBadRequest, "password is required")
		return
	}

	// Security: check lockout.
	locked, lockErr := u.db.IsLockedOut(acct.ID)
	if lockErr != nil {
		u.logger.Error("lockout check (WebAuthn enroll)", "error", lockErr)
	}
	if locked {
		writeJSONError(w, http.StatusTooManyRequests, "account temporarily locked")
		return
	}

	// Security: verify current password.
	ok, verifyErr := auth.VerifyPassword(req.Password, acct.PasswordHash)
	if verifyErr != nil || !ok {
		_ = u.db.RecordLoginFailure(acct.ID)
		writeJSONError(w, http.StatusUnauthorized, "password is incorrect")
		return
	}

	masterKey, err := u.vault.MasterKey()
	if err != nil {
		writeJSONError(w, http.StatusServiceUnavailable, "vault sealed")
		return
	}

	dbCreds, err := u.db.GetWebAuthnCredentials(acct.ID)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}
	libCreds, err := mciaswebauthn.DecryptCredentials(masterKey, dbCreds)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	wa, err := mciaswebauthn.NewWebAuthn(&u.cfg.WebAuthn)
	if err != nil {
		u.logger.Error("create webauthn instance", "error", err)
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	user := mciaswebauthn.NewAccountUser([]byte(acct.UUID), acct.Username, libCreds)
	creation, session, err := wa.BeginRegistration(user,
		libwebauthn.WithExclusions(libwebauthn.Credentials(libCreds).CredentialDescriptors()),
		libwebauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementPreferred),
	)
	if err != nil {
		u.logger.Error("begin webauthn registration", "error", err)
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	nonce, err := storeUICeremony(session, acct.ID, "")
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	optionsJSON, _ := json.Marshal(creation)
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(map[string]interface{}{
		"options": json.RawMessage(optionsJSON),
		"nonce":   nonce,
	})
}

// handleWebAuthnFinish completes WebAuthn credential registration.
func (u *UIServer) handleWebAuthnFinish(w http.ResponseWriter, r *http.Request) {
	if !u.cfg.WebAuthnEnabled() {
		writeJSONError(w, http.StatusNotFound, "WebAuthn not configured")
		return
	}

	claims := claimsFromContext(r.Context())
	if claims == nil {
		writeJSONError(w, http.StatusUnauthorized, "unauthorized")
		return
	}

	acct, err := u.db.GetAccountByUUID(claims.Subject)
	if err != nil {
		writeJSONError(w, http.StatusUnauthorized, "account not found")
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
	var buf bytes.Buffer
	if _, err := buf.ReadFrom(r.Body); err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid request body")
		return
	}

	var wrapper struct {
		Nonce      string          `json:"nonce"`
		Name       string          `json:"name"`
		Credential json.RawMessage `json:"credential"`
	}
	if err := json.Unmarshal(buf.Bytes(), &wrapper); err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid JSON")
		return
	}

	ceremony, ok := consumeUICeremony(wrapper.Nonce)
	if !ok {
		writeJSONError(w, http.StatusBadRequest, "ceremony expired or invalid")
		return
	}
	if ceremony.accountID != acct.ID {
		writeJSONError(w, http.StatusForbidden, "ceremony mismatch")
		return
	}

	masterKey, err := u.vault.MasterKey()
	if err != nil {
		writeJSONError(w, http.StatusServiceUnavailable, "vault sealed")
		return
	}

	dbCreds, err := u.db.GetWebAuthnCredentials(acct.ID)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}
	libCreds, err := mciaswebauthn.DecryptCredentials(masterKey, dbCreds)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	wa, err := mciaswebauthn.NewWebAuthn(&u.cfg.WebAuthn)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	user := mciaswebauthn.NewAccountUser([]byte(acct.UUID), acct.Username, libCreds)
	fakeReq, _ := http.NewRequest(http.MethodPost, "/", bytes.NewReader(wrapper.Credential))
	fakeReq.Header.Set("Content-Type", "application/json")

	cred, err := wa.FinishRegistration(user, *ceremony.session, fakeReq)
	if err != nil {
		u.logger.Error("finish webauthn registration", "error", err)
		writeJSONError(w, http.StatusBadRequest, "registration failed")
		return
	}

	discoverable := cred.Flags.UserVerified && cred.Flags.BackupEligible
	name := wrapper.Name
	if name == "" {
		name = "Passkey"
	}

	modelCred, err := mciaswebauthn.EncryptCredential(masterKey, cred, name, discoverable)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}
	modelCred.AccountID = acct.ID

	credID, err := u.db.CreateWebAuthnCredential(modelCred)
	if err != nil {
		u.logger.Error("store webauthn credential", "error", err)
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	u.writeAudit(r, model.EventWebAuthnEnrolled, &acct.ID, &acct.ID,
		audit.JSON("credential_id", fmt.Sprintf("%d", credID), "name", name))

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]interface{}{
		"id":   credID,
		"name": name,
	})
}

// handleWebAuthnDelete removes a WebAuthn credential from the profile page.
func (u *UIServer) handleWebAuthnDelete(w http.ResponseWriter, r *http.Request) {
	claims := claimsFromContext(r.Context())
	if claims == nil {
		u.renderError(w, r, http.StatusUnauthorized, "unauthorized")
		return
	}

	acct, err := u.db.GetAccountByUUID(claims.Subject)
	if err != nil {
		u.renderError(w, r, http.StatusUnauthorized, "account not found")
		return
	}

	credIDStr := r.PathValue("id")
	credID, err := strconv.ParseInt(credIDStr, 10, 64)
	if err != nil {
		u.renderError(w, r, http.StatusBadRequest, "invalid credential ID")
		return
	}

	if err := u.db.DeleteWebAuthnCredential(credID, acct.ID); err != nil {
		u.renderError(w, r, http.StatusNotFound, "credential not found")
		return
	}

	u.writeAudit(r, model.EventWebAuthnRemoved, &acct.ID, &acct.ID,
		audit.JSON("credential_id", credIDStr))

	// Return updated credentials list fragment.
	creds, _ := u.db.GetWebAuthnCredentials(acct.ID)
	csrfToken, _ := u.setCSRFCookies(w)
	u.render(w, "webauthn_credentials", ProfileData{
		PageData: PageData{
			CSRFToken: csrfToken,
			ActorName: u.actorName(r),
			IsAdmin:   isAdmin(r),
		},
		WebAuthnCreds:   creds,
		DeletePrefix:    "/profile/webauthn",
		WebAuthnEnabled: u.cfg.WebAuthnEnabled(),
	})
}

// ---- Login: WebAuthn ----

// handleWebAuthnLoginBegin starts a WebAuthn login ceremony from the UI.
func (u *UIServer) handleWebAuthnLoginBegin(w http.ResponseWriter, r *http.Request) {
	if !u.cfg.WebAuthnEnabled() {
		writeJSONError(w, http.StatusNotFound, "WebAuthn not configured")
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
	var req struct {
		Username string `json:"username"`
		SSONonce string `json:"sso_nonce"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid JSON")
		return
	}

	wa, err := mciaswebauthn.NewWebAuthn(&u.cfg.WebAuthn)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	var (
		assertion *protocol.CredentialAssertion
		session   *libwebauthn.SessionData
		accountID int64
	)

	if req.Username != "" {
		acct, lookupErr := u.db.GetAccountByUsername(req.Username)
		if lookupErr != nil || acct.Status != model.AccountStatusActive {
			// Security: return discoverable login as dummy for unknown users.
			assertion, session, err = wa.BeginDiscoverableLogin()
		} else {
			locked, lockErr := u.db.IsLockedOut(acct.ID)
			if lockErr != nil {
				u.logger.Error("lockout check (WebAuthn UI login)", "error", lockErr)
			}
			if locked {
				assertion, session, err = wa.BeginDiscoverableLogin()
			} else {
				masterKey, mkErr := u.vault.MasterKey()
				if mkErr != nil {
					writeJSONError(w, http.StatusServiceUnavailable, "vault sealed")
					return
				}
				dbCreds, dbErr := u.db.GetWebAuthnCredentials(acct.ID)
				if dbErr != nil || len(dbCreds) == 0 {
					writeJSONError(w, http.StatusBadRequest, "no passkeys registered")
					return
				}
				libCreds, decErr := mciaswebauthn.DecryptCredentials(masterKey, dbCreds)
				if decErr != nil {
					writeJSONError(w, http.StatusInternalServerError, "internal error")
					return
				}
				user := mciaswebauthn.NewAccountUser([]byte(acct.UUID), acct.Username, libCreds)
				assertion, session, err = wa.BeginLogin(user)
				accountID = acct.ID
			}
		}
	} else {
		assertion, session, err = wa.BeginDiscoverableLogin()
	}

	if err != nil {
		u.logger.Error("begin webauthn login", "error", err)
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	nonce, err := storeUICeremony(session, accountID, req.SSONonce)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	optionsJSON, _ := json.Marshal(assertion)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]interface{}{
		"options": json.RawMessage(optionsJSON),
		"nonce":   nonce,
	})
}

// handleWebAuthnLoginFinish completes a WebAuthn login from the UI.
func (u *UIServer) handleWebAuthnLoginFinish(w http.ResponseWriter, r *http.Request) {
	if !u.cfg.WebAuthnEnabled() {
		writeJSONError(w, http.StatusNotFound, "WebAuthn not configured")
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
	var buf bytes.Buffer
	if _, err := buf.ReadFrom(r.Body); err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid request body")
		return
	}

	var wrapper struct {
		Nonce      string          `json:"nonce"`
		Credential json.RawMessage `json:"credential"`
	}
	if err := json.Unmarshal(buf.Bytes(), &wrapper); err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid JSON")
		return
	}

	ceremony, ok := consumeUICeremony(wrapper.Nonce)
	if !ok {
		writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
		return
	}

	wa, err := mciaswebauthn.NewWebAuthn(&u.cfg.WebAuthn)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	masterKey, err := u.vault.MasterKey()
	if err != nil {
		writeJSONError(w, http.StatusServiceUnavailable, "vault sealed")
		return
	}

	fakeReq, _ := http.NewRequest(http.MethodPost, "/", bytes.NewReader(wrapper.Credential))
	fakeReq.Header.Set("Content-Type", "application/json")

	var (
		acct    *model.Account
		cred    *libwebauthn.Credential
		dbCreds []*model.WebAuthnCredential
	)

	if ceremony.accountID != 0 {
		acct, err = u.db.GetAccountByID(ceremony.accountID)
		if err != nil {
			writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
			return
		}
		dbCreds, err = u.db.GetWebAuthnCredentials(acct.ID)
		if err != nil {
			writeJSONError(w, http.StatusInternalServerError, "internal error")
			return
		}
		libCreds, decErr := mciaswebauthn.DecryptCredentials(masterKey, dbCreds)
		if decErr != nil {
			writeJSONError(w, http.StatusInternalServerError, "internal error")
			return
		}
		user := mciaswebauthn.NewAccountUser([]byte(acct.UUID), acct.Username, libCreds)
		cred, err = wa.FinishLogin(user, *ceremony.session, fakeReq)
		if err != nil {
			u.writeAudit(r, model.EventWebAuthnLoginFail, &acct.ID, nil, `{"reason":"assertion_failed"}`)
			_ = u.db.RecordLoginFailure(acct.ID)
			writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
			return
		}
	} else {
		handler := func(rawID, userHandle []byte) (libwebauthn.User, error) {
			acctUUID := string(userHandle)
			foundAcct, lookupErr := u.db.GetAccountByUUID(acctUUID)
			if lookupErr != nil {
				return nil, fmt.Errorf("account not found")
			}
			if foundAcct.Status != model.AccountStatusActive {
				return nil, fmt.Errorf("account inactive")
			}
			acct = foundAcct

			foundDBCreds, credErr := u.db.GetWebAuthnCredentials(foundAcct.ID)
			if credErr != nil {
				return nil, fmt.Errorf("load credentials: %w", credErr)
			}
			dbCreds = foundDBCreds

			libCreds, decErr := mciaswebauthn.DecryptCredentials(masterKey, foundDBCreds)
			if decErr != nil {
				return nil, fmt.Errorf("decrypt credentials: %w", decErr)
			}
			return mciaswebauthn.NewAccountUser(userHandle, foundAcct.Username, libCreds), nil
		}

		cred, err = wa.FinishDiscoverableLogin(handler, *ceremony.session, fakeReq)
		if err != nil {
			u.writeAudit(r, model.EventWebAuthnLoginFail, nil, nil, `{"reason":"discoverable_assertion_failed"}`)
			writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
			return
		}
	}

	if acct == nil {
		writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
		return
	}

	if acct.Status != model.AccountStatusActive {
		writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
		return
	}

	locked, lockErr := u.db.IsLockedOut(acct.ID)
	if lockErr != nil {
		u.logger.Error("lockout check (WebAuthn UI login finish)", "error", lockErr)
	}
	if locked {
		writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
		return
	}

	// Validate sign counter.
	var matchedDBCred *model.WebAuthnCredential
	for _, dc := range dbCreds {
		decrypted, decErr := mciaswebauthn.DecryptCredential(masterKey, dc)
		if decErr != nil {
			continue
		}
		if bytes.Equal(decrypted.ID, cred.ID) {
			matchedDBCred = dc
			break
		}
	}

	if matchedDBCred != nil {
		if cred.Authenticator.SignCount > 0 || matchedDBCred.SignCount > 0 {
			if cred.Authenticator.SignCount <= matchedDBCred.SignCount {
				u.writeAudit(r, model.EventWebAuthnLoginFail, &acct.ID, nil,
					audit.JSON("reason", "counter_rollback"))
				_ = u.db.RecordLoginFailure(acct.ID)
				writeJSONError(w, http.StatusUnauthorized, "invalid credentials")
				return
			}
		}
		_ = u.db.UpdateWebAuthnSignCount(matchedDBCred.ID, cred.Authenticator.SignCount)
		_ = u.db.UpdateWebAuthnLastUsed(matchedDBCred.ID)
	}

	_ = u.db.ClearLoginFailures(acct.ID)

	// SSO redirect flow: issue authorization code and return redirect URL as JSON.
	if ceremony.ssoNonce != "" {
		if callbackURL, ok := u.buildSSOCallback(r, ceremony.ssoNonce, acct.ID); ok {
			u.writeAudit(r, model.EventWebAuthnLoginOK, &acct.ID, nil, "")
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{"redirect": callbackURL})
			return
		}
		// SSO session expired — fall through to normal login.
	}

	// Issue JWT and set session cookie.
	expiry := u.cfg.DefaultExpiry()
	roles, err := u.db.GetRoles(acct.ID)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}
	for _, rol := range roles {
		if rol == "admin" {
			expiry = u.cfg.AdminExpiry()
			break
		}
	}

	privKey, err := u.vault.PrivKey()
	if err != nil {
		writeJSONError(w, http.StatusServiceUnavailable, "vault sealed")
		return
	}
	tokenStr, tokenClaims, err := token.IssueToken(privKey, u.cfg.Tokens.Issuer, acct.UUID, roles, expiry)
	if err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	if err := u.db.TrackToken(tokenClaims.JTI, acct.ID, tokenClaims.IssuedAt, tokenClaims.ExpiresAt); err != nil {
		writeJSONError(w, http.StatusInternalServerError, "internal error")
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    tokenStr,
		Path:     "/",
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
		Expires:  tokenClaims.ExpiresAt,
	})

	if _, err := u.setCSRFCookies(w); err != nil {
		u.logger.Error("set CSRF cookie", "error", err)
	}

	u.writeAudit(r, model.EventWebAuthnLoginOK, &acct.ID, nil, "")
	u.writeAudit(r, model.EventTokenIssued, &acct.ID, nil,
		audit.JSON("jti", tokenClaims.JTI, "via", "webauthn_ui"))

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{"redirect": "/dashboard"})
}

// ---- Admin: WebAuthn credential management ----

// handleAdminWebAuthnDelete removes a WebAuthn credential from the admin account detail page.
func (u *UIServer) handleAdminWebAuthnDelete(w http.ResponseWriter, r *http.Request) {
	accountUUID := r.PathValue("id")
	acct, err := u.db.GetAccountByUUID(accountUUID)
	if err != nil {
		u.renderError(w, r, http.StatusNotFound, "account not found")
		return
	}

	credIDStr := r.PathValue("credentialId")
	credID, err := strconv.ParseInt(credIDStr, 10, 64)
	if err != nil {
		u.renderError(w, r, http.StatusBadRequest, "invalid credential ID")
		return
	}

	if err := u.db.DeleteWebAuthnCredentialAdmin(credID); err != nil {
		u.renderError(w, r, http.StatusNotFound, "credential not found")
		return
	}

	claims := claimsFromContext(r.Context())
	var actorID *int64
	if claims != nil {
		if actor, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
			actorID = &actor.ID
		}
	}

	u.writeAudit(r, model.EventWebAuthnRemoved, actorID, &acct.ID,
		audit.JSON("credential_id", credIDStr, "admin", "true"))

	// Return updated credentials list.
	creds, _ := u.db.GetWebAuthnCredentials(acct.ID)
	csrfToken, _ := u.setCSRFCookies(w)
	u.render(w, "webauthn_credentials", struct { //nolint:govet // fieldalignment: anonymous struct
		PageData
		WebAuthnCreds   []*model.WebAuthnCredential
		DeletePrefix    string
		WebAuthnEnabled bool
	}{
		PageData: PageData{
			CSRFToken: csrfToken,
			ActorName: u.actorName(r),
			IsAdmin:   isAdmin(r),
		},
		WebAuthnCreds:   creds,
		DeletePrefix:    "/accounts/" + accountUUID + "/webauthn",
		WebAuthnEnabled: u.cfg.WebAuthnEnabled(),
	})
}

// writeJSONError writes a JSON error response.
func writeJSONError(w http.ResponseWriter, status int, msg string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
