package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/pagination"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/service"
	"baron-sso-backend/internal/utils"
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"maps"
	"net"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/google/uuid"
)

type DevHandler struct {
	Hydra              *service.HydraAdminService
	Redis              domain.RedisRepository
	HeadlessJWKS       *service.HeadlessJWKSCacheService
	SecretRepo         domain.ClientSecretRepository
	AuditRepo          domain.AuditRepository
	KratosAdmin        service.KratosAdminService
	ConsentRepo        repository.ClientConsentRepository
	Keto               service.KetoService
	KetoOutbox         repository.KetoOutboxRepository
	RPSvc              service.RelyingPartyService
	TenantSvc          service.TenantService
	DeveloperSvc       developerRequestService
	RPUserMetadataRepo repository.RPUserMetadataRepository
	RPUsageQueries     domain.RPUsageQueryRepository
	Auth               interface {
		GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
	}
}

type developerRequestService interface {
	RequestAccess(ctx context.Context, req domain.DeveloperRequest) error
	GetRequestStatus(ctx context.Context, userID, tenantID string) (*domain.DeveloperRequest, error)
	GetRequestByID(ctx context.Context, id uint) (*domain.DeveloperRequest, error)
	ListRequests(ctx context.Context, userID, status string) ([]domain.DeveloperRequest, error)
	ApproveRequest(ctx context.Context, id uint, adminNotes string) error
	RejectRequest(ctx context.Context, id uint, adminNotes string) error
	CancelApprovedRequest(ctx context.Context, id uint, adminNotes string) error
}

func NewDevHandler(
	redis domain.RedisRepository,
	secretRepo domain.ClientSecretRepository,
	consentRepo repository.ClientConsentRepository,
	rpSvc service.RelyingPartyService,
	keto service.KetoService,
	ketoOutbox repository.KetoOutboxRepository,
	tenantSvc service.TenantService,
	developerSvc *service.DeveloperService,
	auth ...interface {
		GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
	},
) *DevHandler {
	var authProvider interface {
		GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
	}
	if len(auth) > 0 {
		authProvider = auth[0]
	}

	return &DevHandler{
		Hydra:        service.NewHydraAdminService(),
		Redis:        redis,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(redis, nil),
		SecretRepo:   secretRepo,
		AuditRepo:    nil,
		KratosAdmin:  service.NewKratosAdminService(),
		ConsentRepo:  consentRepo,
		Keto:         keto,
		KetoOutbox:   ketoOutbox,
		RPSvc:        rpSvc,
		TenantSvc:    tenantSvc,
		DeveloperSvc: developerSvc,
		Auth:         authProvider,
	}
}

type devAuditListResponse struct {
	Items      []domain.AuditLog `json:"items"`
	Limit      int               `json:"limit"`
	Cursor     string            `json:"cursor,omitempty"`
	NextCursor string            `json:"next_cursor,omitempty"`
}

type devStatsResponse struct {
	TotalClients   int64 `json:"total_clients"`
	ActiveSessions int64 `json:"active_sessions"`
	AuthFailures   int64 `json:"auth_failures_24h"`
}

type devRPUsageDailyResponse struct {
	Items    []domain.RPUsageDailyMetric `json:"items"`
	Days     int                         `json:"days"`
	Period   string                      `json:"period"`
	TenantID string                      `json:"tenantId,omitempty"`
}

type clientSummary struct {
	ID                               string         `json:"id"`
	Name                             string         `json:"name"`
	Type                             string         `json:"type"`
	Status                           string         `json:"status"`
	CreatedAt                        *time.Time     `json:"createdAt,omitempty"`
	RedirectURIs                     []string       `json:"redirectUris"`
	Scopes                           []string       `json:"scopes"`
	ClientSecret                     string         `json:"clientSecret,omitempty"`
	TokenEndpointAuthMethod          string         `json:"tokenEndpointAuthMethod,omitempty"`
	SkipConsent                      bool           `json:"skipConsent"`
	JwksUri                          string         `json:"jwksUri,omitempty"`
	Jwks                             any            `json:"jwks,omitempty"`
	BackchannelLogoutURI             string         `json:"backchannelLogoutUri,omitempty"`
	BackchannelLogoutSessionRequired bool           `json:"backchannelLogoutSessionRequired"`
	Metadata                         map[string]any `json:"metadata,omitempty"`
}

type clientListResponse struct {
	Items      []clientSummary `json:"items"`
	Limit      int             `json:"limit"`
	Offset     int             `json:"offset"`
	Cursor     string          `json:"cursor,omitempty"`
	NextCursor string          `json:"nextCursor,omitempty"`
}

type clientDetailResponse struct {
	Client            clientSummary                  `json:"client"`
	Endpoints         clientEndpoints                `json:"endpoints"`
	HeadlessJWKSCache *domain.HeadlessJWKSCacheState `json:"headlessJwksCache,omitempty"`
}

type clientEndpoints struct {
	Discovery     string `json:"discovery"`
	Issuer        string `json:"issuer"`
	Authorization string `json:"authorization"`
	Token         string `json:"token"`
	UserInfo      string `json:"userinfo"`
}

type clientRelationSummary struct {
	Relation    string `json:"relation"`
	Subject     string `json:"subject"`
	SubjectType string `json:"subjectType"`
	SubjectID   string `json:"subjectId"`
	UserName    string `json:"userName,omitempty"`
	UserEmail   string `json:"userEmail,omitempty"`
	UserLoginID string `json:"userLoginId,omitempty"`
}

type clientRelationListResponse struct {
	Items []clientRelationSummary `json:"items"`
}

type devUserSummary struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Email   string `json:"email"`
	LoginID string `json:"loginId,omitempty"`
}

type devUserListResponse struct {
	Items []devUserSummary `json:"items"`
}

type clientRelationUpsertRequest struct {
	Relation string `json:"relation"`
	Subject  string `json:"subject"`
	UserID   string `json:"userId"`
}

type consentSummary struct {
	Subject         string         `json:"subject"`
	UserName        string         `json:"userName,omitempty"`
	ClientID        string         `json:"clientId"`
	ClientName      string         `json:"clientName,omitempty"`
	GrantedScopes   []string       `json:"grantedScopes"`
	AuthenticatedAt string         `json:"authenticatedAt,omitempty"`
	CreatedAt       time.Time      `json:"createdAt"`
	DeletedAt       *time.Time     `json:"deletedAt,omitempty"`
	Status          string         `json:"status"`
	TenantID        string         `json:"tenantId,omitempty"`
	TenantName      string         `json:"tenantName,omitempty"`
	RPMetadata      domain.JSONMap `json:"rpMetadata,omitempty"`
}

type consentListResponse struct {
	Items      []consentSummary `json:"items"`
	Total      int64            `json:"total"`
	Limit      int              `json:"limit"`
	Offset     int              `json:"offset"`
	Cursor     string           `json:"cursor,omitempty"`
	NextCursor string           `json:"nextCursor,omitempty"`
}

type clientUpsertRequest struct {
	ID                               *string         `json:"id"`
	Name                             *string         `json:"name"`
	Type                             *string         `json:"type"`
	Status                           *string         `json:"status"`
	RedirectURIs                     *[]string       `json:"redirectUris"`
	Scopes                           *[]string       `json:"scopes"`
	GrantTypes                       *[]string       `json:"grantTypes"`
	ResponseTypes                    *[]string       `json:"responseTypes"`
	TokenEndpointAuthMethod          *string         `json:"tokenEndpointAuthMethod"`
	SkipConsent                      *bool           `json:"skipConsent"`
	JwksUri                          *string         `json:"jwksUri"`
	Jwks                             any             `json:"jwks"`
	BackchannelLogoutURI             *string         `json:"backchannelLogoutUri"`
	BackchannelLogoutSessionRequired *bool           `json:"backchannelLogoutSessionRequired"`
	Metadata                         *map[string]any `json:"metadata"`
}

type normalizedIDTokenClaim struct {
	Namespace       string `json:"namespace"`
	Key             string `json:"key"`
	Value           string `json:"value"`
	ValueType       string `json:"valueType"`
	ReadPermission  string `json:"readPermission"`
	WritePermission string `json:"writePermission"`
}

var protectedSystemClientIDs = map[string]struct{}{
	"oathkeeper-introspect": {},
}

var reservedSystemClientNames = map[string]string{
	"adminfront": "adminfront",
	"devfront":   "devfront",
}

var allowedRelyingPartyOperatorRelations = map[string]struct{}{
	"admins":              {},
	"creator":             {},
	"config_editor":       {},
	"secret_viewer":       {},
	"secret_rotator":      {},
	"jwks_viewer":         {},
	"jwks_operator":       {},
	"consent_viewer":      {},
	"consent_revoker":     {},
	"relationship_viewer": {},
	"audit_viewer":        {},
	"status_operator":     {},
}

func normalizeUserRole(role string) string {
	return domain.NormalizeRole(role)
}

func isDevConsoleRoleAllowed(role string) bool {
	r := normalizeUserRole(role)
	return r == domain.RoleSuperAdmin || r == domain.RoleUser
}

func isDevConsoleViewerRole(role string) bool {
	r := normalizeUserRole(role)
	return r == domain.RoleSuperAdmin || r == domain.RoleUser
}

func setCurrentProfileContext(c *fiber.Ctx, profile *domain.UserProfileResponse) {
	if profile == nil {
		return
	}
	c.Locals("user_profile", profile)
	if existingUserID, _ := c.Locals("user_id").(string); existingUserID == "" && profile.ID != "" {
		c.Locals("user_id", profile.ID)
	}
}

func (h *DevHandler) getCurrentProfile(c *fiber.Ctx) *domain.UserProfileResponse {
	if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
		setCurrentProfileContext(c, profile)
		return profile
	}
	if h.Auth != nil {
		enriched, err := h.Auth.GetEnrichedProfile(c)
		if err == nil && enriched != nil {
			setCurrentProfileContext(c, enriched)
			return enriched
		}
	}
	return nil
}

func tenantIDFromProfile(profile *domain.UserProfileResponse) string {
	if profile == nil || profile.TenantID == nil {
		return ""
	}
	return strings.TrimSpace(*profile.TenantID)
}

func addClientIDToSet(set map[string]struct{}, raw any) {
	switch value := raw.(type) {
	case string:
		for chunk := range strings.SplitSeq(value, ",") {
			id := strings.TrimSpace(chunk)
			if id != "" {
				set[id] = struct{}{}
			}
		}
	case []string:
		for _, item := range value {
			id := strings.TrimSpace(item)
			if id != "" {
				set[id] = struct{}{}
			}
		}
	case []any:
		for _, item := range value {
			if str, ok := item.(string); ok {
				id := strings.TrimSpace(str)
				if id != "" {
					set[id] = struct{}{}
				}
			}
		}
	}
}

func managedClientIDsFromProfile(profile *domain.UserProfileResponse) map[string]struct{} {
	ids := make(map[string]struct{})
	if profile == nil {
		return ids
	}

	if profile.RelyingPartyID != nil {
		if id := strings.TrimSpace(*profile.RelyingPartyID); id != "" {
			ids[id] = struct{}{}
		}
	}

	if profile.Metadata == nil {
		return ids
	}

	for _, key := range []string{
		"managed_client_ids",
		"managedClientIds",
		"relying_party_id",
		"relyingPartyId",
		"client_id",
		"clientId",
	} {
		if raw, ok := profile.Metadata[key]; ok {
			addClientIDToSet(ids, raw)
		}
	}

	return ids
}

func ketoSubjectFromProfile(profile *domain.UserProfileResponse) string {
	if profile == nil {
		return ""
	}
	id := strings.TrimSpace(profile.ID)
	if id == "" {
		return ""
	}
	return "User:" + id
}

func (h *DevHandler) checkProfileKetoPermission(c *fiber.Ctx, profile *domain.UserProfileResponse, namespace, object, relation string) (bool, error) {
	if profile == nil {
		return false, nil
	}
	if normalizeUserRole(profile.Role) == domain.RoleSuperAdmin {
		return true, nil
	}
	if h.Keto == nil {
		return false, nil
	}
	subject := ketoSubjectFromProfile(profile)
	if subject == "" {
		return false, nil
	}
	return h.Keto.CheckPermission(c.Context(), subject, namespace, object, relation)
}

func (h *DevHandler) canViewClientByPermit(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) bool {
	if profile == nil {
		return false
	}
	role := normalizeUserRole(profile.Role)
	if role == domain.RoleSuperAdmin {
		return true
	}
	if canAccessClientByLegacyScope(profile, summary) {
		return true
	}

	if h.hasDirectRelyingPartyOperatorRelation(c, profile, summary.ID) {
		return true
	}

	allowed, err := h.checkProfileKetoPermission(c, profile, "RelyingParty", summary.ID, "view")
	return err == nil && allowed
}

func (h *DevHandler) hasDirectRelyingPartyOperatorRelation(c *fiber.Ctx, profile *domain.UserProfileResponse, clientID string) bool {
	if h.Keto == nil || profile == nil {
		return false
	}
	subject := ketoSubjectFromProfile(profile)
	if subject == "" || strings.TrimSpace(clientID) == "" {
		return false
	}
	for relation := range allowedRelyingPartyOperatorRelations {
		tuples, err := h.Keto.ListRelations(c.Context(), "RelyingParty", clientID, relation, subject)
		if err == nil && len(tuples) > 0 {
			return true
		}
	}
	return false
}

func (h *DevHandler) canManageTenantClientsByPermit(c *fiber.Ctx, profile *domain.UserProfileResponse, tenantID string) bool {
	if strings.TrimSpace(tenantID) == "" {
		return false
	}
	allowed, err := h.checkProfileKetoPermission(c, profile, "Tenant", tenantID, "grant_dev_permissions")
	if err == nil && allowed {
		return true
	}
	return h.hasApprovedDeveloperRequest(c, profile, tenantID)
}

func (h *DevHandler) hasApprovedDeveloperRequest(c *fiber.Ctx, profile *domain.UserProfileResponse, tenantID string) bool {
	if h.DeveloperSvc == nil || profile == nil {
		return false
	}
	userID := strings.TrimSpace(profile.ID)
	tenantID = strings.TrimSpace(tenantID)
	if userID == "" || tenantID == "" {
		return false
	}
	status, err := h.DeveloperSvc.GetRequestStatus(c.Context(), userID, tenantID)
	if err != nil || status == nil {
		return false
	}
	return status.Status == domain.DeveloperRequestStatusApproved &&
		strings.TrimSpace(status.UserID) == userID &&
		strings.TrimSpace(status.TenantID) == tenantID
}

func (h *DevHandler) canOperateClientByPermit(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary, relation string) bool {
	allowed, err := h.checkProfileKetoPermission(c, profile, "RelyingParty", summary.ID, relation)
	return err == nil && allowed
}

func (h *DevHandler) canViewClientSecret(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) bool {
	if canAccessClientByLegacyScope(profile, summary) {
		return true
	}
	return h.canOperateClientByPermit(c, profile, summary, "view_secret")
}

func (h *DevHandler) canBypassPrivateClientRestriction(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary, relation string) bool {
	if h.canOperateClientByPermit(c, profile, summary, relation) {
		return true
	}
	allowed, err := h.checkAppManagerPermission(c)
	return err == nil && allowed
}

func (h *DevHandler) redactClientSecretUnlessAllowed(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) clientSummary {
	if summary.ClientSecret == "" {
		return summary
	}
	if h.canViewClientSecret(c, profile, summary) {
		return summary
	}
	summary.ClientSecret = ""
	return summary
}

func (h *DevHandler) canViewClientRelations(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) bool {
	if h.canOperateClientByPermit(c, profile, summary, "view_relationships") {
		return true
	}
	return canAccessClientByLegacyScope(profile, summary)
}

func (h *DevHandler) canManageClientRelations(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) bool {
	if profile == nil {
		return false
	}
	if normalizeUserRole(profile.Role) == domain.RoleSuperAdmin {
		return true
	}
	if h.canOperateClientByPermit(c, profile, summary, "manage") {
		return true
	}

	clientTenantID := resolveClientTenantID(summary)
	if clientTenantID != "" && h.canManageTenantClientsByPermit(c, profile, clientTenantID) {
		return true
	}

	return canAccessClientByLegacyScope(profile, summary)
}

func (h *DevHandler) auditClientIDsByPermit(c *fiber.Ctx, profile *domain.UserProfileResponse, clientFilter string) map[string]struct{} {
	ids := make(map[string]struct{})
	if profile == nil || h.Hydra == nil {
		return ids
	}
	if normalizeUserRole(profile.Role) == domain.RoleSuperAdmin {
		return ids
	}

	clientFilter = strings.TrimSpace(clientFilter)
	if clientFilter != "" {
		summary, err := h.loadClientSummary(c.Context(), clientFilter)
		if err == nil && h.canOperateClientByPermit(c, profile, summary, "audit_viewer") {
			ids[summary.ID] = struct{}{}
		}
		return ids
	}

	clients, err := h.Hydra.ListClients(c.Context(), 500, 0)
	if err != nil {
		slog.Warn("Failed to list clients for audit permission filtering", "error", err)
		return ids
	}
	for _, client := range clients {
		if isHiddenSystemClient(client) {
			continue
		}
		summary := h.mapClientSummary(client)
		if h.canOperateClientByPermit(c, profile, summary, "audit_viewer") {
			ids[summary.ID] = struct{}{}
		}
	}
	return ids
}

func mergeStringSets(dst map[string]struct{}, src map[string]struct{}) map[string]struct{} {
	if dst == nil {
		dst = make(map[string]struct{}, len(src))
	}
	for key := range src {
		dst[key] = struct{}{}
	}
	return dst
}

func shouldScopeDashboardToExplicitClients(role string) bool {
	switch normalizeUserRole(role) {
	case "rp_admin", domain.RoleUser:
		return true
	default:
		return false
	}
}

func clientIDSetFromSummaries(items []clientSummary) map[string]struct{} {
	ids := make(map[string]struct{}, len(items))
	for _, item := range items {
		id := strings.TrimSpace(item.ID)
		if id != "" {
			ids[id] = struct{}{}
		}
	}
	return ids
}

func canAccessClientByLegacyScope(profile *domain.UserProfileResponse, summary clientSummary) bool {
	if profile == nil {
		return false
	}

	role := normalizeUserRole(profile.Role)
	return role == domain.RoleSuperAdmin
}

func resolveClientTenantID(summary clientSummary) string {
	if summary.Metadata == nil {
		return ""
	}
	clientTenantID, _ := summary.Metadata["tenant_id"].(string)
	return strings.TrimSpace(clientTenantID)
}

func isRPAdminClientAllowed(profile *domain.UserProfileResponse, clientID string) bool {
	// [Deprecated] isRPAdminClientAllowed is now simplified.
	// Non-superadmins are already checked by tenant in canAccessClientByLegacyScope.
	role := normalizeUserRole(profileRole(profile))
	return role == domain.RoleSuperAdmin || role == domain.RoleUser
}

func manageableTenantKeysFromProfile(profile *domain.UserProfileResponse) map[string]struct{} {
	keys := make(map[string]struct{})
	if profile == nil {
		return keys
	}

	addKey := func(value string) {
		trimmed := strings.ToLower(strings.TrimSpace(value))
		if trimmed != "" {
			keys[trimmed] = struct{}{}
		}
	}

	if profile.TenantID != nil {
		addKey(*profile.TenantID)
	}
	for _, tenant := range profile.ManageableTenants {
		addKey(tenant.ID)
		addKey(tenant.Slug)
	}
	for _, tenant := range profile.JoinedTenants {
		addKey(tenant.ID)
		addKey(tenant.Slug)
	}

	return keys
}

func canAccessIdentityByTenant(profile *domain.UserProfileResponse, identity service.KratosIdentity) bool {
	if normalizeUserRole(profileRole(profile)) == domain.RoleSuperAdmin {
		return true
	}

	keys := manageableTenantKeysFromProfile(profile)
	if len(keys) == 0 {
		return false
	}

	for _, raw := range []string{
		extractTraitString(identity.Traits, "tenant_id"),
	} {
		if _, ok := keys[strings.ToLower(strings.TrimSpace(raw))]; ok {
			return true
		}
	}

	return false
}

func mapDevUserSummary(identity service.KratosIdentity) devUserSummary {
	traits := identity.Traits
	return devUserSummary{
		ID:      identity.ID,
		Name:    extractTraitString(traits, "name"),
		Email:   extractTraitString(traits, "email"),
		LoginID: resolvePasswordLoginID(traits),
	}
}

func profileRole(profile *domain.UserProfileResponse) string {
	if profile == nil {
		return ""
	}
	return strings.TrimSpace(profile.Role)
}

func isProtectedSystemClientID(clientID string) bool {
	_, ok := protectedSystemClientIDs[strings.TrimSpace(clientID)]
	return ok
}

func tenantAccessPolicyChanged(before, after map[string]any) bool {
	if clientTenantAccessRestricted(before) != clientTenantAccessRestricted(after) {
		return true
	}

	beforeAllowed := clientAllowedTenants(before)
	afterAllowed := clientAllowedTenants(after)
	if len(beforeAllowed) != len(afterAllowed) {
		return true
	}
	for i := range beforeAllowed {
		if beforeAllowed[i] != afterAllowed[i] {
			return true
		}
	}
	return false
}

func (h *DevHandler) revokeClientConsentsForPolicyChange(ctx context.Context, clientID string) error {
	return revokeClientConsentsForPolicyChange(ctx, h.Hydra, h.ConsentRepo, clientID)
}

func isProtectedSystemClient(client domain.HydraClient) bool {
	return isProtectedSystemClientID(client.ClientID)
}

func isReservedSystemClientAlias(client domain.HydraClient) bool {
	ownerID, reserved := reservedSystemClientOwnerID(client.ClientName)
	if !reserved {
		return false
	}
	return !strings.EqualFold(strings.TrimSpace(client.ClientID), ownerID)
}

func isHiddenSystemClient(client domain.HydraClient) bool {
	return isProtectedSystemClient(client) || isReservedSystemClientAlias(client)
}

func reservedSystemClientOwnerID(name string) (string, bool) {
	ownerID, ok := reservedSystemClientNames[strings.ToLower(strings.TrimSpace(name))]
	return ownerID, ok
}

func normalizeRelyingPartyRelation(relation string) string {
	return strings.TrimSpace(relation)
}

func isAllowedRelyingPartyOperatorRelation(relation string) bool {
	_, ok := allowedRelyingPartyOperatorRelations[normalizeRelyingPartyRelation(relation)]
	return ok
}

func normalizeClientRelationSubject(subject, userID string) string {
	subject = strings.TrimSpace(subject)
	if subject != "" {
		return subject
	}
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return ""
	}
	return "User:" + userID
}

func parseClientRelationSubject(subject string) (string, string) {
	subject = strings.TrimSpace(subject)
	if subject == "" {
		return "", ""
	}
	parts := strings.SplitN(subject, ":", 2)
	if len(parts) != 2 {
		return "", ""
	}
	return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
}

func validateClientRelationWriteInput(relation, subject string) error {
	relation = normalizeRelyingPartyRelation(relation)
	if !isAllowedRelyingPartyOperatorRelation(relation) {
		return fmt.Errorf("unsupported relation")
	}

	subjectType, subjectID := parseClientRelationSubject(subject)
	if subjectType != "User" || subjectID == "" || strings.Contains(subjectID, "#") {
		return fmt.Errorf("subject must be in User:<id> format")
	}

	return nil
}

func mapRelationTupleSummary(tuple service.RelationTuple, identity *service.KratosIdentity) clientRelationSummary {
	subjectType, subjectID := parseClientRelationSubject(tuple.SubjectID)
	summary := clientRelationSummary{
		Relation:    tuple.Relation,
		Subject:     tuple.SubjectID,
		SubjectType: subjectType,
		SubjectID:   subjectID,
	}
	if identity != nil {
		summary.UserName = extractTraitString(identity.Traits, "name")
		summary.UserEmail = extractTraitString(identity.Traits, "email")
		summary.UserLoginID = resolvePasswordLoginID(identity.Traits)
	}
	return summary
}

func dedupeRelationTuples(tuples []service.RelationTuple) []service.RelationTuple {
	if len(tuples) <= 1 {
		return tuples
	}

	seen := make(map[string]struct{}, len(tuples))
	deduped := make([]service.RelationTuple, 0, len(tuples))
	for _, tuple := range tuples {
		key := strings.TrimSpace(tuple.Relation) + "\x00" + strings.TrimSpace(tuple.SubjectID)
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		deduped = append(deduped, tuple)
	}
	return deduped
}

func (h *DevHandler) loadClientSummary(ctx context.Context, clientID string) (clientSummary, error) {
	clientID = strings.TrimSpace(clientID)
	if clientID == "" {
		return clientSummary{}, fmt.Errorf("client id is required")
	}
	client, err := h.Hydra.GetClient(ctx, clientID)
	if err != nil {
		return clientSummary{}, err
	}
	return h.mapClientSummary(*client), nil
}

func (h *DevHandler) getRelationRequestProfile(c *fiber.Ctx) *domain.UserProfileResponse {
	return h.getCurrentProfile(c)
}

func (h *DevHandler) SearchUsers(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}

	// Tightened Security: Only SuperAdmin bypasses the client-specific manage check.
	// Regular users (RoleUser) or RPAdmins must have the 'manage' permit for the requested clientId.
	if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
		clientID := strings.TrimSpace(c.Query("clientId"))
		if clientID == "" {
			return errorJSON(c, fiber.StatusBadRequest, "clientId is required for user search")
		}
		summary, err := h.loadClientSummary(c.Context(), clientID)
		if err != nil || !h.canManageClientRelations(c, profile, summary) {
			// canManageClientRelations checks for 'manage' permit in Keto.
			return errorJSON(c, fiber.StatusForbidden, "forbidden: manage permission required for user search")
		}
	}

	if h.KratosAdmin == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "kratos admin unavailable")
	}

	search := strings.ToLower(strings.TrimSpace(c.Query("search")))
	limit := c.QueryInt("limit", 10)
	if limit <= 0 {
		limit = 10
	}
	if limit > 20 {
		limit = 20
	}

	identities, err := h.KratosAdmin.ListIdentities(c.Context())
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	items := make([]devUserSummary, 0, limit)
	for _, identity := range identities {
		if !canAccessIdentityByTenant(profile, identity) {
			continue
		}

		summary := mapDevUserSummary(identity)
		if search != "" {
			matched := false
			for _, candidate := range []string{
				strings.ToLower(summary.Name),
				strings.ToLower(summary.Email),
				strings.ToLower(summary.LoginID),
			} {
				if candidate != "" && strings.Contains(candidate, search) {
					matched = true
					break
				}
			}
			if !matched {
				continue
			}
		}

		items = append(items, summary)
		if len(items) >= limit {
			break
		}
	}

	return c.JSON(devUserListResponse{Items: items})
}

func validateReservedSystemClientName(clientID, name string) error {
	ownerID, reserved := reservedSystemClientOwnerID(name)
	if !reserved {
		return nil
	}
	if strings.EqualFold(strings.TrimSpace(clientID), ownerID) {
		return nil
	}
	return fmt.Errorf("forbidden: reserved system client name")
}

func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
	profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
	if (!ok || profile == nil) && h.Auth != nil {
		enriched, err := h.Auth.GetEnrichedProfile(c)
		if err == nil && enriched != nil {
			profile = enriched
			ok = true
			setCurrentProfileContext(c, enriched)
		}
	}
	if ok && profile != nil {
		setCurrentProfileContext(c, profile)
		role := normalizeUserRole(profile.Role)
		if role == domain.RoleSuperAdmin {
			slog.Info("Dev private permission granted by super_admin role", "user_id", profile.ID)
			return true, nil
		}

		// Super Admin bypass by email
		if isAdminEmail(profile.Email) {
			slog.Info("Dev private permission granted by ADMIN_EMAIL match", "email", profile.Email)
			return true, nil
		}

		subject := strings.TrimSpace(profile.ID)
		if subject == "" && strings.TrimSpace(profile.Email) != "" && h.KratosAdmin != nil {
			resolved, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), strings.TrimSpace(profile.Email))
			if err == nil && strings.TrimSpace(resolved) != "" {
				subject = strings.TrimSpace(resolved)
			}
		}
		if subject == "" {
			slog.Warn("Dev private permission denied: missing subject in profile", "email", profile.Email)
			return false, nil
		}
		if h.Keto == nil {
			slog.Warn("Dev private permission denied: keto service unavailable")
			return false, nil
		}

		// Check with Keto: System:global#manage_all
		allowed, err := h.Keto.CheckPermission(c.Context(), "User:"+subject, "System", "global", "manage_all")
		if err != nil {
			// Fail closed for dev private endpoints: deny on permission backend error.
			slog.Warn("Dev private permission check failed; denying access", "subject", subject, "error", err)
			return false, nil
		}
		slog.Info("Dev private permission evaluated by Keto", "subject", subject, "allowed", allowed)

		return allowed, nil
	}

	authHeader := c.Get("Authorization")
	bearerToken := extractBearerToken(authHeader)
	tokenSubject, tokenEmail := extractAuthClaimsFromBearer(authHeader)
	tokenRole := ""

	// Fallback for OIDC access tokens that do not include full claims locally.
	if bearerToken != "" && (tokenSubject == "" || tokenEmail == "") {
		if info, err := h.fetchOIDCUserInfo(c.Context(), bearerToken); err == nil && info != nil {
			if tokenSubject == "" {
				tokenSubject = strings.TrimSpace(info.Sub)
			}
			if tokenEmail == "" {
				tokenEmail = strings.TrimSpace(info.Email)
			}
			tokenRole = normalizeUserRole(info.Role)
		} else if err != nil {
			slog.Warn("Dev private permission userinfo fallback failed", "error", err)
		}
	}
	tokenRole = normalizeUserRole(tokenRole)

	if tokenRole == domain.RoleSuperAdmin {
		slog.Info("Dev private permission granted by token role", "role", tokenRole)
		return true, nil
	}
	if isAdminEmail(tokenEmail) {
		slog.Info("Dev private permission granted by token email", "email", tokenEmail)
		return true, nil
	}

	// If subject is missing, resolve it from Kratos by identifier(email) so Keto checks can still run.
	if tokenSubject == "" && tokenEmail != "" && h.KratosAdmin != nil {
		resolved, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), tokenEmail)
		if err == nil && strings.TrimSpace(resolved) != "" {
			tokenSubject = strings.TrimSpace(resolved)
		}
	}
	if tokenSubject == "" {
		return false, nil
	}
	if h.Keto == nil {
		slog.Warn("Dev private permission denied: keto service unavailable")
		return false, nil
	}

	// Fallback: resolve role from Kratos identity traits when user_profile is not injected.
	if h.KratosAdmin != nil {
		identity, err := h.KratosAdmin.GetIdentity(c.Context(), tokenSubject)
		if err == nil && identity != nil {
			if rawRole, ok := identity.Traits["role"].(string); ok && normalizeUserRole(rawRole) == domain.RoleSuperAdmin {
				slog.Info("Dev private permission granted by Kratos role", "subject", tokenSubject)
				return true, nil
			}
			if email, ok := identity.Traits["email"].(string); ok && isAdminEmail(email) {
				slog.Info("Dev private permission granted by Kratos email", "subject", tokenSubject, "email", email)
				return true, nil
			}
		}
	}

	// Check with Keto: System:global#manage_all
	allowed, err := h.Keto.CheckPermission(c.Context(), "User:"+tokenSubject, "System", "global", "manage_all")
	if err != nil {
		// Fail closed for dev private endpoints: deny on permission backend error.
		slog.Warn("Dev private permission check failed; denying access", "subject", tokenSubject, "error", err)
		return false, nil
	}
	slog.Info("Dev private permission evaluated by Keto(subject)", "subject", tokenSubject, "allowed", allowed)

	return allowed, nil
}

func (h *DevHandler) listVisibleClientSummaries(
	c *fiber.Ctx,
	profile *domain.UserProfileResponse,
	limit int,
	offset int,
) ([]clientSummary, error) {
	if profile == nil {
		return nil, fiber.NewError(fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleRoleAllowed(role) {
		return nil, fiber.NewError(fiber.StatusForbidden, "forbidden")
	}

	userTenantID := tenantIDFromProfile(profile)
	isSuperAdmin := role == domain.RoleSuperAdmin

	isAppManager, err := h.checkAppManagerPermission(c)
	if err != nil {
		slog.Error("Failed to check app manager permission", "error", err)
	}

	clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
	if err != nil {
		return nil, err
	}

	items := make([]clientSummary, 0, len(clients))
	for _, client := range clients {
		if isHiddenSystemClient(client) {
			continue
		}

		summary := h.mapClientSummary(client)
		canViewByPermit := h.canViewClientByPermit(c, profile, summary)

		if summary.Type == "private" && !isAppManager && !canViewByPermit {
			continue
		}

		if !isSuperAdmin {
			clientTenantID, _ := summary.Metadata["tenant_id"].(string)
			if clientTenantID != userTenantID && !canViewByPermit {
				continue
			}
		}

		if !isSuperAdmin && !canAccessClientByLegacyScope(profile, summary) && !canViewByPermit {
			continue
		}

		items = append(items, h.redactClientSecretUnlessAllowed(c, profile, summary))
	}

	return items, nil
}

func (h *DevHandler) listAllVisibleClientSummaries(c *fiber.Ctx, profile *domain.UserProfileResponse) ([]clientSummary, error) {
	const pageSize = 500
	items := make([]clientSummary, 0)
	for offset := 0; ; offset += pageSize {
		page, err := h.listVisibleClientSummaries(c, profile, pageSize, offset)
		if err != nil {
			return nil, err
		}
		items = append(items, page...)
		if len(page) < pageSize {
			break
		}
	}
	return items, nil
}

func clientSummaryCursorKey(client clientSummary) (time.Time, string) {
	timestamp := time.Unix(0, 0).UTC()
	if client.CreatedAt != nil && !client.CreatedAt.IsZero() {
		timestamp = client.CreatedAt.UTC()
	}
	return timestamp, client.ID
}

func extractAuthClaimsFromBearer(authHeader string) (string, string) {
	authHeader = strings.TrimSpace(authHeader)
	if !strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
		return "", ""
	}

	token := strings.TrimSpace(authHeader[len("Bearer "):])
	if token == "" || strings.Count(token, ".") != 2 {
		return "", ""
	}

	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return "", ""
	}

	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		payload, err = base64.URLEncoding.DecodeString(parts[1])
		if err != nil {
			return "", ""
		}
	}

	var claims map[string]any
	if err := json.Unmarshal(payload, &claims); err != nil {
		return "", ""
	}
	sub := ""
	if sub, ok := claims["sub"].(string); ok {
		sub = strings.TrimSpace(sub)
	}
	email := ""
	if claimEmail, ok := claims["email"].(string); ok {
		email = strings.TrimSpace(claimEmail)
	}

	return sub, email
}

func isAdminEmail(email string) bool {
	adminEmail := strings.TrimSpace(os.Getenv("ADMIN_EMAIL"))
	return adminEmail != "" && strings.EqualFold(strings.TrimSpace(email), adminEmail)
}

func extractBearerToken(authHeader string) string {
	authHeader = strings.TrimSpace(authHeader)
	if !strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
		return ""
	}
	return strings.TrimSpace(authHeader[len("Bearer "):])
}

type oidcUserInfo struct {
	Sub      string `json:"sub"`
	Email    string `json:"email"`
	TenantID string `json:"tenant_id"`
	Role     string `json:"role"`
}

func (h *DevHandler) fetchOIDCUserInfo(ctx context.Context, accessToken string) (*oidcUserInfo, error) {
	if strings.TrimSpace(accessToken) == "" {
		return nil, fmt.Errorf("missing access token")
	}
	if h.Hydra == nil || strings.TrimSpace(h.Hydra.PublicURL) == "" {
		return nil, fmt.Errorf("hydra public url is not configured")
	}

	endpoint := strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("userinfo failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var payload map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, err
	}

	pick := func(keys ...string) string {
		for _, key := range keys {
			if raw, ok := payload[key]; ok {
				if value, ok := raw.(string); ok {
					value = strings.TrimSpace(value)
					if value != "" {
						return value
					}
				}
			}
		}
		return ""
	}

	return &oidcUserInfo{
		Sub:      pick("sub"),
		Email:    pick("email"),
		TenantID: pick("tenant_id", "tenantId"),
		Role:     pick("role"),
	}, nil
}

func (h *DevHandler) ListClients(c *fiber.Ctx) error {
	h.injectTenantContextFromHeader(c)
	limit := c.QueryInt("limit", 50)
	offset := c.QueryInt("offset", 0)
	cursorRaw := strings.TrimSpace(c.Query("cursor"))
	if limit <= 0 {
		limit = 50
	}
	if offset < 0 {
		offset = 0
	}

	profile := h.getCurrentProfile(c)
	allItems, err := h.listAllVisibleClientSummaries(c, profile)
	if err != nil {
		status := fiber.StatusInternalServerError
		errMsg := err.Error()
		var fiberErr *fiber.Error
		if errors.As(err, &fiberErr) {
			status = fiberErr.Code
			errMsg = fiberErr.Message
		} else if errors.Is(err, service.ErrHydraNotFound) {
			status = fiber.StatusNotFound
			errMsg = "clients not found"
		} else if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "dial tcp") {
			status = fiber.StatusServiceUnavailable
			errMsg = "Hydra service is unavailable. Please check if Ory Hydra is running."
		}
		return errorJSON(c, status, errMsg)
	}

	var items []clientSummary
	nextCursor := ""
	if cursorRaw != "" {
		ordered := append([]clientSummary(nil), allItems...)
		pagination.SortByKeyDesc(ordered, clientSummaryCursorKey)
		items, nextCursor, err = pagination.PageByCursor(ordered, limit, cursorRaw, clientSummaryCursorKey)
		if err != nil {
			return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
		}
		offset = 0
	} else {
		if offset > len(allItems) {
			offset = len(allItems)
		}
		end := min(offset+limit, len(allItems))
		items = allItems[offset:end]
		if len(allItems) > end && len(items) > 0 {
			lastTimestamp, lastID := clientSummaryCursorKey(items[len(items)-1])
			nextCursor = pagination.Encode(lastTimestamp, lastID)
		}
	}

	return c.JSON(clientListResponse{
		Items:      items,
		Limit:      limit,
		Offset:     offset,
		Cursor:     cursorRaw,
		NextCursor: nextCursor,
	})
}

func (h *DevHandler) ListClientRelations(c *fiber.Ctx) error {
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	profile := h.getRelationRequestProfile(c)
	summary, err := h.loadClientSummary(c.Context(), clientID)
	if err != nil {
		return errorJSON(c, fiber.StatusNotFound, "client not found")
	}

	if !h.canViewClientRelations(c, profile, summary) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if h.Keto == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "keto service unavailable")
	}

	items := make([]clientRelationSummary, 0)
	for relation := range allowedRelyingPartyOperatorRelations {
		tuples, err := h.Keto.ListRelations(c.Context(), "RelyingParty", clientID, relation, "")
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
		tuples = dedupeRelationTuples(tuples)
		for _, tuple := range tuples {
			var identity *service.KratosIdentity
			if tuple.SubjectID != "" && h.KratosAdmin != nil {
				_, subjectID := parseClientRelationSubject(tuple.SubjectID)
				if subjectID != "" {
					identity, _ = h.KratosAdmin.GetIdentity(c.Context(), subjectID)
				}
			}
			items = append(items, mapRelationTupleSummary(tuple, identity))
		}
	}

	return c.JSON(clientRelationListResponse{Items: items})
}

func (h *DevHandler) AddClientRelation(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	var req clientRelationUpsertRequest
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	req.Relation = normalizeRelyingPartyRelation(req.Relation)
	req.Subject = normalizeClientRelationSubject(req.Subject, req.UserID)
	if err := validateClientRelationWriteInput(req.Relation, req.Subject); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}

	profile := h.getRelationRequestProfile(c)
	summary, err := h.loadClientSummary(c.Context(), clientID)
	if err != nil {
		return errorJSON(c, fiber.StatusNotFound, "client not found")
	}
	if !h.canManageClientRelations(c, profile, summary) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if h.Keto == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "keto service unavailable")
	}
	if h.KetoOutbox == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "keto outbox unavailable")
	}

	existing, err := h.Keto.ListRelations(c.Context(), "RelyingParty", clientID, req.Relation, req.Subject)
	if err == nil && len(existing) > 0 {
		return errorJSON(c, fiber.StatusConflict, "relation already exists")
	}

	if err := h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
		Namespace: "RelyingParty",
		Object:    clientID,
		Relation:  req.Relation,
		Subject:   req.Subject,
		Action:    domain.KetoOutboxActionCreate,
	}); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "ADD_RELATION",
		"target_id": clientID,
		"tenant_id": tenantID,
		"after": map[string]any{
			"relation": req.Relation,
			"subject":  req.Subject,
		},
	})

	return c.Status(fiber.StatusCreated).JSON(mapRelationTupleSummary(service.RelationTuple{
		Object:    clientID,
		Relation:  req.Relation,
		SubjectID: req.Subject,
	}, nil))
}

func (h *DevHandler) RemoveClientRelation(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	relation := normalizeRelyingPartyRelation(c.Query("relation"))
	subject := normalizeClientRelationSubject(c.Query("subject"), c.Query("userId"))
	if err := validateClientRelationWriteInput(relation, subject); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}

	profile := h.getRelationRequestProfile(c)
	summary, err := h.loadClientSummary(c.Context(), clientID)
	if err != nil {
		return errorJSON(c, fiber.StatusNotFound, "client not found")
	}
	if !h.canManageClientRelations(c, profile, summary) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if h.KetoOutbox == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "keto outbox unavailable")
	}

	if err := h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
		Namespace: "RelyingParty",
		Object:    clientID,
		Relation:  relation,
		Subject:   subject,
		Action:    domain.KetoOutboxActionDelete,
	}); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "REMOVE_RELATION",
		"target_id": clientID,
		"tenant_id": tenantID,
		"before": map[string]any{
			"relation": relation,
			"subject":  subject,
		},
	})

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *DevHandler) GetClient(c *fiber.Ctx) error {
	h.injectTenantContextFromHeader(c)
	clientID := c.Params("id")
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	client, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	if isHiddenSystemClient(*client) {
		return errorJSON(c, fiber.StatusNotFound, "client not found")
	}

	summary := h.mapClientSummary(*client)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	if !canAccessClientByLegacyScope(profile, summary) && !h.canViewClientByPermit(c, profile, summary) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	// Check permission for private clients
	if summary.Type == "private" && !h.canViewClientByPermit(c, profile, summary) {
		isAppManager, err := h.checkAppManagerPermission(c)
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
		}
		if !isAppManager {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
		}
	}

	cacheState, _ := h.publicHeadlessJWKSCacheState(summary.ID)
	summary = h.redactClientSecretUnlessAllowed(c, profile, summary)

	return c.JSON(clientDetailResponse{
		Client:            summary,
		HeadlessJWKSCache: cacheState,
		Endpoints: clientEndpoints{
			Discovery:     strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
			Issuer:        h.Hydra.PublicURL,
			Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
			Token:         strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
			UserInfo:      strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
		},
	})
}

func (h *DevHandler) publicHeadlessJWKSCacheState(clientID string) (*domain.HeadlessJWKSCacheState, error) {
	if h.HeadlessJWKS == nil {
		h.HeadlessJWKS = service.NewHeadlessJWKSCacheService(h.Redis, nil)
	}
	if h.HeadlessJWKS == nil {
		return nil, nil
	}
	return h.HeadlessJWKS.PublicState(clientID)
}

func (h *DevHandler) GetRPUserMetadata(c *fiber.Ctx) error {
	clientID := strings.TrimSpace(c.Params("id"))
	userID := strings.TrimSpace(c.Params("userId"))
	if clientID == "" || userID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id and user id are required")
	}
	if h.RPUserMetadataRepo == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "rp user metadata repository unavailable")
	}

	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}

	summary, err := h.loadClientSummary(c.Context(), clientID)
	if err != nil {
		return errorJSON(c, fiber.StatusNotFound, "client not found")
	}
	if !h.canViewClientByPermit(c, profile, summary) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permission to view client metadata")
	}

	metadata, err := h.RPUserMetadataRepo.Get(c.Context(), clientID, userID)
	if err != nil {
		return c.JSON(fiber.Map{
			"clientId": clientID,
			"userId":   userID,
			"metadata": domain.JSONMap{},
		})
	}

	return c.JSON(metadata)
}

func (h *DevHandler) UpsertRPUserMetadata(c *fiber.Ctx) error {
	clientID := strings.TrimSpace(c.Params("id"))
	userID := strings.TrimSpace(c.Params("userId"))
	if clientID == "" || userID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id and user id are required")
	}
	if h.RPUserMetadataRepo == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "rp user metadata repository unavailable")
	}

	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}

	summary, err := h.loadClientSummary(c.Context(), clientID)
	if err != nil {
		return errorJSON(c, fiber.StatusNotFound, "client not found")
	}
	if !h.canManageClientRelations(c, profile, summary) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permission to update client metadata")
	}

	var req struct {
		Metadata map[string]any `json:"metadata"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}
	if req.Metadata == nil {
		req.Metadata = map[string]any{}
	}
	normalizedMetadata, err := normalizeRPUserMetadataForClient(req.Metadata, summary.Metadata)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}

	row := &domain.RPUserMetadata{
		ClientID: clientID,
		UserID:   userID,
		Metadata: normalizedMetadata,
	}
	if err := h.RPUserMetadataRepo.Upsert(c.Context(), row); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if err := h.syncRPUserMetadataToKratos(c.Context(), userID, clientID, normalizedMetadata); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.JSON(row)
}

func (h *DevHandler) syncRPUserMetadataToKratos(ctx context.Context, userID string, clientID string, metadata domain.JSONMap) error {
	if h == nil || h.KratosAdmin == nil {
		return nil
	}
	identity, err := h.KratosAdmin.GetIdentity(ctx, userID)
	if err != nil {
		return fmt.Errorf("failed to load kratos identity for rp user metadata: %w", err)
	}
	if identity == nil {
		return errors.New("kratos identity not found for rp user metadata")
	}
	traits := identity.Traits
	if traits == nil {
		traits = map[string]any{}
	}
	rawRPClaims, _ := traits["rp_custom_claims"].(map[string]any)
	if rawRPClaims == nil {
		rawRPClaims = map[string]any{}
	}
	rawRPClaims[clientID] = metadata
	traits["rp_custom_claims"] = rawRPClaims
	_, err = h.KratosAdmin.UpdateIdentity(ctx, identity.ID, traits, identity.State)
	if err != nil {
		return fmt.Errorf("failed to update kratos rp user metadata: %w", err)
	}
	return nil
}

type rpUserMetadataClaimSchema struct {
	Key             string
	ValueType       string
	ReadPermission  string
	WritePermission string
}

func normalizeCustomClaimPermission(value any) string {
	permission := strings.TrimSpace(readInterfaceString(value, ""))
	switch permission {
	case "user_and_admin":
		return "user_and_admin"
	default:
		return "admin_only"
	}
}

func normalizeCustomClaimPermissions(value any, fallbackRead string, fallbackWrite string) map[string]any {
	var record map[string]any
	switch typed := value.(type) {
	case map[string]any:
		record = typed
	case domain.JSONMap:
		record = map[string]any(typed)
	}
	return map[string]any{
		"readPermission":  normalizeCustomClaimPermission(readMapValueOrFallback(record, "readPermission", fallbackRead)),
		"writePermission": normalizeCustomClaimPermission(readMapValueOrFallback(record, "writePermission", fallbackWrite)),
	}
}

func readMapValueOrFallback(values map[string]any, key string, fallback string) any {
	if values == nil {
		return fallback
	}
	if value, ok := values[key]; ok {
		return value
	}
	return fallback
}

func normalizeRPUserMetadataForClient(metadata map[string]any, clientMetadata map[string]any) (domain.JSONMap, error) {
	schemas, err := rpUserMetadataClaimSchemas(clientMetadata)
	if err != nil {
		return nil, err
	}

	normalized := domain.JSONMap{}
	for rawKey, rawValue := range metadata {
		key := strings.TrimSpace(rawKey)
		if key == "" || isEmptyRPUserMetadataValue(rawValue) {
			continue
		}
		if strings.HasSuffix(key, "_permissions") {
			claimKey := strings.TrimSuffix(key, "_permissions")
			schema, ok := schemas[claimKey]
			if !ok {
				return nil, fmt.Errorf("rp user metadata claim is not configured: %s", claimKey)
			}
			normalized[key] = normalizeCustomClaimPermissions(rawValue, schema.ReadPermission, schema.WritePermission)
			continue
		}
		schema, ok := schemas[key]
		if !ok {
			return nil, fmt.Errorf("rp user metadata claim is not configured: %s", key)
		}
		textValue, err := stringifyRPUserMetadataValue(rawValue)
		if err != nil {
			return nil, fmt.Errorf("rp user metadata %s is invalid: %w", key, err)
		}
		parsed, err := parseConfiguredClaimValue(textValue, schema.ValueType)
		if err != nil {
			return nil, fmt.Errorf("rp user metadata %s is invalid: %w", key, err)
		}
		normalized[key] = parsed
		permissionKey := key + "_permissions"
		if _, exists := normalized[permissionKey]; !exists {
			normalized[permissionKey] = map[string]any{
				"readPermission":  schema.ReadPermission,
				"writePermission": schema.WritePermission,
			}
		}
	}
	return normalized, nil
}

func rpUserMetadataClaimSchemas(clientMetadata map[string]any) (map[string]rpUserMetadataClaimSchema, error) {
	rawClaims, ok := clientMetadata[domain.MetadataIDTokenClaims]
	if !ok || rawClaims == nil {
		return map[string]rpUserMetadataClaimSchema{}, nil
	}

	claims, err := normalizeIDTokenClaimsForDevConsole(rawClaims)
	if err != nil {
		return nil, err
	}
	schemas := make(map[string]rpUserMetadataClaimSchema, len(claims))
	for _, claim := range claims {
		if claim.Namespace != "rp_claims" {
			continue
		}
		schemas[claim.Key] = rpUserMetadataClaimSchema{
			Key:             claim.Key,
			ValueType:       claim.ValueType,
			ReadPermission:  claim.ReadPermission,
			WritePermission: claim.WritePermission,
		}
	}
	return schemas, nil
}

func isEmptyRPUserMetadataValue(value any) bool {
	if value == nil {
		return true
	}
	if text, ok := value.(string); ok {
		return strings.TrimSpace(text) == ""
	}
	return false
}

func stringifyRPUserMetadataValue(value any) (string, error) {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed), nil
	case bool:
		return strconv.FormatBool(typed), nil
	case float64:
		return strconv.FormatFloat(typed, 'f', -1, 64), nil
	case float32:
		return strconv.FormatFloat(float64(typed), 'f', -1, 32), nil
	case int:
		return strconv.Itoa(typed), nil
	case int64:
		return strconv.FormatInt(typed, 10), nil
	case int32:
		return strconv.FormatInt(int64(typed), 10), nil
	case json.Number:
		return typed.String(), nil
	default:
		data, err := json.Marshal(value)
		if err != nil {
			return "", err
		}
		return string(data), nil
	}
}

func (h *DevHandler) syncHeadlessJWKSCache(ctx context.Context, client domain.HydraClient, reason string) {
	if h.HeadlessJWKS == nil {
		h.HeadlessJWKS = service.NewHeadlessJWKSCacheService(h.Redis, nil)
	}
	if h.HeadlessJWKS == nil {
		return
	}
	if !client.IsHeadlessLoginEnabled() {
		_ = h.HeadlessJWKS.DeleteState(client.ClientID)
		return
	}
	if _, err := h.HeadlessJWKS.ForceRefresh(ctx, client, reason); err != nil {
		slog.Warn("failed to refresh headless jwks cache after client save", "clientID", client.ClientID, "reason", reason, "error", err)
	}
}

func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := c.Params("id")
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	var req struct {
		Status string `json:"status"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	status := strings.ToLower(strings.TrimSpace(req.Status))
	if status != "active" && status != "inactive" {
		return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
	}

	// [Security] Check permission before patching
	current, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	if isHiddenSystemClient(*current) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
	}

	summary := h.mapClientSummary(*current)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	canChangeStatusByPermit := h.canOperateClientByPermit(c, profile, summary, "change_status")
	canEditConfigByPermit := h.canOperateClientByPermit(c, profile, summary, "edit_config")
	canChangeStatus := canChangeStatusByPermit || canEditConfigByPermit
	if !canAccessClientByLegacyScope(profile, summary) && !canChangeStatus {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	if summary.Type == "private" &&
		!h.canBypassPrivateClientRestriction(c, profile, summary, "change_status") &&
		!h.canBypassPrivateClientRestriction(c, profile, summary, "edit_config") {
		if !canChangeStatus {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
		}
	}

	beforeStatus := summary.Status
	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "UPDATE_CLIENT_STATUS",
		"target_id": clientID,
		"tenant_id": tenantID,
		"before": map[string]any{
			"status": beforeStatus,
		},
		"after": map[string]any{
			"status": status,
		},
	})

	updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	updatedSummary := h.mapClientSummary(*updated)
	updatedSummary = h.redactClientSecretUnlessAllowed(c, profile, updatedSummary)
	cacheState, _ := h.publicHeadlessJWKSCacheState(updatedSummary.ID)
	return c.JSON(clientDetailResponse{
		Client:            updatedSummary,
		HeadlessJWKSCache: cacheState,
		Endpoints: clientEndpoints{
			Discovery:     strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
			Issuer:        h.Hydra.PublicURL,
			Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
			Token:         strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
			UserInfo:      strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
		},
	})
}

func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleRoleAllowed(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if tenantID == "" && profile.TenantID != nil {
		tenantID = *profile.TenantID
	}
	if (role == "rp_admin" || role == domain.RoleUser) && !h.canManageTenantClientsByPermit(c, profile, tenantID) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant grant permission is required")
	}

	var req clientUpsertRequest
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	clientID := strings.TrimSpace(valueOr(req.ID, ""))
	if clientID == "" {
		clientID = uuid.NewString()
	}
	if isProtectedSystemClientID(clientID) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: reserved system client id")
	}

	name := strings.TrimSpace(valueOr(req.Name, ""))
	if name == "" {
		name = clientID
	}
	if err := validateReservedSystemClientName(clientID, name); err != nil {
		return errorJSON(c, fiber.StatusForbidden, err.Error())
	}

	redirectURIs := derefSlice(req.RedirectURIs, nil)
	if len(redirectURIs) == 0 {
		return errorJSON(c, fiber.StatusBadRequest, "redirectUris is required")
	}

	scopes := derefSlice(req.Scopes, defaultClientScopes())
	grantTypes := derefSlice(req.GrantTypes, defaultGrantTypes())
	responseTypes := derefSlice(req.ResponseTypes, defaultResponseTypes())

	clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "private")))
	if clientType != "pkce" && clientType != "private" {
		return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private")
	}

	status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
	if status != "active" && status != "inactive" {
		return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
	}
	if requestIncludesInlineHeadlessJWKS(req) {
		return errorJSON(c, fiber.StatusBadRequest, "headless login supports jwksUri only; inline jwks is not supported")
	}

	metadata := mergeMetadata(nil, req.Metadata)
	if metadata == nil {
		metadata = map[string]any{}
	}

	// [Tenant Isolation] Record owner information
	if profile != nil {
		metadata["user_id"] = profile.ID
		if tenantID == "" && profile.TenantID != nil {
			tenantID = *profile.TenantID
		}
	}

	if tenantID != "" {
		metadata["tenant_id"] = tenantID
	}
	var err error
	metadata["status"] = status
	metadata["created_at"] = time.Now().Format(time.RFC3339)
	backchannelLogoutURI := strings.TrimSpace(valueOr(req.BackchannelLogoutURI, ""))
	backchannelLogoutSessionRequired := valueOrBool(req.BackchannelLogoutSessionRequired, false)
	metadata, err = normalizeBackchannelLogoutMetadata(metadata, backchannelLogoutURI, backchannelLogoutSessionRequired)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	metadata, err = normalizeClientTenantAccessMetadata(metadata)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	metadata, err = normalizeIDTokenClaimsMetadata(metadata)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	clientType = normalizeClientTypeForHeadless(clientType, metadata)

	// [Security] Check permission for private clients
	if clientType == "private" {
		isAppManager, err := h.checkAppManagerPermission(c)
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
		}
		if !isAppManager && !h.canManageTenantClientsByPermit(c, profile, tenantID) {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions to create private client")
		}
	}

	tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
	if tokenAuthMethod == "" {
		if clientType == "pkce" {
			tokenAuthMethod = "none"
		} else {
			tokenAuthMethod = "client_secret_basic"
		}
	}
	if err := validateHeadlessClientInput(valueOr(req.JwksUri, ""), req.Jwks, metadata); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	tokenAuthMethod, jwksURI, jwks, metadata := normalizeHeadlessClientConfig(
		tokenAuthMethod,
		valueOr(req.JwksUri, ""),
		req.Jwks,
		metadata,
	)

	clientReq := domain.HydraClient{
		ClientID:                         clientID,
		ClientName:                       name,
		RedirectURIs:                     redirectURIs,
		GrantTypes:                       grantTypes,
		ResponseTypes:                    responseTypes,
		Scope:                            strings.Join(scopes, " "),
		TokenEndpointAuthMethod:          tokenAuthMethod,
		SkipConsent:                      new(valueOrBool(req.SkipConsent, true)),
		JWKSUri:                          jwksURI,
		JWKS:                             jwks,
		BackChannelLogoutURI:             backchannelLogoutURI,
		BackChannelLogoutSessionRequired: new(backchannelLogoutSessionRequired),
		Metadata:                         metadata,
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "CREATE_CLIENT",
		"target_id": clientID,
		"tenant_id": tenantID,
		"after": map[string]any{
			"type":               clientType,
			"status":             status,
			"redirect_uri_count": len(redirectURIs),
			"scope_count":        len(scopes),
		},
	})

	created, err := h.Hydra.CreateClient(c.Context(), clientReq)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	h.syncHeadlessJWKSCache(c.Context(), *created, "client_create")

	// [New] Automatically grant admin permission to the creator in Keto
	if h.KetoOutbox != nil && profile != nil {
		subject := "User:" + profile.ID
		if err := h.grantCreatorAdminRelation(c, created.ClientID, subject); err != nil {
			slog.Warn("failed to grant automatic admin permission to creator", "clientID", created.ClientID, "userID", profile.ID, "error", err)
		} else {
			slog.Info("granted automatic admin permission to creator", "clientID", created.ClientID, "userID", profile.ID)
		}
	}

	// Store secret in metadata for later retrieval
	if created.ClientSecret != "" {
		// 1. Store in PostgreSQL (Source of Truth)
		if h.SecretRepo != nil {
			_ = h.SecretRepo.Upsert(c.Context(), created.ClientID, created.ClientSecret)
		}

		// 2. Also store in Redis (Cache)
		if h.Redis != nil {
			_ = h.Redis.Set("client_secret:"+created.ClientID, created.ClientSecret, 0)
		}
	}

	h.setAuditDetailsExtra(c, map[string]any{"target_id": created.ClientID})

	summary := h.mapClientSummary(*created)
	cacheState, _ := h.publicHeadlessJWKSCacheState(summary.ID)
	return c.Status(fiber.StatusCreated).JSON(clientDetailResponse{
		Client:            summary,
		HeadlessJWKSCache: cacheState,
		Endpoints: clientEndpoints{
			Discovery:     strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
			Issuer:        h.Hydra.PublicURL,
			Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
			Token:         strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
			UserInfo:      strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
		},
	})
}

func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	var req clientUpsertRequest
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	current, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	if isHiddenSystemClient(*current) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
	}

	currentSummary := h.mapClientSummary(*current)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	isSuperAdmin := role == domain.RoleSuperAdmin
	if !isSuperAdmin && !h.canOperateClientByPermit(c, profile, currentSummary, "edit_config") {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: edit_config permission is required")
	}

	clientType := ""
	if req.Type != nil {
		clientType = strings.ToLower(strings.TrimSpace(*req.Type))
		if clientType != "pkce" && clientType != "private" {
			return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private")
		}
	}

	// [Security] Check permission for private clients (both current and new type)
	if !isSuperAdmin && (currentSummary.Type == "private" || clientType == "private") {
		if !h.canBypassPrivateClientRestriction(c, profile, currentSummary, "edit_config") {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
		}
	}

	status := ""
	if req.Status != nil {
		status = strings.ToLower(strings.TrimSpace(*req.Status))
		if status != "active" && status != "inactive" {
			return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
		}
	}

	tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
	if tokenAuthMethod == "" && clientType != "" {
		if clientType == "pkce" {
			tokenAuthMethod = "none"
		} else {
			tokenAuthMethod = "client_secret_basic"
		}
	}

	if req.RedirectURIs != nil && len(*req.RedirectURIs) == 0 {
		return errorJSON(c, fiber.StatusBadRequest, "redirectUris cannot be empty")
	}
	if requestIncludesInlineHeadlessJWKS(req) {
		return errorJSON(c, fiber.StatusBadRequest, "headless login supports jwksUri only; inline jwks is not supported")
	}

	metadata := mergeMetadata(current.Metadata, req.Metadata)
	if status != "" {
		if metadata == nil {
			metadata = map[string]any{}
		}
		metadata["status"] = status
	}
	resolvedBackchannelLogoutURI := valueOr(req.BackchannelLogoutURI, current.BackchannelLogoutURI())
	resolvedBackchannelLogoutSessionRequired := valueOrBool(req.BackchannelLogoutSessionRequired, current.BackchannelLogoutSessionRequiredValue())
	metadata, err = normalizeBackchannelLogoutMetadata(
		metadata,
		resolvedBackchannelLogoutURI,
		resolvedBackchannelLogoutSessionRequired,
	)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	metadata, err = normalizeClientTenantAccessMetadata(metadata)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	metadata, err = normalizeIDTokenClaimsMetadata(metadata)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	resolvedClientType := currentSummary.Type
	if clientType != "" {
		resolvedClientType = clientType
	}
	resolvedClientType = normalizeClientTypeForHeadless(resolvedClientType, metadata)
	resolvedTokenAuthMethod := resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod)
	resolvedJWKSURI := valueOr(req.JwksUri, current.JWKSUri)
	resolvedJWKS := req.Jwks
	if req.Jwks == nil {
		if readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
			resolvedJWKS = nil
		} else {
			resolvedJWKS = current.JWKS
		}
	}
	if err := validateHeadlessClientInput(resolvedJWKSURI, resolvedJWKS, metadata); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	resolvedTokenAuthMethod, resolvedJWKSURI, resolvedJWKS, metadata = normalizeHeadlessClientConfig(
		resolvedTokenAuthMethod,
		resolvedJWKSURI,
		resolvedJWKS,
		metadata,
	)
	resolvedSkipConsent := valueOrBool(req.SkipConsent, valueOrBool(current.SkipConsent, true))

	updated := domain.HydraClient{
		ClientID:                         current.ClientID,
		ClientName:                       valueOr(req.Name, current.ClientName),
		RedirectURIs:                     derefSlice(req.RedirectURIs, current.RedirectURIs),
		GrantTypes:                       derefSlice(req.GrantTypes, current.GrantTypes),
		ResponseTypes:                    derefSlice(req.ResponseTypes, current.ResponseTypes),
		Scope:                            buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
		TokenEndpointAuthMethod:          resolvedTokenAuthMethod,
		SkipConsent:                      new(resolvedSkipConsent),
		JWKSUri:                          resolvedJWKSURI,
		JWKS:                             resolvedJWKS,
		BackChannelLogoutURI:             strings.TrimSpace(resolvedBackchannelLogoutURI),
		BackChannelLogoutSessionRequired: new(resolvedBackchannelLogoutSessionRequired),
		Metadata:                         metadata,
	}
	if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
		return errorJSON(c, fiber.StatusForbidden, err.Error())
	}
	tenantPolicyChanged := tenantAccessPolicyChanged(current.Metadata, updated.Metadata)

	beforeScopes := strings.Fields(current.Scope)
	afterScopes := strings.Fields(updated.Scope)
	beforeAllowedTenants := readStringSliceMetadata(current.Metadata, "allowed_tenants")
	afterAllowedTenants := readStringSliceMetadata(metadata, "allowed_tenants")
	beforeIDTokenClaims := readMetadataValueOrNil(current.Metadata, "id_token_claims")
	afterIDTokenClaims := readMetadataValueOrNil(metadata, "id_token_claims")

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "UPDATE_CLIENT",
		"target_id": clientID,
		"tenant_id": tenantID,
		"before": map[string]any{
			"name":                                currentSummary.Name,
			"type":                                currentSummary.Type,
			"status":                              currentSummary.Status,
			"scopes":                              beforeScopes,
			"tenant_access_restricted":            readMetadataBoolValue(current.Metadata, "tenant_access_restricted"),
			"allowed_tenants":                     beforeAllowedTenants,
			"id_token_claims":                     beforeIDTokenClaims,
			"token_endpoint_auth_method":          current.TokenEndpointAuthMethod,
			"jwks_uri":                            current.JWKSUri,
			"backchannel_logout_uri":              strings.TrimSpace(current.BackchannelLogoutURI()),
			"backchannel_logout_session_required": current.BackchannelLogoutSessionRequiredValue(),
			"headless_login_enabled":              readMetadataBoolValue(current.Metadata, domain.MetadataHeadlessLoginEnabled),
			"headless_token_endpoint_auth_method": readMetadataStringValue(current.Metadata, domain.MetadataHeadlessTokenEndpointAuthMethod),
			"headless_jwks_uri":                   readMetadataStringValue(current.Metadata, domain.MetadataHeadlessJWKSURI),
		},
		"after": map[string]any{
			"name":                                strings.TrimSpace(updated.ClientName),
			"type":                                clientTypeOrDefault(updated.TokenEndpointAuthMethod),
			"status":                              resolveStatusFromMetadata(updated.Metadata),
			"scopes":                              afterScopes,
			"tenant_access_restricted":            readMetadataBoolValue(metadata, "tenant_access_restricted"),
			"allowed_tenants":                     afterAllowedTenants,
			"id_token_claims":                     afterIDTokenClaims,
			"token_endpoint_auth_method":          resolvedTokenAuthMethod,
			"jwks_uri":                            resolvedJWKSURI,
			"backchannel_logout_uri":              strings.TrimSpace(resolvedBackchannelLogoutURI),
			"backchannel_logout_session_required": resolvedBackchannelLogoutSessionRequired,
			"headless_login_enabled":              readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled),
			"headless_token_endpoint_auth_method": readMetadataStringValue(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod),
			"headless_jwks_uri":                   readMetadataStringValue(metadata, domain.MetadataHeadlessJWKSURI),
		},
	})

	updatedClient, err := h.Hydra.UpdateClient(c.Context(), clientID, updated)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if tenantPolicyChanged {
		if err := h.revokeClientConsentsForPolicyChange(c.Context(), clientID); err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, "failed to revoke existing consents after tenant policy update: "+err.Error())
		}
	}
	h.syncHeadlessJWKSCache(c.Context(), *updatedClient, "client_update")

	if updatedClient.ClientSecret != "" {
		if h.SecretRepo != nil {
			_ = h.SecretRepo.Upsert(c.Context(), updatedClient.ClientID, updatedClient.ClientSecret)
		}
		if h.Redis != nil {
			_ = h.Redis.Set("client_secret:"+updatedClient.ClientID, updatedClient.ClientSecret, 0)
		}
	}

	summary := h.mapClientSummary(*updatedClient)
	cacheState, _ := h.publicHeadlessJWKSCacheState(summary.ID)
	return c.JSON(clientDetailResponse{
		Client:            summary,
		HeadlessJWKSCache: cacheState,
		Endpoints: clientEndpoints{
			Discovery:     strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
			Issuer:        h.Hydra.PublicURL,
			Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
			Token:         strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
			UserInfo:      strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
		},
	})
}

func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	current, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	if isHiddenSystemClient(*current) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
	}

	summary := h.mapClientSummary(*current)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	if !canAccessClientByLegacyScope(profile, summary) && !h.canOperateClientByPermit(c, profile, summary, "manage") {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	// [Security] Check permission for private clients
	if summary.Type == "private" {
		if !h.canBypassPrivateClientRestriction(c, profile, summary, "manage") {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
		}
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "DELETE_CLIENT",
		"target_id": clientID,
		"tenant_id": tenantID,
	})

	if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	// 1. Clean up PostgreSQL
	if h.SecretRepo != nil {
		_ = h.SecretRepo.Delete(c.Context(), clientID)
	}

	// 2. Clean up Redis
	if h.Redis != nil {
		_ = h.Redis.Delete("client_secret:" + clientID)
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
	h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Query("client_id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client_id is required")
	}

	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	client, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	summary := h.mapClientSummary(*client)
	canViewConsentsByPermit := h.canOperateClientByPermit(c, profile, summary, "view_consents")
	if !canAccessClientByLegacyScope(profile, summary) && !canViewConsentsByPermit {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	subject := strings.TrimSpace(c.Query("subject"))
	limit := c.QueryInt("limit", 50)
	offset := c.QueryInt("offset", 0)
	cursorRaw := strings.TrimSpace(c.Query("cursor"))
	if limit <= 0 {
		limit = 50
	}
	if offset < 0 {
		offset = 0
	}

	// [Isolation] Get admin tenant ID from locals or header
	adminTenantID := ""
	if profile != nil {
		if role != domain.RoleSuperAdmin && !canViewConsentsByPermit && profile.TenantID != nil {
			adminTenantID = *profile.TenantID
		}
	}
	if adminTenantID == "" {
		adminTenantID = c.Get("X-Tenant-ID")
	}

	statusFilter := strings.ToLower(strings.TrimSpace(c.Query("status")))

	var consents []domain.ClientConsentWithTenantInfo
	var total int64

	if subject != "" {
		// Resolve subject if it's email/name (Legacy support)
		if _, err := uuid.Parse(subject); err != nil {
			resolved, _ := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), subject)
			if resolved != "" {
				subject = resolved
			}
		}
	}

	queryLimit := limit
	queryOffset := offset
	if cursorRaw != "" || subject != "" || (statusFilter != "" && statusFilter != "all") {
		queryLimit = 10000
		queryOffset = 0
	}
	if adminTenantID != "" {
		consents, total, err = h.ConsentRepo.ListByTenant(c.Context(), clientID, adminTenantID, queryLimit, queryOffset)
	} else {
		consents, total, err = h.ConsentRepo.List(c.Context(), clientID, queryLimit, queryOffset)
	}

	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	items := make([]consentSummary, 0, len(consents))
	for _, consent := range consents {
		// Filter by subject if search is active
		if subject != "" && consent.Subject != subject {
			continue
		}

		var deletedAt *time.Time
		status := "active"
		if consent.DeletedAt.Valid {
			deletedAt = &consent.DeletedAt.Time
			status = "revoked"
		}

		// Filter by status if requested
		if statusFilter != "" && statusFilter != "all" {
			if statusFilter == "active" && status != "active" {
				continue
			}
			if statusFilter == "revoked" && status != "revoked" {
				continue
			}
		}

		userName := ""
		if h.KratosAdmin != nil {
			identity, err := h.KratosAdmin.GetIdentity(c.Context(), consent.Subject)
			if err == nil && identity != nil {
				if name, ok := identity.Traits["name"].(string); ok {
					userName = name
				} else if email, ok := identity.Traits["email"].(string); ok {
					userName = email
				}
			}
		}

		var rpMetadata domain.JSONMap
		if h.RPUserMetadataRepo != nil {
			if row, err := h.RPUserMetadataRepo.Get(c.Context(), consent.ClientID, consent.Subject); err == nil && row != nil && len(row.Metadata) > 0 {
				rpMetadata = row.Metadata
			}
		}

		items = append(items, consentSummary{
			Subject:         consent.Subject,
			UserName:        userName,
			ClientID:        consent.ClientID,
			GrantedScopes:   consent.GrantedScopes,
			AuthenticatedAt: consent.UpdatedAt.Format(time.RFC3339),
			CreatedAt:       consent.CreatedAt,
			DeletedAt:       deletedAt,
			Status:          status,
			TenantID:        consent.TenantID,
			TenantName:      consent.TenantName,
			RPMetadata:      rpMetadata,
		})
	}

	pagination.SortByKeyDesc(items, consentSummaryCursorKey)
	nextCursor := ""
	if cursorRaw != "" {
		items, nextCursor, err = pagination.PageByCursor(items, limit, cursorRaw, consentSummaryCursorKey)
		if err != nil {
			return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
		}
		offset = 0
	} else if queryLimit != limit {
		if offset > len(items) {
			offset = len(items)
		}
		end := min(offset+limit, len(items))
		pageItems := items[offset:end]
		if len(items) > end && len(pageItems) > 0 {
			lastTimestamp, lastID := consentSummaryCursorKey(pageItems[len(pageItems)-1])
			nextCursor = pagination.Encode(lastTimestamp, lastID)
		}
		items = pageItems
	} else if total > int64(offset+len(items)) && len(items) > 0 {
		lastTimestamp, lastID := consentSummaryCursorKey(items[len(items)-1])
		nextCursor = pagination.Encode(lastTimestamp, lastID)
	}

	return c.JSON(consentListResponse{
		Items:      items,
		Total:      total,
		Limit:      limit,
		Offset:     offset,
		Cursor:     cursorRaw,
		NextCursor: nextCursor,
	})
}

func consentSummaryCursorKey(consent consentSummary) (time.Time, string) {
	return consent.CreatedAt, consent.ClientID + ":" + consent.Subject
}

func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	subject := strings.TrimSpace(c.Query("subject"))
	if subject == "" {
		return errorJSON(c, fiber.StatusBadRequest, "subject is required")
	}
	clientID := strings.TrimSpace(c.Query("client_id"))
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if clientID != "" {
		client, err := h.Hydra.GetClient(c.Context(), clientID)
		if err != nil {
			if errors.Is(err, service.ErrHydraNotFound) {
				return errorJSON(c, fiber.StatusNotFound, "client not found")
			}
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
		summary := h.mapClientSummary(*client)
		if !canAccessClientByLegacyScope(profile, summary) && !h.canOperateClientByPermit(c, profile, summary, "revoke_consents") {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
		}
	}

	// If subject is not a UUID, try to resolve it as an identifier (email/username)
	if _, err := uuid.Parse(subject); err != nil {
		resolved, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), subject)
		if err == nil && resolved != "" {
			subject = resolved
		}
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "REVOKE_CONSENT",
		"target_id": clientID,
		"tenant_id": tenantID,
		"after": map[string]any{
			"subject": subject,
		},
	})

	// 1. Revoke in Hydra
	if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	// 2. Sync to Local DB (Delete)
	if h.ConsentRepo != nil {
		_ = h.ConsentRepo.Delete(c.Context(), subject, clientID)
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	current, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	if isHiddenSystemClient(*current) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
	}

	summary := h.mapClientSummary(*current)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	// [Tenant Isolation]
	if !canAccessClientByLegacyScope(profile, summary) && !h.canOperateClientByPermit(c, profile, summary, "rotate_secret") {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	// [Security] Check permission for private clients
	if summary.Type == "private" {
		if !h.canBypassPrivateClientRestriction(c, profile, summary, "rotate_secret") {
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
		}
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "ROTATE_SECRET",
		"target_id": clientID,
		"tenant_id": tenantID,
	})

	// 1. Generate new secret
	newSecret, err := generateRandomSecret(20)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to generate secret")
	}

	// 2. Update Hydra
	current.ClientSecret = newSecret
	updated, err := h.Hydra.UpdateClient(c.Context(), clientID, *current)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	// 3. Update Persistence (DB & Redis)
	if h.SecretRepo != nil {
		if err := h.SecretRepo.Upsert(c.Context(), clientID, newSecret); err != nil {
			// Log error but don't fail the request as Hydra is already updated
			fmt.Printf("failed to update secret in repo: %v\n", err)
		}
	}

	if h.Redis != nil {
		_ = h.Redis.Set("client_secret:"+clientID, newSecret, 0)
	}

	// Return the new secret
	updatedSummary := h.mapClientSummary(*updated)
	updatedSummary.ClientSecret = newSecret
	cacheState, _ := h.publicHeadlessJWKSCacheState(updatedSummary.ID)

	return c.JSON(clientDetailResponse{
		Client:            updatedSummary,
		HeadlessJWKSCache: cacheState,
		Endpoints: clientEndpoints{
			Discovery:     strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
			Issuer:        h.Hydra.PublicURL,
			Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
			Token:         strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
			UserInfo:      strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
		},
	})
}

func (h *DevHandler) RefreshHeadlessJWKSCache(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	current, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if isHiddenSystemClient(*current) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
	}

	summary := h.mapClientSummary(*current)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if !canAccessClientByLegacyScope(profile, summary) && !h.canOperateClientByPermit(c, profile, summary, "operate_jwks") {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	if !current.IsHeadlessLoginEnabled() {
		return errorJSON(c, fiber.StatusBadRequest, "headless login is not enabled for this client")
	}

	if h.HeadlessJWKS == nil {
		h.HeadlessJWKS = service.NewHeadlessJWKSCacheService(h.Redis, nil)
	}
	if h.HeadlessJWKS == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "headless jwks cache service is unavailable")
	}
	if _, err := h.HeadlessJWKS.ForceRefresh(c.Context(), *current, "manual_refresh"); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, headlessClientAssertionErrorMessage(err))
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "REFRESH_HEADLESS_JWKS_CACHE",
		"target_id": clientID,
		"tenant_id": tenantID,
	})

	cacheState, _ := h.publicHeadlessJWKSCacheState(clientID)
	return c.JSON(clientDetailResponse{
		Client:            summary,
		HeadlessJWKSCache: cacheState,
		Endpoints: clientEndpoints{
			Discovery:     strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
			Issuer:        h.Hydra.PublicURL,
			Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
			Token:         strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
			UserInfo:      strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
		},
	})
}

func (h *DevHandler) RevokeHeadlessJWKSCache(c *fiber.Ctx) error {
	tenantID := h.injectTenantContextFromHeader(c)
	clientID := strings.TrimSpace(c.Params("id"))
	if clientID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "client id is required")
	}

	current, err := h.Hydra.GetClient(c.Context(), clientID)
	if err != nil {
		if errors.Is(err, service.ErrHydraNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "client not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if isHiddenSystemClient(*current) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
	}

	summary := h.mapClientSummary(*current)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if !canAccessClientByLegacyScope(profile, summary) && !h.canOperateClientByPermit(c, profile, summary, "operate_jwks") {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
	}

	if h.HeadlessJWKS == nil {
		h.HeadlessJWKS = service.NewHeadlessJWKSCacheService(h.Redis, nil)
	}
	if h.HeadlessJWKS != nil {
		_ = h.HeadlessJWKS.DeleteState(clientID)
	}

	h.setAuditDetailsExtra(c, map[string]any{
		"action":    "REVOKE_HEADLESS_JWKS_CACHE",
		"target_id": clientID,
		"tenant_id": tenantID,
	})

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *DevHandler) ListAuditLogs(c *fiber.Ctx) error {
	if h.AuditRepo == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable")
	}

	h.injectTenantContextFromHeader(c)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	limit := c.QueryInt("limit", 50)
	if limit <= 0 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}

	actionFilter := strings.ToUpper(strings.TrimSpace(c.Query("action")))
	clientFilter := strings.TrimSpace(c.Query("client_id"))
	statusFilter := strings.ToLower(strings.TrimSpace(c.Query("status")))
	allowedClientIDs := managedClientIDsFromProfile(profile)
	allowedClientIDs = mergeStringSets(allowedClientIDs, h.auditClientIDsByPermit(c, profile, clientFilter))
	if role != domain.RoleSuperAdmin && len(allowedClientIDs) == 0 && (role == "rp_admin" || role == domain.RoleUser) {
		return c.JSON(devAuditListResponse{
			Items:  []domain.AuditLog{},
			Limit:  limit,
			Cursor: c.Query("cursor"),
		})
	}

	tenantFilter := strings.TrimSpace(c.Query("tenant_id"))
	if tenantFilter == "" {
		tenantFilter = h.resolveDevTenantScope(c)
	}
	if role != domain.RoleSuperAdmin && len(allowedClientIDs) > 0 {
		tenantFilter = ""
	}
	if role != domain.RoleSuperAdmin && tenantFilter == "" && len(allowedClientIDs) == 0 {
		tenantFilter = tenantIDFromProfile(profile)
	}

	cursorRaw := c.Query("cursor")
	cursor, err := parseAuditCursor(cursorRaw)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "Invalid cursor")
	}

	collected := make([]domain.AuditLog, 0, limit+1)
	nextCursor := cursor
	scanned := 0
	const pageSize = 100
	const maxScan = 3000

	for len(collected) < limit+1 && scanned < maxScan {
		page, findErr := h.AuditRepo.FindPage(c.Context(), pageSize, nextCursor, tenantFilter)
		if findErr != nil {
			return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs")
		}
		if len(page) == 0 {
			break
		}

		for _, logItem := range page {
			scanned++
			if h.matchesDevAuditFilter(logItem, tenantFilter, clientFilter, actionFilter, statusFilter, allowedClientIDs) {
				collected = append(collected, logItem)
				if len(collected) == limit+1 {
					break
				}
			}
		}

		last := page[len(page)-1]
		nextCursor = &domain.AuditCursor{Timestamp: last.Timestamp, EventID: last.EventID}
		if len(page) < pageSize {
			break
		}
	}

	nextCursorRaw := ""
	if len(collected) > limit {
		last := collected[limit-1]
		nextCursorRaw = encodeAuditCursor(last)
		collected = collected[:limit]
	}

	return c.JSON(devAuditListResponse{
		Items:      collected,
		Limit:      limit,
		Cursor:     cursorRaw,
		NextCursor: nextCursorRaw,
	})
}

func (h *DevHandler) GetStats(c *fiber.Ctx) error {
	h.injectTenantContextFromHeader(c)
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}

	visibleClients, err := h.listVisibleClientSummaries(c, profile, 500, 0)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to resolve visible clients")
	}

	userTenantID := tenantIDFromProfile(profile)
	totalClients := int64(len(visibleClients))
	visibleClientIDs := clientIDSetFromSummaries(visibleClients)

	// 2. Auth Failures (24h)
	var authFailures int64
	var activeSessions int64
	if h.AuditRepo != nil {
		failureSince := time.Now().Add(-24 * time.Hour)
		sessionSince := time.Now().Add(-1 * time.Hour)
		if shouldScopeDashboardToExplicitClients(role) {
			authFailures, activeSessions, _ = h.countScopedDashboardAuditMetrics(
				c,
				userTenantID,
				visibleClientIDs,
				failureSince,
				sessionSince,
			)
		} else {
			authFailures, _ = h.AuditRepo.CountFailuresSince(c.Context(), failureSince, userTenantID)
			activeSessions, _ = h.AuditRepo.CountActiveSessionsSince(c.Context(), sessionSince, userTenantID)
		}
	}

	return c.JSON(devStatsResponse{
		TotalClients:   totalClients,
		ActiveSessions: activeSessions,
		AuthFailures:   authFailures,
	})
}

func (h *DevHandler) GetRPUsageDaily(c *fiber.Ctx) error {
	h.injectTenantContextFromHeader(c)

	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
	}
	role := normalizeUserRole(profile.Role)
	if !isDevConsoleViewerRole(role) {
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
	if h == nil || h.RPUsageQueries == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "rp usage query service unavailable")
	}

	days := 14
	if raw := c.Query("days"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil {
			days = parsed
		}
	}
	period := normalizeRPUsagePeriod(c.Query("period"))

	visibleClients, err := h.listVisibleClientSummaries(c, profile, 500, 0)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to resolve visible clients")
	}

	allowedClientIDs := clientIDSetFromSummaries(visibleClients)
	if role != domain.RoleSuperAdmin && len(allowedClientIDs) == 0 {
		return c.JSON(devRPUsageDailyResponse{
			Items:  []domain.RPUsageDailyMetric{},
			Days:   days,
			Period: period,
		})
	}

	tenantID := ""
	if role != domain.RoleSuperAdmin {
		tenantID = tenantIDFromProfile(profile)
	}

	items, err := h.RPUsageQueries.FindRPUsage(c.Context(), domain.RPUsageQuery{
		Days:     days,
		Period:   period,
		TenantID: tenantID,
	})
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	filtered := make([]domain.RPUsageDailyMetric, 0, len(items))
	for _, item := range items {
		if role != domain.RoleSuperAdmin {
			if _, ok := allowedClientIDs[strings.TrimSpace(item.ClientID)]; !ok {
				continue
			}
		}
		filtered = append(filtered, item)
	}

	return c.JSON(devRPUsageDailyResponse{
		Items:    filtered,
		Days:     days,
		Period:   period,
		TenantID: tenantID,
	})
}

func generateRandomSecret(length int) (string, error) {
	bytes := make([]byte, length)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	// Use Base64 URL encoding (no padding) to look like Hydra's native secrets
	return base64.RawURLEncoding.EncodeToString(bytes), nil
}

func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
	status := "active"
	var createdAt *time.Time

	if client.Metadata != nil {
		if value, ok := client.Metadata["status"].(string); ok && strings.ToLower(value) == "inactive" {
			status = "inactive"
		}
		if value, ok := client.Metadata["created_at"].(string); ok {
			if t, err := time.Parse(time.RFC3339, value); err == nil {
				createdAt = &t
			}
		}
	}

	clientType := "private"
	if client.IsHeadlessLoginEnabled() {
		clientType = "private"
	} else if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
		clientType = "pkce"
	}

	name := strings.TrimSpace(client.ClientName)
	if name == "" {
		name = client.ClientID
	}

	scopes := strings.Fields(client.Scope)

	clientSecret := client.ClientSecret
	// 1. Check Metadata (Legacy/Fallback)
	if clientSecret == "" && client.Metadata != nil {
		if val, ok := client.Metadata["client_secret"].(string); ok {
			clientSecret = val
		}
	}

	// 2. Check Redis (Cache)
	if clientSecret == "" && h.Redis != nil {
		if val, err := h.Redis.Get("client_secret:" + client.ClientID); err == nil && val != "" {
			clientSecret = val
		}
	}

	// 3. Check PostgreSQL (Source of Truth) & Cache Warming
	if clientSecret == "" && h.SecretRepo != nil {
		if val, err := h.SecretRepo.GetByID(context.Background(), client.ClientID); err == nil && val != "" {
			clientSecret = val
			// Warm up cache
			if h.Redis != nil {
				_ = h.Redis.Set("client_secret:"+client.ClientID, clientSecret, 0)
			}
		}
	}

	return clientSummary{
		ID:                               client.ClientID,
		Name:                             name,
		Type:                             clientType,
		Status:                           status,
		CreatedAt:                        createdAt,
		RedirectURIs:                     client.RedirectURIs,
		Scopes:                           scopes,
		ClientSecret:                     clientSecret,
		TokenEndpointAuthMethod:          client.TokenEndpointAuthMethod,
		SkipConsent:                      valueOrBool(client.SkipConsent, true),
		JwksUri:                          client.JWKSUri,
		Jwks:                             client.JWKS,
		BackchannelLogoutURI:             client.BackchannelLogoutURI(),
		BackchannelLogoutSessionRequired: client.BackchannelLogoutSessionRequiredValue(),
		Metadata:                         client.Metadata,
	}
}

func readMetadataStringValue(metadata map[string]any, key string) string {
	if metadata == nil {
		return ""
	}
	raw, _ := metadata[key].(string)
	return strings.TrimSpace(raw)
}

func readMetadataBoolValue(metadata map[string]any, key string) bool {
	if metadata == nil {
		return false
	}
	value, _ := metadata[key].(bool)
	return value
}

func readStringSliceMetadata(metadata map[string]any, key string) []string {
	if metadata == nil {
		return nil
	}
	raw, ok := metadata[key]
	if !ok || raw == nil {
		return nil
	}
	switch typed := raw.(type) {
	case []string:
		result := make([]string, 0, len(typed))
		for _, item := range typed {
			if trimmed := strings.TrimSpace(item); trimmed != "" {
				result = append(result, trimmed)
			}
		}
		return result
	case []any:
		result := make([]string, 0, len(typed))
		for _, item := range typed {
			if str, ok := item.(string); ok {
				if trimmed := strings.TrimSpace(str); trimmed != "" {
					result = append(result, trimmed)
				}
			}
		}
		return result
	default:
		return nil
	}
}

func readMetadataValueOrNil(metadata map[string]any, key string) any {
	if metadata == nil {
		return nil
	}
	value, ok := metadata[key]
	if !ok {
		return nil
	}
	return value
}

func normalizeBackchannelLogoutMetadata(metadata map[string]any, logoutURI string, sessionRequired bool) (map[string]any, error) {
	if metadata == nil {
		metadata = map[string]any{}
	}

	trimmedURI := strings.TrimSpace(logoutURI)
	if err := validateBackchannelLogoutURI(trimmedURI); err != nil {
		return nil, err
	}

	if trimmedURI == "" {
		delete(metadata, domain.MetadataBackChannelLogoutURI)
		delete(metadata, domain.MetadataBackChannelLogoutSessionRequired)
		return metadata, nil
	}

	metadata[domain.MetadataBackChannelLogoutURI] = trimmedURI
	metadata[domain.MetadataBackChannelLogoutSessionRequired] = sessionRequired
	return metadata, nil
}

func validateBackchannelLogoutURI(raw string) error {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return nil
	}

	parsed, err := url.Parse(trimmed)
	if err != nil || parsed == nil {
		return fmt.Errorf("backchannelLogoutUri must be a valid absolute URL")
	}
	if parsed.Scheme == "" || parsed.Host == "" {
		return fmt.Errorf("backchannelLogoutUri must be a valid absolute URL")
	}
	if parsed.Fragment != "" {
		return fmt.Errorf("backchannelLogoutUri must not include a fragment")
	}

	switch strings.ToLower(parsed.Scheme) {
	case "https":
		return nil
	case "http":
		if isAllowedLocalBackchannelLogoutHost(parsed.Hostname()) {
			return nil
		}
		return fmt.Errorf("backchannelLogoutUri must use https outside local development")
	default:
		return fmt.Errorf("backchannelLogoutUri must use http or https")
	}
}

func isAllowedLocalBackchannelLogoutHost(rawHost string) bool {
	host := strings.ToLower(strings.TrimSpace(rawHost))
	if host == "" {
		return false
	}

	switch host {
	case "localhost", "127.0.0.1", "::1", "host.docker.internal":
		return true
	}

	if ip := net.ParseIP(host); ip != nil {
		return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()
	}

	// Docker service names and other single-label local hostnames are
	// permitted only for local HTTP development workflows.
	return !strings.Contains(host, ".")
}

func normalizeClientAutoLoginMetadata(metadata map[string]any) (map[string]any, error) {
	if metadata == nil {
		return metadata, nil
	}

	supported := readMetadataBoolValue(metadata, domain.MetadataAutoLoginSupported)
	rawURL := strings.TrimSpace(readMetadataStringValue(metadata, domain.MetadataAutoLoginURL))
	metadata[domain.MetadataAutoLoginSupported] = supported
	if !supported {
		delete(metadata, domain.MetadataAutoLoginURL)
		return metadata, nil
	}

	if rawURL == "" {
		return nil, errors.New("auto_login_url is required when auto_login_supported is true")
	}
	parsed, err := url.Parse(rawURL)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" || (parsed.Scheme != "https" && parsed.Scheme != "http") {
		return nil, errors.New("auto_login_url must be an http or https URL")
	}
	metadata[domain.MetadataAutoLoginURL] = rawURL
	return metadata, nil
}

func normalizeHeadlessClientConfig(
	tokenAuthMethod string,
	jwksURI string,
	jwks any,
	metadata map[string]any,
) (string, string, any, map[string]any) {
	if metadata == nil {
		metadata = map[string]any{}
	}
	delete(metadata, domain.MetadataRequestObjectSigningAlg)

	headlessEnabled := readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled)
	if headlessEnabled {
		headlessTokenAuthMethod := readMetadataStringValue(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod)
		if headlessTokenAuthMethod == "" && strings.EqualFold(strings.TrimSpace(tokenAuthMethod), "private_key_jwt") {
			headlessTokenAuthMethod = strings.TrimSpace(tokenAuthMethod)
		}
		if headlessTokenAuthMethod == "" || strings.EqualFold(headlessTokenAuthMethod, "none") {
			headlessTokenAuthMethod = "private_key_jwt"
		}
		metadata[domain.MetadataHeadlessTokenEndpointAuthMethod] = headlessTokenAuthMethod

		headlessJWKSURI := strings.TrimSpace(jwksURI)
		if headlessJWKSURI == "" {
			headlessJWKSURI = readMetadataStringValue(metadata, domain.MetadataHeadlessJWKSURI)
		}
		if headlessJWKSURI != "" {
			metadata[domain.MetadataHeadlessJWKSURI] = headlessJWKSURI
		} else {
			delete(metadata, domain.MetadataHeadlessJWKSURI)
		}

		delete(metadata, domain.MetadataHeadlessJWKS)

		return headlessTokenAuthMethod, headlessJWKSURI, nil, metadata
	}

	delete(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod)
	delete(metadata, domain.MetadataHeadlessJWKSURI)
	delete(metadata, domain.MetadataHeadlessJWKS)
	return tokenAuthMethod, jwksURI, jwks, metadata
}

func validateHeadlessClientInput(jwksURI string, jwks any, metadata map[string]any) error {
	if !readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
		return nil
	}

	if jwks != nil {
		return fmt.Errorf("headless login supports jwksUri only; inline jwks is not supported")
	}

	resolvedURI := strings.TrimSpace(jwksURI)
	if resolvedURI == "" {
		resolvedURI = readMetadataStringValue(metadata, domain.MetadataHeadlessJWKSURI)
	}
	if resolvedURI == "" {
		return fmt.Errorf("headless login requires jwksUri; inline jwks is not supported")
	}
	return nil
}

func normalizeClientTypeForHeadless(clientType string, metadata map[string]any) string {
	if readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
		return "private"
	}
	return clientType
}

func normalizeIDTokenClaimsMetadata(metadata map[string]any) (map[string]any, error) {
	if metadata == nil {
		return nil, nil
	}

	rawClaims, exists := metadata[domain.MetadataIDTokenClaims]
	if !exists || rawClaims == nil {
		return metadata, nil
	}

	normalized, err := normalizeIDTokenClaimsForDevConsole(rawClaims)
	if err != nil {
		return nil, err
	}
	metadata[domain.MetadataIDTokenClaims] = normalized
	return metadata, nil
}

func normalizeIDTokenClaims(rawClaims any) ([]normalizedIDTokenClaim, error) {
	return normalizeIDTokenClaimsWithOptions(rawClaims, true)
}

func normalizeIDTokenClaimsForDevConsole(rawClaims any) ([]normalizedIDTokenClaim, error) {
	return normalizeIDTokenClaimsWithOptions(rawClaims, false)
}

func normalizeIDTokenClaimsWithOptions(rawClaims any, allowTopLevel bool) ([]normalizedIDTokenClaim, error) {
	rawList, ok := rawClaims.([]any)
	if !ok {
		if typedList, ok := rawClaims.([]map[string]any); ok {
			rawList = make([]any, 0, len(typedList))
			for _, item := range typedList {
				rawList = append(rawList, item)
			}
		} else if typedList, ok := rawClaims.([]map[string]any); ok {
			rawList = make([]any, 0, len(typedList))
			for _, item := range typedList {
				rawList = append(rawList, item)
			}
		} else {
			return nil, errors.New("metadata.id_token_claims must be an array")
		}
	}

	normalized := make([]normalizedIDTokenClaim, 0, len(rawList))
	seen := make(map[string]struct{}, len(rawList))

	for _, item := range rawList {
		record, ok := item.(map[string]any)
		if !ok {
			if typedRecord, ok := item.(map[string]any); ok {
				record = make(map[string]any, len(typedRecord))
				maps.Copy(record, typedRecord)
			} else {
				return nil, errors.New("metadata.id_token_claims items must be objects")
			}
		}

		namespace := strings.TrimSpace(readInterfaceString(record["namespace"], "top_level"))
		if namespace == "" {
			namespace = "top_level"
		}
		if namespace != "top_level" && namespace != "rp_claims" {
			return nil, fmt.Errorf("metadata.id_token_claims namespace must be top_level or rp_claims: %s", namespace)
		}
		if !allowTopLevel && namespace == "top_level" {
			return nil, errors.New("metadata.id_token_claims top_level namespace is managed from admin user custom claims")
		}

		key := strings.TrimSpace(readInterfaceString(record["key"], ""))
		if key == "" {
			return nil, errors.New("metadata.id_token_claims key is required")
		}
		if namespace == "top_level" && key == "rp_claims" {
			return nil, errors.New("metadata.id_token_claims top-level key rp_claims is reserved")
		}

		valueType := strings.TrimSpace(readInterfaceString(record["valueType"], "text"))
		if valueType == "" {
			valueType = "text"
		}
		switch valueType {
		case "text", "number", "boolean", "array", "object", "date", "datetime":
		default:
			return nil, fmt.Errorf("metadata.id_token_claims valueType is invalid: %s", valueType)
		}

		value := strings.TrimSpace(readInterfaceString(record["value"], ""))
		if _, err := parseConfiguredClaimValue(value, valueType); err != nil {
			return nil, fmt.Errorf("metadata.id_token_claims %s.%s is invalid: %w", namespace, key, err)
		}

		signature := namespace + ":" + key
		if _, exists := seen[signature]; exists {
			return nil, fmt.Errorf("metadata.id_token_claims contains duplicate key: %s.%s", namespace, key)
		}
		seen[signature] = struct{}{}

		normalized = append(normalized, normalizedIDTokenClaim{
			Namespace:       namespace,
			Key:             key,
			Value:           value,
			ValueType:       valueType,
			ReadPermission:  normalizeCustomClaimPermission(record["readPermission"]),
			WritePermission: normalizeCustomClaimPermission(record["writePermission"]),
		})
	}

	return normalized, nil
}

func readInterfaceString(value any, fallback string) string {
	if value == nil {
		return fallback
	}
	if text, ok := value.(string); ok {
		return text
	}
	return fallback
}

func parseConfiguredClaimValue(rawValue string, valueType string) (any, error) {
	trimmed := strings.TrimSpace(rawValue)

	switch valueType {
	case "text":
		return trimmed, nil
	case "number":
		if trimmed == "" {
			return nil, errors.New("number value is required")
		}
		parsed, err := strconv.ParseFloat(trimmed, 64)
		if err != nil {
			return nil, errors.New("number value must be a finite number")
		}
		return parsed, nil
	case "boolean":
		switch strings.ToLower(trimmed) {
		case "true", "1", "yes", "on":
			return true, nil
		case "false", "0", "no", "off":
			return false, nil
		default:
			return nil, errors.New("boolean value must be true/false")
		}
	case "array":
		if trimmed == "" {
			return []string{}, nil
		}
		if strings.HasPrefix(trimmed, "[") {
			var parsed []any
			if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
				return nil, errors.New("array value must be valid JSON array")
			}
			return parsed, nil
		}
		parts := strings.Split(trimmed, ",")
		values := make([]string, 0, len(parts))
		for _, part := range parts {
			part = strings.TrimSpace(part)
			if part != "" {
				values = append(values, part)
			}
		}
		return values, nil
	case "object":
		if trimmed == "" {
			return map[string]any{}, nil
		}
		var parsed map[string]any
		if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
			return nil, errors.New("object value must be valid JSON object")
		}
		return parsed, nil
	case "date":
		if trimmed == "" {
			return nil, errors.New("date value is required")
		}
		if _, err := time.Parse("2006-01-02", trimmed); err != nil {
			return nil, errors.New("date value must use YYYY-MM-DD")
		}
		return trimmed, nil
	case "datetime":
		if trimmed == "" {
			return nil, errors.New("datetime value is required")
		}
		if _, err := time.Parse(time.RFC3339, trimmed); err == nil {
			return trimmed, nil
		}
		if _, err := time.Parse("2006-01-02T15:04", trimmed); err == nil {
			return trimmed, nil
		}
		return nil, errors.New("datetime value must use RFC3339 or YYYY-MM-DDTHH:mm")
	default:
		return nil, fmt.Errorf("unsupported claim value type: %s", valueType)
	}
}

func requestIncludesInlineHeadlessJWKS(req clientUpsertRequest) bool {
	if req.Jwks != nil {
		return true
	}
	if req.Metadata == nil {
		return false
	}
	value, ok := (*req.Metadata)[domain.MetadataHeadlessJWKS]
	return ok && value != nil
}

func defaultClientScopes() []string {
	return []string{"openid", "profile", "email"}
}

func defaultGrantTypes() []string {
	return []string{"authorization_code", "refresh_token"}
}

func defaultResponseTypes() []string {
	return []string{"code"}
}

func buildScope(scopes []string) string {
	return strings.Join(scopes, " ")
}

func valueOr(ptr *string, fallback string) string {
	if ptr == nil {
		return fallback
	}
	return *ptr
}

//go:fix inline
func boolPtr(value bool) *bool {
	return new(value)
}

func valueOrBool(ptr *bool, fallback bool) bool {
	if ptr == nil {
		return fallback
	}
	return *ptr
}

func valueOrSlice(ptr *[]string, fallback []string) []string {
	if ptr == nil {
		return fallback
	}
	return *ptr
}

func derefSlice(ptr *[]string, fallback []string) []string {
	if ptr == nil {
		return fallback
	}
	return *ptr
}

func mergeMetadata(current map[string]any, incoming *map[string]any) map[string]any {
	if incoming == nil {
		return current
	}
	merged := map[string]any{}
	maps.Copy(merged, current)
	maps.Copy(merged, *incoming)
	return merged
}

func resolveTokenAuthMethod(requested, fallback string) string {
	if strings.TrimSpace(requested) == "" {
		return fallback
	}
	return requested
}

func (h *DevHandler) injectTenantContextFromHeader(c *fiber.Ctx) string {
	tenantID := strings.TrimSpace(c.Get("X-Tenant-ID"))
	if tenantID != "" {
		c.Locals("tenant_id", tenantID)
	}
	return tenantID
}

func (h *DevHandler) setAuditDetailsExtra(c *fiber.Ctx, extra map[string]any) {
	if c == nil || len(extra) == 0 {
		return
	}
	if existing := c.Locals("audit_details_extra"); existing != nil {
		if m, ok := existing.(map[string]any); ok {
			maps.Copy(m, extra)
			c.Locals("audit_details_extra", m)
			return
		}
	}
	c.Locals("audit_details_extra", extra)
}

func normalizeAuditAction(eventType string, details map[string]any) string {
	if raw, ok := details["action"].(string); ok && strings.TrimSpace(raw) != "" {
		return strings.ToUpper(strings.TrimSpace(raw))
	}
	normalized := strings.TrimSpace(eventType)
	switch {
	case normalized == "POST /api/v1/dev/clients":
		return "CREATE_CLIENT"
	case strings.HasPrefix(normalized, "PUT /api/v1/dev/clients/"):
		return "UPDATE_CLIENT"
	case strings.HasPrefix(normalized, "PATCH /api/v1/dev/clients/") && strings.HasSuffix(normalized, "/status"):
		return "UPDATE_CLIENT_STATUS"
	case strings.HasPrefix(normalized, "POST /api/v1/dev/clients/") && strings.HasSuffix(normalized, "/secret/rotate"):
		return "ROTATE_SECRET"
	case strings.HasPrefix(normalized, "DELETE /api/v1/dev/clients/"):
		return "DELETE_CLIENT"
	case normalized == "DELETE /api/v1/dev/consents":
		return "REVOKE_CONSENT"
	default:
		return ""
	}
}

func devAuditClientIDFromEventType(eventType string) string {
	parts := strings.Split(strings.TrimSpace(eventType), " ")
	if len(parts) != 2 {
		return ""
	}
	path := strings.Trim(parts[1], "/")
	segments := strings.Split(path, "/")
	for idx := 0; idx+1 < len(segments); idx++ {
		if segments[idx] == "clients" {
			return strings.TrimSpace(segments[idx+1])
		}
	}
	return ""
}

func resolveDevAuditClientID(logItem domain.AuditLog, details map[string]any) string {
	targetID, _ := details["target_id"].(string)
	clientID, _ := details["client_id"].(string)
	resolvedID := strings.TrimSpace(targetID)
	if resolvedID == "" {
		resolvedID = strings.TrimSpace(clientID)
	}
	if resolvedID == "" {
		resolvedID = devAuditClientIDFromEventType(logItem.EventType)
	}
	return resolvedID
}

func resolveStatusFromMetadata(metadata map[string]any) string {
	if metadata != nil {
		if value, ok := metadata["status"].(string); ok && strings.ToLower(strings.TrimSpace(value)) == "inactive" {
			return "inactive"
		}
	}
	return "active"
}

func clientTypeOrDefault(tokenEndpointAuthMethod string) string {
	if strings.EqualFold(tokenEndpointAuthMethod, "none") {
		return "pkce"
	}
	return "private"
}

func (h *DevHandler) matchesDevAuditFilter(
	logItem domain.AuditLog,
	tenantFilter, clientFilter, actionFilter, statusFilter string,
	allowedClientIDs map[string]struct{},
) bool {
	if !strings.Contains(logItem.EventType, "/api/v1/dev/") {
		return false
	}
	details, _ := utils.ParseAuditDetails(logItem.Details)
	if statusFilter != "" && statusFilter != "all" && strings.ToLower(logItem.Status) != statusFilter {
		return false
	}
	if tenantFilter != "" {
		detailTenant, _ := details["tenant_id"].(string)
		if strings.TrimSpace(detailTenant) != tenantFilter {
			return false
		}
	}
	if clientFilter != "" {
		if resolveDevAuditClientID(logItem, details) != clientFilter {
			return false
		}
	}
	if len(allowedClientIDs) > 0 {
		resolvedID := resolveDevAuditClientID(logItem, details)
		if resolvedID == "" {
			return false
		}
		if _, ok := allowedClientIDs[resolvedID]; !ok {
			return false
		}
	}
	if actionFilter != "" {
		if normalizeAuditAction(logItem.EventType, details) != actionFilter {
			return false
		}
	}
	return true
}

func (h *DevHandler) resolveDevTenantScope(c *fiber.Ctx) string {
	fromHeader := strings.TrimSpace(c.Get("X-Tenant-ID"))
	if fromHeader != "" {
		return fromHeader
	}
	if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil && profile.TenantID != nil {
		return strings.TrimSpace(*profile.TenantID)
	}
	return ""
}

func (h *DevHandler) countScopedDashboardAuditMetrics(
	c *fiber.Ctx,
	tenantID string,
	allowedClientIDs map[string]struct{},
	failureSince, sessionSince time.Time,
) (int64, int64, error) {
	if h.AuditRepo == nil || len(allowedClientIDs) == 0 {
		return 0, 0, nil
	}

	oldestSince := failureSince
	if sessionSince.Before(oldestSince) {
		oldestSince = sessionSince
	}

	var failureCount int64
	activeSessions := make(map[string]struct{})
	var cursor *domain.AuditCursor

	const pageSize = 200
	const maxScan = 5000
	scanned := 0

	for scanned < maxScan {
		page, err := h.AuditRepo.FindPage(c.Context(), pageSize, cursor, tenantID)
		if err != nil {
			return 0, 0, err
		}
		if len(page) == 0 {
			break
		}

		stop := false
		for _, logItem := range page {
			scanned++
			if logItem.Timestamp.Before(oldestSince) {
				stop = true
				break
			}

			details, _ := utils.ParseAuditDetails(logItem.Details)
			clientID := strings.TrimSpace(resolveDevAuditClientID(logItem, details))
			if _, ok := allowedClientIDs[clientID]; !ok {
				continue
			}

			if strings.EqualFold(logItem.Status, "failure") && !logItem.Timestamp.Before(failureSince) {
				failureCount++
			}
			if strings.EqualFold(logItem.Status, "success") && !logItem.Timestamp.Before(sessionSince) {
				sessionID := strings.TrimSpace(logItem.SessionID)
				if sessionID != "" {
					activeSessions[sessionID] = struct{}{}
				}
			}
		}

		if stop || len(page) < pageSize {
			break
		}

		last := page[len(page)-1]
		cursor = &domain.AuditCursor{Timestamp: last.Timestamp, EventID: last.EventID}
	}

	return failureCount, int64(len(activeSessions)), nil
}

// ListMyTenants returns the list of tenants the current user manages or belongs to.
func (h *DevHandler) ListMyTenants(c *fiber.Ctx) error {
	profile, err := h.Auth.GetEnrichedProfile(c)
	if err != nil || profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}

	role := normalizeUserRole(profile.Role)
	if role == domain.RoleUser {
		if profile.TenantID == nil || strings.TrimSpace(*profile.TenantID) == "" {
			return c.JSON([]domain.Tenant{})
		}
		tenant, err := h.TenantSvc.GetTenant(c.Context(), *profile.TenantID)
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, "failed to get tenant")
		}
		if tenant == nil {
			return c.JSON([]domain.Tenant{})
		}
		return c.JSON([]domain.Tenant{*tenant})
	}

	if role == domain.RoleSuperAdmin {
		tenants, _, err := h.TenantSvc.ListTenants(c.Context(), 100, 0, "", "")
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, "failed to list tenants")
		}
		return c.JSON(tenants)
	}

	tenants, err := h.TenantSvc.ListManageableTenants(c.Context(), profile.ID)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to list manageable tenants: "+err.Error())
	}

	if profile.TenantID != nil && *profile.TenantID != "" {
		found := false
		for _, t := range tenants {
			if t.ID == *profile.TenantID {
				found = true
				break
			}
		}
		if !found {
			if primary, err := h.TenantSvc.GetTenant(c.Context(), *profile.TenantID); err == nil && primary != nil {
				tenants = append(tenants, *primary)
			}
		}
	}

	return c.JSON(tenants)
}

func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}
	if h.Auth != nil {
		if enriched, err := h.Auth.GetEnrichedProfile(c); err == nil && enriched != nil {
			profile = enriched
			setCurrentProfileContext(c, enriched)
		}
	}

	var req struct {
		Name         string `json:"name"`
		Organization string `json:"organization"`
		Reason       string `json:"reason"`
		TenantID     string `json:"tenantId"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	if req.TenantID == "" && profile.TenantID != nil {
		req.TenantID = *profile.TenantID
	}
	if req.TenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
	}

	name := strings.TrimSpace(profile.Name)
	if name == "" {
		name = strings.TrimSpace(req.Name)
	}
	organization := strings.TrimSpace(req.Organization)
	if h.TenantSvc != nil {
		if tenant, err := h.TenantSvc.GetTenant(c.Context(), req.TenantID); err == nil && tenant != nil && strings.TrimSpace(tenant.Name) != "" {
			organization = strings.TrimSpace(tenant.Name)
		}
	}

	devReq := domain.DeveloperRequest{
		UserID:       profile.ID,
		TenantID:     req.TenantID,
		Name:         name,
		Organization: organization,
		Email:        profile.Email,
		Phone:        profile.Phone,
		Role:         normalizeUserRole(profile.Role),
		Reason:       req.Reason,
		Status:       domain.DeveloperRequestStatusPending,
	}

	if err := h.DeveloperSvc.RequestAccess(c.Context(), devReq); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "ok"})
}

func (h *DevHandler) GetDeveloperRequestStatus(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}

	tenantID := c.Query("tenantId")
	if tenantID == "" && profile.TenantID != nil {
		tenantID = *profile.TenantID
	}
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
	}

	status, err := h.DeveloperSvc.GetRequestStatus(c.Context(), profile.ID, tenantID)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	if status == nil {
		return c.JSON(fiber.Map{"status": "none"})
	}
	if status.Status == domain.DeveloperRequestStatusApproved {
		h.ensureDeveloperGrantRelation(c, status.UserID, status.TenantID)
	}

	return c.JSON(status)
}

func (h *DevHandler) ensureDeveloperGrantRelation(c *fiber.Ctx, userID, tenantID string) {
	if h.KetoOutbox == nil || strings.TrimSpace(userID) == "" || strings.TrimSpace(tenantID) == "" {
		return
	}
	subject := "User:" + strings.TrimSpace(userID)
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		if h.hasDirectTenantRelation(c, tenantID, relation, subject) {
			continue
		}
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  relation,
			Subject:   subject,
			Action:    domain.KetoOutboxActionCreate,
		})
		if h.Keto != nil {
			if err := h.Keto.CreateRelation(c.Context(), "Tenant", tenantID, relation, subject); err != nil {
				slog.Warn("failed to grant immediate developer tenant relation", "tenantID", tenantID, "userID", userID, "relation", relation, "error", err)
			}
		}
	}
}

func (h *DevHandler) grantCreatorAdminRelation(c *fiber.Ctx, clientID string, subject string) error {
	clientID = strings.TrimSpace(clientID)
	subject = strings.TrimSpace(subject)
	if clientID == "" || subject == "" {
		return nil
	}

	if h.Keto != nil {
		existing, err := h.Keto.ListRelations(c.Context(), "RelyingParty", clientID, "admins", subject)
		if err == nil && len(existing) > 0 {
			return nil
		}
		if err == nil {
			if createErr := h.Keto.CreateRelation(c.Context(), "RelyingParty", clientID, "admins", subject); createErr == nil {
				return nil
			} else {
				slog.Warn("failed to grant immediate admin permission to creator; falling back to outbox", "clientID", clientID, "subject", subject, "error", createErr)
			}
		} else {
			slog.Warn("failed to check existing admin relation for creator; falling back to outbox", "clientID", clientID, "subject", subject, "error", err)
		}
	}

	if h.KetoOutbox == nil {
		return nil
	}
	return h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
		Namespace: "RelyingParty",
		Object:    clientID,
		Relation:  "admins",
		Subject:   subject,
		Action:    domain.KetoOutboxActionCreate,
	})
}

func (h *DevHandler) revokeDeveloperGrantRelation(c *fiber.Ctx, userID, tenantID string) {
	if h.KetoOutbox == nil || strings.TrimSpace(userID) == "" || strings.TrimSpace(tenantID) == "" {
		return
	}
	subject := "User:" + strings.TrimSpace(userID)
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  relation,
			Subject:   subject,
			Action:    domain.KetoOutboxActionDelete,
		})
		if h.Keto != nil {
			if err := h.Keto.DeleteRelation(c.Context(), "Tenant", tenantID, relation, subject); err != nil {
				slog.Warn("failed to revoke immediate developer tenant relation", "tenantID", tenantID, "userID", userID, "relation", relation, "error", err)
			}
		}
	}
}

func (h *DevHandler) hasDirectTenantRelation(c *fiber.Ctx, tenantID, relation, subject string) bool {
	if h.Keto == nil {
		return false
	}
	tuples, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, relation, subject)
	return err == nil && len(tuples) > 0
}

func (h *DevHandler) ListDeveloperRequests(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}

	role := normalizeUserRole(profile.Role)
	status := c.Query("status")

	userID := profile.ID
	if role == domain.RoleSuperAdmin {
		// Super Admin can see everyone's requests
		userID = ""
	}

	requests, err := h.DeveloperSvc.ListRequests(c.Context(), userID, status)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.JSON(requests)
}

func (h *DevHandler) ApproveDeveloperRequest(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}
	if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: super_admin only")
	}

	idStr := c.Params("id")
	id, err := strconv.ParseUint(idStr, 10, 32)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
	}

	var reqBody struct {
		AdminNotes string `json:"adminNotes"`
	}
	if err := c.BodyParser(&reqBody); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	devReq, err := h.DeveloperSvc.GetRequestByID(c.Context(), uint(id))
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch request details")
	}

	if err := h.DeveloperSvc.ApproveRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	// Grant Keto Permissions
	if h.KetoOutbox != nil {
		h.ensureDeveloperGrantRelation(c, devReq.UserID, devReq.TenantID)
	}

	return c.JSON(fiber.Map{"status": "ok"})
}

func (h *DevHandler) RejectDeveloperRequest(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}
	if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: super_admin only")
	}

	idStr := c.Params("id")
	id, err := strconv.ParseUint(idStr, 10, 32)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
	}

	var reqBody struct {
		AdminNotes string `json:"adminNotes"`
	}
	if err := c.BodyParser(&reqBody); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	if err := h.DeveloperSvc.RejectRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.JSON(fiber.Map{"status": "ok"})
}

func (h *DevHandler) CancelDeveloperRequestApproval(c *fiber.Ctx) error {
	profile := h.getCurrentProfile(c)
	if profile == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
	}
	if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
		return errorJSON(c, fiber.StatusForbidden, "forbidden: super_admin only")
	}

	idStr := c.Params("id")
	id, err := strconv.ParseUint(idStr, 10, 32)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
	}

	var reqBody struct {
		AdminNotes string `json:"adminNotes"`
	}
	if err := c.BodyParser(&reqBody); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	devReq, err := h.DeveloperSvc.GetRequestByID(c.Context(), uint(id))
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch request details")
	}
	if devReq.Status != domain.DeveloperRequestStatusApproved {
		return errorJSON(c, fiber.StatusBadRequest, "only approved requests can be cancelled")
	}

	if err := h.DeveloperSvc.CancelApprovedRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	h.revokeDeveloperGrantRelation(c, devReq.UserID, devReq.TenantID)

	return c.JSON(fiber.Map{"status": "ok"})
}
