import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
  ExternalLink,
  Info,
  Plus,
  Save,
  Search,
  Shield,
  ShieldHalf,
  Sparkles,
  Trash2,
  Upload,
  X,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate, useParams } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page";
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast";
import type {
  ClientStatus,
  ClientType,
  ClientUpsertRequest,
  MyTenantSummary,
  TenantSummary,
} from "../../lib/devApi";
import {
  type ClientRelation,
  createClient,
  deleteClient,
  fetchClient,
  fetchClientRelations,
  fetchMyTenants,
  refreshHeadlessJwksCache,
  revokeHeadlessJwksCache,
  updateClient,
  updateClientStatus,
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { resolveProfileRole } from "../../lib/role";
import { cn } from "../../lib/utils";
import { fetchMe, type UserProfile } from "../auth/authApi";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import { ClientDetailTabs } from "./ClientDetailTabs";
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";

interface ScopeItem {
  id: string;
  name: string;
  description: string;
  mandatory: boolean;
  locked?: boolean;
}

type ClaimNamespace = "rp_claims";
type ClaimValueType =
  | "text"
  | "number"
  | "boolean"
  | "array"
  | "object"
  | "date"
  | "datetime";
type CustomClaimPermission = "admin_only" | "user_and_admin";

interface IdTokenClaimItem {
  id: string;
  namespace: ClaimNamespace;
  key: string;
  value: string;
  valueType: ClaimValueType;
  readPermission: CustomClaimPermission;
  writePermission: CustomClaimPermission;
}

type SecurityProfile = "private" | "pkce";
type TokenEndpointAuthMethod =
  | "none"
  | "client_secret_basic"
  | "private_key_jwt";

const HEADLESS_LOGIN_ALLOWED_ALGORITHMS = [
  "RS256",
  "RS384",
  "RS512",
  "PS256",
  "PS384",
  "PS512",
  "ES256",
  "ES384",
  "ES512",
  "EdDSA",
] as const;

const HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET = new Set<string>(
  HEADLESS_LOGIN_ALLOWED_ALGORITHMS,
);

function formatHeadlessParsedKeyLabel(
  kid: string | undefined,
  index: number,
): string {
  const trimmedKid = kid?.trim();
  if (trimmedKid) {
    return trimmedKid;
  }
  return `key #${index + 1}`;
}

function isTokenEndpointAuthMethod(
  value: string,
): value is TokenEndpointAuthMethod {
  return (
    value === "none" ||
    value === "client_secret_basic" ||
    value === "private_key_jwt"
  );
}

function readMetadataString(
  metadata: Record<string, unknown>,
  key: string,
): string {
  const value = metadata[key];
  return typeof value === "string" ? value : "";
}

function isClaimNamespace(value: string): value is ClaimNamespace {
  return value === "rp_claims";
}

function isClaimValueType(value: string): value is ClaimValueType {
  return (
    value === "text" ||
    value === "number" ||
    value === "boolean" ||
    value === "array" ||
    value === "object" ||
    value === "date" ||
    value === "datetime"
  );
}

function isCustomClaimPermission(
  value: unknown,
): value is CustomClaimPermission {
  return value === "admin_only" || value === "user_and_admin";
}

function createIdTokenClaimItem(id: string): IdTokenClaimItem {
  return {
    id,
    namespace: "rp_claims",
    key: "",
    value: "",
    valueType: "text",
    readPermission: "admin_only",
    writePermission: "admin_only",
  };
}

function readIdTokenClaimsMetadata(
  metadata: Record<string, unknown>,
): IdTokenClaimItem[] {
  const rawClaims = metadata.id_token_claims;
  if (!Array.isArray(rawClaims)) {
    return [];
  }

  return rawClaims
    .map((item, index) => {
      if (!item || typeof item !== "object") {
        return null;
      }

      const record = item as Record<string, unknown>;
      const namespaceValue =
        typeof record.namespace === "string" &&
        isClaimNamespace(record.namespace)
          ? record.namespace
          : null;
      if (namespaceValue === null) {
        return null;
      }
      const keyValue = typeof record.key === "string" ? record.key : "";
      const rawValue = record.value;
      const valueValue =
        typeof rawValue === "string"
          ? rawValue
          : rawValue == null
            ? ""
            : JSON.stringify(rawValue);
      const valueTypeValue =
        typeof record.valueType === "string" &&
        isClaimValueType(record.valueType)
          ? record.valueType
          : "text";

      return {
        id: `claim-${index + 1}`,
        namespace: namespaceValue,
        key: keyValue,
        value: valueValue,
        valueType: valueTypeValue,
        readPermission: isCustomClaimPermission(record.readPermission)
          ? record.readPermission
          : "admin_only",
        writePermission: isCustomClaimPermission(record.writePermission)
          ? record.writePermission
          : "admin_only",
      };
    })
    .filter((item): item is IdTokenClaimItem => item !== null);
}

function normalizeClaimPreviewValue(
  value: string,
  valueType: ClaimValueType,
): unknown {
  const trimmed = value.trim();
  if (valueType === "number") {
    if (trimmed === "") return "";
    const parsed = Number(trimmed);
    return Number.isFinite(parsed) ? parsed : trimmed;
  }

  if (valueType === "boolean") {
    return ["true", "1", "yes", "on"].includes(trimmed.toLowerCase());
  }

  if (valueType === "array") {
    if (trimmed === "") return [];
    try {
      if (trimmed.startsWith("[")) {
        const parsed = JSON.parse(trimmed);
        return Array.isArray(parsed) ? parsed : [parsed];
      }
    } catch {
      // Fall through to comma-separated parsing.
    }
    return trimmed
      .split(",")
      .map((part) => part.trim())
      .filter(Boolean);
  }

  if (valueType === "object") {
    if (trimmed === "") return {};
    try {
      const parsed = JSON.parse(trimmed);
      return parsed;
    } catch {
      return trimmed;
    }
  }

  return trimmed;
}

function buildIdTokenClaimsPreview(
  items: IdTokenClaimItem[],
): Record<string, unknown> {
  const preview: Record<string, unknown> = {};
  const rpClaims: Record<string, unknown> = {};

  for (const item of items) {
    const key = item.key.trim();
    if (!key) {
      continue;
    }

    rpClaims[key] = normalizeClaimPreviewValue(item.value, item.valueType);
  }

  if (Object.keys(rpClaims).length > 0) {
    preview.rp_claims = rpClaims;
  }

  return preview;
}

function isValidUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return url.protocol === "https:" || url.protocol === "http:";
  } catch {
    return false;
  }
}

function isValidBackchannelLogoutUrl(value: string): boolean {
  const trimmed = value.trim();
  if (!trimmed) {
    return true;
  }

  try {
    const url = new URL(trimmed);
    if (url.hash) {
      return false;
    }
    if (url.protocol === "https:") {
      return true;
    }
    if (url.protocol !== "http:") {
      return false;
    }
    const host = url.hostname.toLowerCase();
    if (
      host === "localhost" ||
      host === "127.0.0.1" ||
      host === "::1" ||
      host === "host.docker.internal"
    ) {
      return true;
    }
    if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
      return (
        host.startsWith("10.") ||
        host.startsWith("192.168.") ||
        /^172\.(1[6-9]|2\d|3[0-1])\./.test(host) ||
        host.startsWith("169.254.")
      );
    }
    // Docker service names and other single-label local hosts are allowed
    // only for HTTP local development use.
    return !host.includes(".");
  } catch {
    return false;
  }
}

function formatDateTime(value?: string) {
  if (!value) return "-";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return date.toLocaleString();
}

function ClientGeneralPage() {
  const auth = useAuth();
  const params = useParams();
  const navigate = useNavigate();
  const queryClient = useQueryClient();
  const hasAccessToken = Boolean(auth.user?.access_token);
  const clientId = params.id;
  const isCreate = !clientId;
  const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
  const systemRole = resolveProfileRole(userProfile);
  const { data: me, isLoading: isLoadingMe } = useQuery<UserProfile>({
    queryKey: ["userMe"],
    queryFn: fetchMe,
    enabled: hasAccessToken,
  });
  const currentUserId = me?.id ?? auth.user?.profile.sub;
  const effectiveSystemRole = me?.role?.trim() || systemRole;
  const {
    hasDeveloperAccess: hasClientCreateAccess,
    isDeveloperRequestPending,
    canRequestDeveloperAccess,
    isLoadingDeveloperAccessGate,
  } = useDeveloperAccessGate({
    hasAccessToken,
    profileRole: effectiveSystemRole,
    tenantId: userProfile?.tenant_id as string | undefined,
    requiredPages: ["client_create"],
    isLoadingIdentity: isLoadingMe,
  });
  const { data, isLoading, error } = useQuery({
    queryKey: ["client", clientId],
    queryFn: () => fetchClient(clientId as string),
    enabled: !isCreate,
  });
  const { data: relationData } = useQuery({
    queryKey: ["client-relations", clientId],
    queryFn: () => fetchClientRelations(clientId as string),
    enabled: !isCreate,
    retry: false,
  });
  const { data: tenantData } = useQuery({
    queryKey: ["my-tenants"],
    queryFn: fetchMyTenants,
  });

  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [logoUrl, setLogoUrl] = useState("");
  const [logoPreviewStatus, setLogoPreviewStatus] = useState<
    "idle" | "loading" | "loaded" | "error"
  >("idle");
  const [clientType, setClientType] = useState<ClientType>("private");
  const [status, setStatus] = useState<ClientStatus>("active");
  const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
  const [redirectUris, setRedirectUris] = useState("");
  const [backchannelLogoutUri, setBackchannelLogoutUri] = useState("");
  const [
    backchannelLogoutSessionRequired,
    setBackchannelLogoutSessionRequired,
  ] = useState(false);
  const [
    isBackchannelSessionRequiredInfoOpen,
    setIsBackchannelSessionRequiredInfoOpen,
  ] = useState(false);
  const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
  const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
  const [tenantSearch, setTenantSearch] = useState("");
  const [isTenantSearchOpen, setIsTenantSearchOpen] = useState(false);
  const [autoLoginSupported, setAutoLoginSupported] = useState(false);
  const [autoLoginUrl, setAutoLoginUrl] = useState("");

  // Public Key Registration States
  const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
    useState<TokenEndpointAuthMethod>("client_secret_basic");
  const [jwksUri, setJwksUri] = useState("");
  const [headlessLoginEnabled, setHeadlessLoginEnabled] = useState(false);

  const [scopes, setScopes] = useState<ScopeItem[]>(() => [
    {
      id: "1",
      name: "openid",
      description: t("msg.dev.clients.scopes.openid", "OIDC 인증 필수 스코프"),
      mandatory: true,
    },
    {
      id: "2",
      name: "tenant",
      description: t("msg.dev.clients.scopes.tenant", "소속 테넌트 정보 접근"),
      mandatory: false,
    },
    {
      id: "3",
      name: "profile",
      description: t("msg.dev.clients.scopes.profile", "기본 프로필 정보 접근"),
      mandatory: false,
    },
    {
      id: "4",
      name: "email",
      description: t("msg.dev.clients.scopes.email", "이메일 주소 접근"),
      mandatory: false,
    },
  ]);
  const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);

  const tenantScopeDescription = t(
    "msg.dev.clients.scopes.tenant",
    "소속 테넌트 정보 접근",
  );

  const buildTenantScope = useCallback(
    (id: string): ScopeItem => ({
      id,
      name: "tenant",
      description: tenantScopeDescription,
      mandatory: true,
      locked: true,
    }),
    [tenantScopeDescription],
  );

  const normalizeScopesForTenantAccess = useCallback(
    (nextScopes: ScopeItem[], restricted: boolean): ScopeItem[] => {
      const normalized = nextScopes.map((scope) => {
        if (scope.name.trim() !== "tenant") {
          return scope;
        }
        return {
          ...scope,
          description: scope.description || tenantScopeDescription,
          mandatory: restricted,
          locked: restricted,
        };
      });

      if (
        restricted &&
        !normalized.some((scope) => scope.name.trim() === "tenant")
      ) {
        normalized.push(buildTenantScope(`tenant-${Date.now()}`));
      }

      const openidScopes = normalized.filter(
        (scope) => scope.name.trim() === "openid",
      );
      const tenantScopes = normalized.filter(
        (scope) => scope.name.trim() === "tenant",
      );
      const remainingScopes = normalized.filter((scope) => {
        const name = scope.name.trim();
        return name !== "openid" && name !== "tenant";
      });

      return [...openidScopes, ...tenantScopes, ...remainingScopes];
    },
    [buildTenantScope, tenantScopeDescription],
  );

  useEffect(() => {
    if (!data) return;
    const { client } = data;
    const metadata = client.metadata ?? {};
    const headlessEnabled = !!metadata.headless_login_enabled;

    setName(client.name || client.id);
    setClientType(headlessEnabled ? "private" : client.type);
    setStatus(client.status);
    setInitialStatus(client.status);
    if (typeof metadata.description === "string")
      setDescription(metadata.description);
    if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
    setBackchannelLogoutUri(
      client.backchannelLogoutUri ||
        readMetadataString(metadata, "backchannel_logout_uri"),
    );
    setBackchannelLogoutSessionRequired(
      client.backchannelLogoutSessionRequired === true ||
        metadata.backchannel_logout_session_required === true,
    );
    setAutoLoginSupported(metadata.auto_login_supported === true);
    if (typeof metadata.auto_login_url === "string")
      setAutoLoginUrl(metadata.auto_login_url);

    setHeadlessLoginEnabled(headlessEnabled);
    const restrictedTenants = Array.isArray(metadata.allowed_tenants)
      ? metadata.allowed_tenants
          .map((value) => (typeof value === "string" ? value.trim() : ""))
          .filter(Boolean)
      : [];
    setTenantAccessRestricted(
      restrictedTenants.length > 0 ||
        metadata.tenant_access_restricted === true,
    );
    setAllowedTenantIds(restrictedTenants);

    const savedAuthMethod =
      client.tokenEndpointAuthMethod ||
      (client.type === "pkce" ? "none" : "client_secret_basic");
    const headlessAuthMethod = readMetadataString(
      metadata,
      "headless_token_endpoint_auth_method",
    );
    const selectedAuthMethod =
      headlessEnabled && isTokenEndpointAuthMethod(headlessAuthMethod)
        ? headlessAuthMethod
        : savedAuthMethod;
    if (isTokenEndpointAuthMethod(selectedAuthMethod)) {
      setTokenEndpointAuthMethod(selectedAuthMethod);
    }

    const headlessJwksUri = readMetadataString(metadata, "headless_jwks_uri");
    if (headlessJwksUri) {
      setJwksUri(headlessJwksUri);
    } else if (client.jwksUri) {
      setJwksUri(client.jwksUri);
    } else {
      setJwksUri("");
    }

    // Fallbacks from metadata if top-level fields are empty
    if (!client.tokenEndpointAuthMethod && !headlessEnabled) {
      const metaAuth = readMetadataString(
        metadata,
        "token_endpoint_auth_method",
      );
      if (isTokenEndpointAuthMethod(metaAuth)) {
        setTokenEndpointAuthMethod(metaAuth);
      }
    }

    if (!client.jwksUri && !headlessEnabled) {
      const metaJwksUri = readMetadataString(metadata, "jwks_uri");
      if (metaJwksUri) {
        setJwksUri(metaJwksUri);
      }
    }

    const savedScopes = metadata.structured_scopes as ScopeItem[] | undefined;
    if (savedScopes && Array.isArray(savedScopes)) {
      setScopes(
        normalizeScopesForTenantAccess(
          savedScopes,
          restrictedTenants.length > 0 ||
            metadata.tenant_access_restricted === true,
        ),
      );
    } else {
      setScopes(
        normalizeScopesForTenantAccess(
          client.scopes.map((s, idx) => ({
            id: String(idx + 1),
            name: s,
            description: "",
            mandatory: s === "openid",
          })),
          restrictedTenants.length > 0 ||
            metadata.tenant_access_restricted === true,
        ),
      );
    }
    setIdTokenClaims(readIdTokenClaimsMetadata(metadata));
  }, [data, normalizeScopesForTenantAccess]);

  const securityProfile: SecurityProfile =
    clientType === "pkce" ? "pkce" : "private";
  const canEditExistingClientGeneralSettings =
    effectiveSystemRole === "super_admin" ||
    relationData?.items?.some(
      (item: ClientRelation) =>
        item.subject === `User:${currentUserId}` &&
        (item.relation === "admins" || item.relation === "config_editor"),
    ) === true;
  const isGeneralSettingsReadOnly =
    !isCreate && relationData != null && !canEditExistingClientGeneralSettings;
  const trimmedLogoUrl = logoUrl.trim();
  const trimmedAutoLoginUrl = autoLoginUrl.trim();
  const hasLogoUrl = trimmedLogoUrl.length > 0;
  const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
  const trimmedBackchannelLogoutUri = backchannelLogoutUri.trim();
  const hasBackchannelLogoutUri = trimmedBackchannelLogoutUri.length > 0;
  const hasValidBackchannelLogoutUri =
    !hasBackchannelLogoutUri ||
    isValidBackchannelLogoutUrl(trimmedBackchannelLogoutUri);
  const hasValidAutoLoginUrl =
    !autoLoginSupported ||
    (trimmedAutoLoginUrl.length > 0 && isValidUrl(trimmedAutoLoginUrl));

  useEffect(() => {
    if (!hasLogoUrl) {
      setLogoPreviewStatus("idle");
      return;
    }
    if (!hasValidLogoUrl) {
      setLogoPreviewStatus("error");
      return;
    }
    setLogoPreviewStatus("loading");
  }, [hasLogoUrl, hasValidLogoUrl]);

  const handleSecurityProfileChange = (profile: SecurityProfile) => {
    setClientType(profile);
    if (profile === "pkce") {
      setHeadlessLoginEnabled(false);
      setTokenEndpointAuthMethod("none");
    } else {
      setTokenEndpointAuthMethod(
        headlessLoginEnabled ? "private_key_jwt" : "client_secret_basic",
      );
    }
  };

  const handleHeadlessToggle = (enabled: boolean) => {
    setHeadlessLoginEnabled(enabled);
    if (enabled) {
      setClientType("private");
      setTokenEndpointAuthMethod("private_key_jwt");
      return;
    }

    if (clientType === "private") {
      setTokenEndpointAuthMethod("client_secret_basic");
    }
  };

  const handleTenantAccessToggle = (enabled: boolean) => {
    setTenantAccessRestricted(enabled);
    setIsTenantSearchOpen(enabled);
    if (!enabled) {
      setTenantSearch("");
    }
    setScopes((current) => normalizeScopesForTenantAccess(current, enabled));
  };

  const toggleAllowedTenant = (tenantId: string) => {
    setAllowedTenantIds((current) =>
      current.includes(tenantId)
        ? current.filter((id) => id !== tenantId)
        : [...current, tenantId],
    );
  };

  const handleSelectAllowedTenant = (tenantId: string) => {
    setAllowedTenantIds((current) =>
      current.includes(tenantId) ? current : [...current, tenantId],
    );
    setTenantSearch("");
    setIsTenantSearchOpen(true);
  };

  const addScope = () => {
    const newId = String(Date.now());
    setScopes([
      ...scopes,
      { id: newId, name: "", description: "", mandatory: false },
    ]);
  };

  const updateScope = <K extends keyof ScopeItem>(
    id: string,
    field: K,
    value: ScopeItem[K],
  ) => {
    setScopes((current) =>
      current.map((scope) => {
        if (scope.id !== id) {
          return scope;
        }
        if (scope.locked) {
          return scope;
        }
        return { ...scope, [field]: value };
      }),
    );
  };

  const removeScope = (id: string) => {
    setScopes((current) =>
      current.filter((scope) => scope.id !== id || scope.locked === true),
    );
  };

  const addIdTokenClaim = () => {
    setIdTokenClaims((current) => [
      ...current,
      createIdTokenClaimItem(`claim-${Date.now()}`),
    ]);
  };

  const updateIdTokenClaim = <K extends keyof IdTokenClaimItem>(
    id: string,
    field: K,
    value: IdTokenClaimItem[K],
  ) => {
    setIdTokenClaims((current) =>
      current.map((claim) => {
        if (claim.id !== id) {
          return claim;
        }
        return { ...claim, [field]: value };
      }),
    );
  };

  const removeIdTokenClaim = (id: string) => {
    setIdTokenClaims((current) => current.filter((claim) => claim.id !== id));
  };

  const handleStatusChange = (nextStatus: ClientStatus) => {
    setStatus(nextStatus);
    const statusLabel =
      nextStatus === "active"
        ? t("ui.common.status.active", "Active")
        : t("ui.common.status.inactive", "Inactive");
    toast(
      t(
        "msg.dev.clients.general.status_changed",
        "상태가 {{status}}로 변경되었습니다.",
        { status: statusLabel },
      ),
    );
  };

  const validationErrors: string[] = [];
  const trimmedJwksUri = jwksUri.trim();
  const currentHeadlessJwksCache = data?.headlessJwksCache;
  const parsedKeysForCurrentJwksUri =
    headlessLoginEnabled &&
    trimmedJwksUri !== "" &&
    currentHeadlessJwksCache?.jwksUri === trimmedJwksUri
      ? (currentHeadlessJwksCache.parsedKeys ?? [])
      : [];
  const unsupportedParsedAlgorithms = parsedKeysForCurrentJwksUri
    .map((key, index) => ({
      alg: key.alg?.trim() ?? "",
      label: formatHeadlessParsedKeyLabel(key.kid, index),
    }))
    .filter(
      (entry) =>
        entry.alg !== "" &&
        !HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(entry.alg),
    );
  const missingParsedAlgorithms = parsedKeysForCurrentJwksUri
    .map((key, index) => ({
      alg: key.alg?.trim() ?? "",
      label: formatHeadlessParsedKeyLabel(key.kid, index),
    }))
    .filter((entry) => entry.alg === "");
  const unsupportedParsedAlgorithmSummary = unsupportedParsedAlgorithms
    .map((entry) => `${entry.label}: ${entry.alg}`)
    .join(", ");
  const missingParsedAlgorithmSummary = missingParsedAlgorithms
    .map((entry) => entry.label)
    .join(", ");
  const allowedHeadlessAlgorithmsTooltip = t(
    "msg.dev.clients.general.public_key.allowed_algorithms_tooltip",
    "허용 알고리즘: {{algorithms}}",
    { algorithms: HEADLESS_LOGIN_ALLOWED_ALGORITHMS.join(", ") },
  );
  const normalizedIdTokenClaims = idTokenClaims.map((claim) => ({
    ...claim,
    key: claim.key.trim(),
    value: claim.value.trim(),
  }));

  if (headlessLoginEnabled) {
    if (!trimmedJwksUri) {
      validationErrors.push(
        t(
          "msg.dev.clients.general.public_key.validation.missing_jwks_uri",
          "JWKS URI를 입력해야 합니다.",
        ),
      );
    } else if (!isValidUrl(trimmedJwksUri)) {
      validationErrors.push(
        t(
          "msg.dev.clients.general.public_key.validation.invalid_jwks_uri",
          "JWKS URI 형식이 올바르지 않습니다.",
        ),
      );
    }
    if (unsupportedParsedAlgorithms.length > 0) {
      validationErrors.push(
        t(
          "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms",
          "JWKS에 지원하지 않는 알고리즘이 있습니다: {{details}}",
          { details: unsupportedParsedAlgorithmSummary },
        ),
      );
    }
    if (missingParsedAlgorithms.length > 0) {
      validationErrors.push(
        t(
          "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms",
          "JWKS에 알고리즘(`alg`)이 선언되지 않은 키가 있습니다: {{details}}",
          { details: missingParsedAlgorithmSummary },
        ),
      );
    }
  }

  if (tenantAccessRestricted && allowedTenantIds.length === 0) {
    validationErrors.push(
      t(
        "ui.dev.clients.general.tenant_access.validation_required",
        "테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다.",
      ),
    );
  }
  if (autoLoginSupported && !hasValidAutoLoginUrl) {
    validationErrors.push(
      t(
        "msg.dev.clients.general.auto_login.invalid_url",
        "자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
      ),
    );
  }

  const claimValidationErrors: string[] = [];
  const seenClaimKeys = new Set<string>();
  for (const claim of normalizedIdTokenClaims) {
    if (!claim.key) {
      claimValidationErrors.push(
        t(
          "msg.dev.clients.general.id_token_claims.key_required",
          "Claim key를 입력해야 합니다.",
        ),
      );
      continue;
    }

    const keySignature = `${claim.namespace}:${claim.key}`;
    if (seenClaimKeys.has(keySignature)) {
      claimValidationErrors.push(
        t(
          "msg.dev.clients.general.id_token_claims.duplicate_key",
          "중복된 claim key가 있습니다: {{namespace}}.{{key}}",
          {
            namespace: t(
              "ui.dev.clients.general.id_token_claims.namespace_rp_claims",
              "rp_claims",
            ),
            key: claim.key,
          },
        ),
      );
      continue;
    }
    seenClaimKeys.add(keySignature);
  }
  validationErrors.push(...claimValidationErrors);

  const hasValidationErrors = validationErrors.length > 0;
  const idTokenClaimPreview = buildIdTokenClaimsPreview(
    normalizedIdTokenClaims,
  );
  const idTokenClaimPreviewJson = JSON.stringify(idTokenClaimPreview, null, 2);
  const normalizedTenantSearch = tenantSearch.trim().toLowerCase();
  const tenantOptions: Array<TenantSummary | MyTenantSummary> =
    tenantData ?? [];
  const filteredTenants = tenantOptions.filter((tenant) => {
    if (!normalizedTenantSearch) {
      return true;
    }
    const searchable =
      `${tenant.name} ${tenant.slug} ${tenant.description ?? ""} ${tenant.type ?? ""}`.toLowerCase();
    return searchable.includes(normalizedTenantSearch);
  });
  const tenantSuggestions = filteredTenants
    .filter((tenant) => !allowedTenantIds.includes(tenant.id))
    .slice(0, 8);
  const selectedAllowedTenants = allowedTenantIds
    .map((tenantId) => tenantOptions.find((item) => item.id === tenantId))
    .filter(
      (tenant): tenant is TenantSummary | MyTenantSummary => tenant != null,
    );

  const refreshHeadlessJwksCacheMutation = useMutation({
    mutationFn: async () => {
      if (!clientId) throw new Error("Missing client id");
      return refreshHeadlessJwksCache(clientId);
    },
    onSuccess: (result) => {
      if (clientId) {
        queryClient.setQueryData(["client", clientId], result);
        queryClient.invalidateQueries({ queryKey: ["client", clientId] });
      }
      toast(
        t(
          "msg.dev.clients.general.public_key.cache_refreshed",
          "JWKS 캐시를 새로 고쳤습니다.",
        ),
      );
    },
    onError: (err) => {
      const errorMessage =
        (err as AxiosError<{ error?: string }>).response?.data?.error ??
        (err as Error)?.message ??
        t("msg.common.unknown_error", "unknown error");
      toast(
        t(
          "msg.dev.clients.general.public_key.cache_refresh_failed",
          "JWKS 캐시 새로고침에 실패했습니다: {{error}}",
          { error: errorMessage },
        ),
      );
    },
  });

  const revokeHeadlessJwksCacheMutation = useMutation({
    mutationFn: async () => {
      if (!clientId) throw new Error("Missing client id");
      return revokeHeadlessJwksCache(clientId);
    },
    onSuccess: () => {
      if (clientId) {
        queryClient.invalidateQueries({ queryKey: ["client", clientId] });
      }
      toast(
        t(
          "msg.dev.clients.general.public_key.cache_revoked",
          "JWKS 캐시를 삭제했습니다.",
        ),
      );
    },
    onError: (err) => {
      const errorMessage =
        (err as AxiosError<{ error?: string }>).response?.data?.error ??
        (err as Error)?.message ??
        t("msg.common.unknown_error", "unknown error");
      toast(
        t(
          "msg.dev.clients.general.public_key.cache_revoke_failed",
          "JWKS 캐시 삭제에 실패했습니다: {{error}}",
          { error: errorMessage },
        ),
      );
    },
  });

  const mutation = useMutation({
    mutationFn: async () => {
      if (hasLogoUrl && !hasValidLogoUrl) {
        throw new Error(
          t(
            "msg.dev.clients.general.identity.logo_invalid",
            "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
          ),
        );
      }
      if (hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri) {
        throw new Error(
          t(
            "msg.dev.clients.general.backchannel_logout.invalid",
            "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https를 사용하고, 로컬 개발 환경은 localhost/127.0.0.1, host.docker.internal, Docker 서비스명, 사설 IP의 http만 허용됩니다.",
          ),
        );
      }
      if (isGeneralSettingsReadOnly) {
        throw new Error(
          t(
            "msg.dev.clients.general.read_only_forbidden",
            "이 RP의 일반 설정을 수정할 권한이 없습니다.",
          ),
        );
      }
      if (autoLoginSupported && !hasValidAutoLoginUrl) {
        throw new Error(
          t(
            "msg.dev.clients.general.auto_login.invalid_url",
            "자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
          ),
        );
      }

      const normalizedScopes = normalizeScopesForTenantAccess(
        scopes,
        tenantAccessRestricted,
      );
      const normalizedAllowedTenantIds = Array.from(
        new Set(allowedTenantIds.map((id) => id.trim()).filter(Boolean)),
      );
      const scopeNames = normalizedScopes
        .map((scope) => scope.name.trim())
        .filter(Boolean);

      const persistedClientType = headlessLoginEnabled ? "private" : clientType;
      const effectiveTokenEndpointAuthMethod = tokenEndpointAuthMethod;

      const payload: ClientUpsertRequest = {
        name,
        type: persistedClientType,
        scopes: scopeNames,
        tokenEndpointAuthMethod: effectiveTokenEndpointAuthMethod,
        jwksUri:
          (headlessLoginEnabled ||
            effectiveTokenEndpointAuthMethod === "private_key_jwt") &&
          trimmedJwksUri
            ? trimmedJwksUri
            : undefined,
        backchannelLogoutUri: trimmedBackchannelLogoutUri || undefined,
        backchannelLogoutSessionRequired:
          trimmedBackchannelLogoutUri !== ""
            ? backchannelLogoutSessionRequired
            : false,
        metadata: {
          description,
          logo_url: trimmedLogoUrl,
          auto_login_supported: autoLoginSupported,
          auto_login_url: autoLoginSupported ? trimmedAutoLoginUrl : undefined,
          structured_scopes: normalizedScopes,
          id_token_claims: normalizedIdTokenClaims,
          token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
          headless_login_enabled: headlessLoginEnabled,
          headless_token_endpoint_auth_method: headlessLoginEnabled
            ? tokenEndpointAuthMethod
            : undefined,
          headless_jwks_uri: headlessLoginEnabled ? trimmedJwksUri : undefined,
          tenant_access_restricted: tenantAccessRestricted,
          allowed_tenants: tenantAccessRestricted
            ? normalizedAllowedTenantIds
            : [],
          backchannel_logout_uri: trimmedBackchannelLogoutUri || undefined,
          backchannel_logout_session_required:
            trimmedBackchannelLogoutUri !== ""
              ? backchannelLogoutSessionRequired
              : undefined,
        },
      };

      if (isCreate) {
        payload.status = status;
        payload.redirectUris = redirectUris
          .split(",")
          .map((uri) => uri.trim())
          .filter(Boolean);
        return createClient(payload);
      }

      const updated = await updateClient(clientId as string, payload);
      if (status !== initialStatus) {
        await updateClientStatus(clientId as string, status);
      }
      return updated;
    },
    onSuccess: (result) => {
      queryClient.invalidateQueries({ queryKey: ["clients"] });
      if (status !== initialStatus) {
        setInitialStatus(status);
      }
      if (result?.client?.id) {
        navigate(`/clients/${result.client.id}/settings`);
      }
      toast(t("msg.dev.clients.general.saved", "설정이 저장되었습니다."));
    },
    onError: (err) => {
      const axiosError = err as AxiosError<{ error?: string }>;
      if (axiosError.response?.status === 403) {
        alert(
          isCreate
            ? t(
                "msg.dev.clients.general.create_forbidden",
                "이 RP를 생성할 권한이 없습니다.\n관리자에게 개발자 권한 부여를 요청해 주세요.",
              )
            : t(
                "msg.dev.clients.general.save_forbidden",
                "이 RP 설정을 수정할 권한이 없습니다.\n관리자에게 RP 일반 설정 또는 RP 관리자 관계 부여를 요청해 주세요.",
              ),
        );
        return;
      }

      const errorMessage =
        axiosError.response?.data?.error ??
        (err as Error)?.message ??
        t("msg.common.unknown_error", "unknown error");
      toast(
        t(
          "msg.dev.clients.general.save_error",
          "저장에 실패했습니다: {{error}}",
          {
            error: errorMessage,
          },
        ),
        "error",
      );
    },
  });

  const deleteMutation = useMutation({
    mutationFn: (id: string) => deleteClient(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["clients"] });
      toast(t("msg.dev.clients.deleted", "앱이 삭제되었습니다."));
      navigate("/clients");
    },
    onError: (err) => {
      const errorMessage =
        (err as AxiosError<{ error?: string }>).response?.data?.error ??
        (err as Error)?.message;
      toast(
        t("msg.dev.clients.delete_error", "삭제 실패: {{error}}", {
          error: errorMessage,
        }),
        "error",
      );
    },
  });

  const handleDelete = () => {
    if (
      clientId &&
      window.confirm(
        t(
          "msg.dev.clients.delete_confirm",
          "정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.",
        ),
      )
    ) {
      deleteMutation.mutate(clientId);
    }
  };

  if ((isCreate && isLoadingDeveloperAccessGate) || (!isCreate && isLoading)) {
    return (
      <div className="p-8 text-center">
        {t(
          "msg.dev.clients.general.loading",
          isCreate ? "Loading client creation..." : "Loading client...",
        )}
      </div>
    );
  }
  if (isCreate && !hasClientCreateAccess) {
    return (
      <div className="p-8">
        <div className="mx-auto max-w-2xl">
          <DeveloperAccessRequestCard
            title={t("ui.dev.clients.general.title_create", "Create Client")}
            isPending={isDeveloperRequestPending}
            canRequest={canRequestDeveloperAccess}
            pendingMessage={t(
              "msg.dev.clients.general.create_pending",
              "개발자 권한 신청을 검토 중입니다.",
            )}
            deniedMessage={t(
              "msg.dev.clients.general.create_forbidden",
              "이 RP를 생성할 권한이 없습니다.",
            )}
            pendingDetailMessage={t(
              "msg.dev.clients.general.create_pending_detail",
              "super admin이 승인하면 연동 앱을 추가할 수 있습니다.",
            )}
            deniedDetailMessage={t(
              "msg.dev.clients.general.create_forbidden_detail",
              "개발자 권한 신청에서 연동 앱 추가 권한을 선택한 뒤 승인받아주세요.",
            )}
            actionLabel={t(
              "ui.dev.welcome.btn_request",
              "개발자 등록 신청하기",
            )}
            onAction={() => navigate("/developer-requests")}
          />
        </div>
      </div>
    );
  }
  if (!isCreate && (error || !data)) {
    const errMsg =
      (error as AxiosError<{ error?: string }>).response?.data?.error ??
      (error as Error)?.message;
    return (
      <div className="p-8 text-center text-red-500">
        {t(
          "msg.dev.clients.general.load_error",
          "Error loading client: {{error}}",
          {
            error: errMsg || t("msg.common.unknown_error", "unknown error"),
          },
        )}
      </div>
    );
  }

  const publicKeyStatusTone = headlessLoginEnabled
    ? hasValidationErrors
      ? "border-destructive/40 bg-destructive/5"
      : "border-primary/30 bg-primary/5"
    : "border-border bg-muted/20";

  const displayName = isCreate
    ? t("ui.dev.clients.general.display_new", "새 클라이언트")
    : data?.client?.name || data?.client?.id;

  return (
    <div className="space-y-8">
      <header className="space-y-4">
        <div className="flex flex-wrap items-start justify-between gap-4">
          <div className="space-y-2">
            <nav className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
              <Link to="/" className="hover:text-primary">
                {t("ui.dev.clients.consents.breadcrumb.home", "Home")}
              </Link>
              <span>/</span>
              <Link to="/clients" className="hover:text-primary">
                {t("ui.dev.clients.consents.breadcrumb.clients", "Apps")}
              </Link>
              <span>/</span>
              <span>{displayName}</span>
              {!isCreate && (
                <>
                  <span>/</span>
                  <span className="text-foreground font-semibold">
                    {t("ui.dev.clients.details.tab.settings", "Settings")}
                  </span>
                </>
              )}
            </nav>
            <PageHeader
              icon={<ShieldHalf size={20} />}
              title={
                isCreate
                  ? t("ui.dev.clients.general.title_create", "Create Client")
                  : t("ui.dev.clients.general.title_edit", "Client Settings")
              }
              description={t(
                "ui.dev.clients.general.subtitle",
                "앱 정보, 권한 스코프, 보안 설정을 관리합니다.",
              )}
            />
          </div>
          {!isCreate && (
            <Badge
              variant={status === "active" ? "info" : "muted"}
              className="px-3 py-1 text-xs uppercase"
            >
              {status === "active"
                ? t("ui.common.status.active", "Active")
                : t("ui.common.status.inactive", "Inactive")}
            </Badge>
          )}
        </div>
        {!isCreate && (
          <ClientDetailTabs activeTab="settings" clientId={clientId} />
        )}
        {isGeneralSettingsReadOnly && (
          <div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-700 dark:text-amber-200">
            {t(
              "msg.dev.clients.general.read_only_hint",
              "이 RP의 일반 설정은 `RP 관리자` 또는 `RP 일반 설정` 관계가 있는 사용자만 수정할 수 있습니다.",
            )}
          </div>
        )}
      </header>

      {/* 1. Application Identity */}
      <div className="glass-panel p-6">
        <div className="flex items-center justify-between mb-6">
          <div>
            <CardTitle className="text-xl font-bold mb-2">
              {t(
                "ui.dev.clients.general.identity.title",
                "Application Identity",
              )}
            </CardTitle>
            <CardDescription>
              {t(
                "msg.dev.clients.general.identity.subtitle",
                "앱 이름과 설명, 로고를 설정합니다.",
              )}
            </CardDescription>
          </div>
        </div>
        <div className="grid gap-8 md:grid-cols-2">
          <div className="space-y-5">
            <div className="space-y-2">
              <Label className="text-sm font-semibold">
                {t("ui.dev.clients.general.identity.name", "앱 이름")}{" "}
                <span className="text-destructive">*</span>
              </Label>
              <Input
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder={t(
                  "ui.dev.clients.general.identity.name_placeholder",
                  "My Awesome Application",
                )}
                disabled={isGeneralSettingsReadOnly}
              />
            </div>
            <div className="space-y-2">
              <Label className="text-sm font-semibold">
                {t(
                  "ui.dev.clients.general.identity.description",
                  "Description",
                )}
              </Label>
              <Textarea
                rows={3}
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                placeholder={t(
                  "ui.dev.clients.general.identity.description_placeholder",
                  "앱에 대한 간단한 설명을 입력하세요.",
                )}
                disabled={isGeneralSettingsReadOnly}
              />
            </div>
          </div>
          <div className="space-y-5">
            <div className="space-y-2">
              <Label className="text-sm font-semibold">
                {t("ui.dev.clients.general.identity.logo", "App Logo URL")}
              </Label>
              <div className="flex gap-4">
                <div className="flex-1 space-y-2">
                  <Input
                    value={logoUrl}
                    onChange={(e) => setLogoUrl(e.target.value)}
                    aria-invalid={!hasValidLogoUrl}
                    className={!hasValidLogoUrl ? "border-destructive" : ""}
                    placeholder={t(
                      "ui.dev.clients.general.identity.logo_placeholder",
                      "https://example.com/logo.png",
                    )}
                    disabled={isGeneralSettingsReadOnly}
                  />
                  <p className="text-xs text-muted-foreground">
                    {t(
                      "msg.dev.clients.general.identity.logo_help",
                      "인증 화면에 표시될 PNG/SVG URL입니다.",
                    )}
                  </p>
                  {!hasValidLogoUrl ? (
                    <p className="text-xs text-destructive">
                      {t(
                        "msg.dev.clients.general.identity.logo_invalid",
                        "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
                      )}
                    </p>
                  ) : null}
                  {hasLogoUrl && hasValidLogoUrl ? (
                    <div className="flex items-center gap-2 text-xs">
                      <span
                        className={cn("text-muted-foreground", {
                          "text-foreground": logoPreviewStatus === "loaded",
                          "text-destructive": logoPreviewStatus === "error",
                        })}
                      >
                        {logoPreviewStatus === "loading"
                          ? t(
                              "msg.dev.clients.general.identity.logo_preview_loading",
                              "로고 미리보기를 불러오는 중입니다.",
                            )
                          : logoPreviewStatus === "loaded"
                            ? t(
                                "msg.dev.clients.general.identity.logo_preview_ready",
                                "로고 미리보기를 확인했습니다.",
                              )
                            : logoPreviewStatus === "error"
                              ? t(
                                  "msg.dev.clients.general.identity.logo_preview_failed",
                                  "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요.",
                                )
                              : null}
                      </span>
                      <a
                        href={trimmedLogoUrl}
                        target="_blank"
                        rel="noreferrer"
                        className="inline-flex items-center gap-1 text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
                      >
                        <ExternalLink className="h-3 w-3" />
                        {t(
                          "ui.dev.clients.general.identity.logo_open",
                          "새 탭에서 열기",
                        )}
                      </a>
                    </div>
                  ) : null}
                </div>
                <div
                  className={cn(
                    "flex h-20 w-20 shrink-0 items-center justify-center rounded-lg border-2 border-dashed",
                    hasLogoUrl &&
                      hasValidLogoUrl &&
                      logoPreviewStatus !== "error"
                      ? "bg-white"
                      : "bg-muted/40",
                    logoPreviewStatus === "error"
                      ? "border-destructive/60"
                      : "border-border",
                  )}
                >
                  {hasLogoUrl && hasValidLogoUrl ? (
                    <img
                      key={trimmedLogoUrl}
                      src={trimmedLogoUrl}
                      alt={t(
                        "ui.dev.clients.general.identity.logo_preview",
                        "Logo Preview",
                      )}
                      className="h-full w-full object-contain"
                      onLoad={() => setLogoPreviewStatus("loaded")}
                      onError={() => setLogoPreviewStatus("error")}
                    />
                  ) : (
                    <div className="flex flex-col items-center justify-center gap-1 px-2 text-center">
                      <Upload
                        className={cn("h-5 w-5 text-muted-foreground", {
                          "text-destructive": logoPreviewStatus === "error",
                        })}
                      />
                      {logoPreviewStatus === "error" ? (
                        <span className="text-[10px] leading-tight text-destructive">
                          {t(
                            "ui.dev.clients.general.identity.logo_preview_error_badge",
                            "미리보기 실패",
                          )}
                        </span>
                      ) : (
                        <span className="text-[10px] leading-tight text-muted-foreground">
                          {t(
                            "ui.dev.clients.general.identity.logo_preview_empty",
                            "미리보기",
                          )}
                        </span>
                      )}
                    </div>
                  )}
                </div>
              </div>
            </div>

            <div className="space-y-2">
              <Label className="text-sm font-semibold">
                {t("ui.dev.clients.table.status", "상태")}
              </Label>
              <div className="flex gap-2">
                <Button
                  type="button"
                  size="sm"
                  variant={status === "active" ? "default" : "outline"}
                  onClick={() => handleStatusChange("active")}
                  disabled={isGeneralSettingsReadOnly}
                >
                  {t("ui.common.status.active", "활성")}
                </Button>
                <Button
                  type="button"
                  size="sm"
                  variant={status === "inactive" ? "default" : "outline"}
                  onClick={() => handleStatusChange("inactive")}
                  disabled={isGeneralSettingsReadOnly}
                >
                  {t("ui.common.status.inactive", "비활성")}
                </Button>
              </div>
            </div>
          </div>
        </div>
      </div>

      <Card className="glass-panel">
        <CardHeader className="pb-4">
          <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
            <div className="space-y-1">
              <CardTitle className="text-xl font-bold">
                {t("ui.dev.clients.general.auto_login.title", "자동 로그인")}
              </CardTitle>
              <CardDescription>
                {t(
                  "msg.dev.clients.general.auto_login.subtitle",
                  "RP가 자체 로그인 시작 URL에서 OIDC 요청을 만들 수 있으면 userfront에서 바로 로그인 진입을 제공합니다.",
                )}
              </CardDescription>
            </div>
            <div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
              <div className="space-y-0.5 text-right">
                <p className="text-sm font-semibold">
                  {autoLoginSupported
                    ? t("ui.common.enabled", "사용")
                    : t("ui.common.disabled", "사용 안 함")}
                </p>
                <p className="text-xs text-muted-foreground">
                  {t(
                    "ui.dev.clients.general.auto_login.supported",
                    "자동 로그인 지원",
                  )}
                </p>
              </div>
              <Switch
                checked={autoLoginSupported}
                onCheckedChange={setAutoLoginSupported}
                id="auto-login-supported"
                aria-label={t(
                  "ui.dev.clients.general.auto_login.supported",
                  "자동 로그인 지원",
                )}
              />
            </div>
          </div>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="auto-login-url" className="text-sm font-semibold">
              {t(
                "ui.dev.clients.general.auto_login.url",
                "자동 로그인 시작 URL",
              )}
            </Label>
            <Input
              id="auto-login-url"
              value={autoLoginUrl}
              onChange={(event) => setAutoLoginUrl(event.target.value)}
              disabled={!autoLoginSupported}
              aria-invalid={!hasValidAutoLoginUrl}
              className={!hasValidAutoLoginUrl ? "border-destructive" : ""}
              placeholder={t(
                "ui.dev.clients.general.auto_login.url_placeholder",
                "https://app.example.com/login?auto=1",
              )}
            />
            <p className="text-xs text-muted-foreground">
              {t(
                "msg.dev.clients.general.auto_login.help",
                "이 URL은 RP가 state, nonce, PKCE 값을 직접 생성한 뒤 Baron OIDC로 리다이렉트해야 합니다.",
              )}
            </p>
            {!hasValidAutoLoginUrl ? (
              <p className="text-xs text-destructive">
                {t(
                  "msg.dev.clients.general.auto_login.invalid_url",
                  "자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
                )}
              </p>
            ) : null}
          </div>
        </CardContent>
      </Card>

      {/* 2. Scopes */}
      <Card className="glass-panel">
        <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
          <div>
            <CardTitle className="text-xl font-bold">
              {t("ui.dev.clients.general.scopes.title", "Scopes")}
            </CardTitle>
            <CardDescription>
              {t(
                "msg.dev.clients.general.scopes.subtitle",
                "이 클라이언트가 요청할 수 있는 권한 범위를 정의합니다.",
              )}
            </CardDescription>
          </div>
          <Button
            variant="outline"
            size="sm"
            onClick={addScope}
            className="gap-2"
            disabled={isGeneralSettingsReadOnly}
          >
            <Plus className="h-4 w-4" />
            {t("ui.dev.clients.general.scopes.add", "Scope 추가")}
          </Button>
        </CardHeader>
        <CardContent className="space-y-6">
          {isCreate && (
            <div className="space-y-2 border-b border-border pb-6 mb-6">
              <Label className="text-sm font-semibold">
                {t("ui.dev.clients.general.redirect.label", "Redirect URIs")}{" "}
                <span className="text-destructive">*</span>
              </Label>
              <Textarea
                value={redirectUris}
                onChange={(e) => setRedirectUris(e.target.value)}
                placeholder={t(
                  "ui.dev.clients.general.redirect.placeholder",
                  "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)",
                )}
                className="font-mono text-sm"
                disabled={isGeneralSettingsReadOnly}
              />
              <p className="text-xs text-muted-foreground">
                {t(
                  "msg.dev.clients.general.redirect.help",
                  "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connection 탭에서 수정 가능합니다.",
                )}
              </p>
            </div>
          )}

          <div className="space-y-4 border-b border-border pb-6 mb-6">
            <div className="space-y-2">
              <Label
                className="text-sm font-semibold"
                htmlFor="backchannel-logout-uri"
              >
                {t(
                  "ui.dev.clients.general.backchannel_logout.uri",
                  "Back-Channel Logout URI",
                )}
              </Label>
              <Input
                id="backchannel-logout-uri"
                value={backchannelLogoutUri}
                onChange={(e) => setBackchannelLogoutUri(e.target.value)}
                placeholder={t(
                  "ui.dev.clients.general.backchannel_logout.uri_placeholder",
                  "https://rp.example.com/oidc/backchannel-logout",
                )}
                className="font-mono text-sm"
                disabled={isGeneralSettingsReadOnly}
              />
              <p className="text-xs text-muted-foreground">
                {t(
                  "msg.dev.clients.general.backchannel_logout.uri_help",
                  "Baron이 세션 종료 이벤트를 서버 간 POST로 전달할 RP endpoint입니다.",
                )}
              </p>
              {hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri ? (
                <p className="text-xs text-destructive">
                  {t(
                    "msg.dev.clients.general.backchannel_logout.invalid",
                    "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https를 사용하고, 로컬 개발 환경은 localhost/127.0.0.1, host.docker.internal, Docker 서비스명, 사설 IP의 http만 허용됩니다.",
                  )}
                </p>
              ) : null}
            </div>

            <div className="flex items-center justify-between rounded-lg border border-border bg-muted/20 px-4 py-3">
              <div className="space-y-1">
                <div className="flex items-center gap-2">
                  <Label
                    className="text-sm font-semibold"
                    htmlFor="backchannel-logout-session-required"
                  >
                    {t(
                      "ui.dev.clients.general.backchannel_logout.session_required",
                      "SID Claim Required",
                    )}
                  </Label>
                  <button
                    type="button"
                    className={`rounded-full p-0.5 transition-colors ${
                      isBackchannelSessionRequiredInfoOpen
                        ? "text-primary"
                        : "text-muted-foreground/60 hover:text-primary"
                    }`}
                    onClick={() =>
                      setIsBackchannelSessionRequiredInfoOpen((prev) => !prev)
                    }
                    aria-label={t(
                      "ui.dev.clients.general.backchannel_logout.session_required_info",
                      "SID Claim Required 설명 보기",
                    )}
                  >
                    {isBackchannelSessionRequiredInfoOpen ? (
                      <X className="h-3.5 w-3.5" />
                    ) : (
                      <Info className="h-3.5 w-3.5" />
                    )}
                  </button>
                </div>
                <p className="text-xs text-muted-foreground">
                  {t(
                    "msg.dev.clients.general.backchannel_logout.session_required_help",
                    "RP가 logout_token에 sid claim이 포함된 경우에만 처리하도록 요구할 때 사용합니다.",
                  )}
                </p>
                {isBackchannelSessionRequiredInfoOpen ? (
                  <div className="mt-2 animate-in fade-in slide-in-from-top-1 rounded-lg border border-primary/20 bg-primary/5 p-3 text-xs leading-relaxed text-foreground shadow-sm">
                    <div className="flex items-center gap-1.5 font-bold text-primary mb-1">
                      <Info className="h-3 w-3" />
                      {t("ui.common.info", "상세 안내")}
                    </div>
                    <div>
                      {t(
                        "msg.dev.clients.general.backchannel_logout.session_required_on",
                        "켜면: logout_token 안에 sid가 있을 때만 로그아웃 처리",
                      )}
                    </div>
                    <div>
                      {t(
                        "msg.dev.clients.general.backchannel_logout.session_required_off",
                        "끄면: sid가 없어도 sub만으로 로그아웃 처리 가능",
                      )}
                    </div>
                  </div>
                ) : null}
              </div>
              <Switch
                id="backchannel-logout-session-required"
                checked={backchannelLogoutSessionRequired}
                onCheckedChange={setBackchannelLogoutSessionRequired}
                disabled={isGeneralSettingsReadOnly || !hasBackchannelLogoutUri}
              />
            </div>
          </div>

          <div className="rounded-md border border-border overflow-hidden">
            <table className="w-full text-sm">
              <thead className="bg-muted/50 border-b border-border text-xs uppercase tracking-wider text-muted-foreground">
                <tr>
                  <th className="px-4 py-3 text-left font-bold">
                    {t(
                      "ui.dev.clients.general.scopes.table.name",
                      "Scope Name",
                    )}
                  </th>
                  <th className="px-4 py-3 text-left font-bold">
                    {t(
                      "ui.dev.clients.general.scopes.table.description",
                      "Description",
                    )}
                  </th>
                  <th className="px-4 py-3 text-center font-bold">
                    {t(
                      "ui.dev.clients.general.scopes.table.mandatory",
                      "Mandatory",
                    )}
                  </th>
                  <th className="px-4 py-3 text-right font-bold">
                    {t("ui.dev.clients.general.scopes.table.delete", "Delete")}
                  </th>
                </tr>
              </thead>
              <tbody className="divide-y divide-border">
                {scopes.map((s) => (
                  <tr
                    key={s.id}
                    className={cn(
                      "transition-colors",
                      s.locked ? "bg-primary/5" : "hover:bg-muted/30",
                    )}
                  >
                    <td className="px-4 py-3">
                      <Input
                        value={s.name}
                        onChange={(e) =>
                          updateScope(s.id, "name", e.target.value)
                        }
                        className="h-8 font-mono text-xs"
                        placeholder={t(
                          "ui.dev.clients.general.scopes.name_placeholder",
                          "e.g. profile",
                        )}
                        disabled={s.locked || isGeneralSettingsReadOnly}
                      />
                    </td>
                    <td className="px-4 py-3">
                      <Input
                        value={s.description}
                        onChange={(e) =>
                          updateScope(s.id, "description", e.target.value)
                        }
                        className="h-8 text-xs"
                        placeholder={t(
                          "ui.dev.clients.general.scopes.description_placeholder",
                          "권한에 대한 설명",
                        )}
                        disabled={s.locked || isGeneralSettingsReadOnly}
                      />
                    </td>
                    <td className="px-4 py-3 text-center">
                      <div className="flex justify-center">
                        <Switch
                          checked={s.mandatory}
                          onCheckedChange={(checked) =>
                            updateScope(s.id, "mandatory", checked)
                          }
                          disabled={s.locked || isGeneralSettingsReadOnly}
                        />
                      </div>
                    </td>
                    <td className="px-4 py-3 text-right">
                      <Button
                        variant="ghost"
                        size="icon"
                        onClick={() => removeScope(s.id)}
                        className="h-8 w-8 text-muted-foreground hover:text-destructive"
                        disabled={s.locked || isGeneralSettingsReadOnly}
                      >
                        <Trash2 className="h-4 w-4" />
                      </Button>
                    </td>
                  </tr>
                ))}
                {scopes.length === 0 && (
                  <tr>
                    <td
                      colSpan={4}
                      className="px-4 py-8 text-center text-muted-foreground"
                    >
                      {t(
                        "msg.dev.clients.general.scopes.empty",
                        "등록된 스코프가 없습니다.",
                      )}
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>
        </CardContent>
      </Card>

      <Card className="glass-panel">
        <CardHeader className="pb-4">
          <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
            <div className="space-y-1">
              <CardTitle className="text-xl font-bold">
                {t(
                  "ui.dev.clients.general.tenant_access.title",
                  "테넌트 접근 제한",
                )}
              </CardTitle>
              <CardDescription>
                {t(
                  "ui.dev.clients.general.tenant_access.subtitle",
                  "허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
                )}
              </CardDescription>
            </div>
            <div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
              <div className="space-y-0.5 text-right">
                <p className="text-sm font-semibold">
                  {tenantAccessRestricted
                    ? t(
                        "ui.dev.clients.general.tenant_access.enabled",
                        "제한 있음",
                      )
                    : t(
                        "ui.dev.clients.general.tenant_access.disabled",
                        "제한 없음",
                      )}
                </p>
                <p className="text-xs text-muted-foreground">
                  {t(
                    "ui.dev.clients.general.tenant_access.title",
                    "테넌트 접근 제한",
                  )}
                </p>
              </div>
              <Switch
                checked={tenantAccessRestricted}
                onCheckedChange={handleTenantAccessToggle}
                id="tenant-access-toggle"
                disabled={isGeneralSettingsReadOnly}
              />
            </div>
          </div>
        </CardHeader>
        <CardContent className="space-y-5">
          <p className="text-sm text-muted-foreground">
            {t(
              "ui.dev.clients.general.tenant_access.hint",
              "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
            )}
          </p>

          <div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
            <div className="space-y-3">
              <Label htmlFor="tenant-search" className="text-sm font-semibold">
                {t(
                  "ui.dev.clients.general.tenant_access.search_placeholder",
                  "테넌트 이름 또는 슬러그로 검색",
                )}
              </Label>
              <div className="relative">
                <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
                <Input
                  id="tenant-search"
                  value={tenantSearch}
                  onFocus={() => {
                    if (tenantAccessRestricted) {
                      setIsTenantSearchOpen(true);
                    }
                  }}
                  onBlur={() => {
                    window.setTimeout(() => setIsTenantSearchOpen(false), 120);
                  }}
                  onChange={(e) => {
                    setTenantSearch(e.target.value);
                    if (tenantAccessRestricted) {
                      setIsTenantSearchOpen(true);
                    }
                  }}
                  placeholder={t(
                    "ui.dev.clients.general.tenant_access.search_placeholder",
                    "테넌트 이름 또는 슬러그로 검색",
                  )}
                  className="pl-10"
                  disabled={
                    isGeneralSettingsReadOnly || !tenantAccessRestricted
                  }
                />
                {tenantAccessRestricted && isTenantSearchOpen && (
                  <div className="absolute z-20 mt-2 max-h-72 w-full overflow-y-auto rounded-xl border border-border bg-background shadow-lg">
                    {tenantSuggestions.length > 0 ? (
                      tenantSuggestions.map((tenant) => (
                        <button
                          key={tenant.id}
                          type="button"
                          className="flex w-full items-start justify-between gap-3 border-b border-border/40 px-4 py-3 text-left transition hover:bg-muted/40 last:border-b-0"
                          onMouseDown={(event) => {
                            event.preventDefault();
                            handleSelectAllowedTenant(tenant.id);
                          }}
                          disabled={isGeneralSettingsReadOnly}
                        >
                          <div className="min-w-0 space-y-1">
                            <div className="flex items-center gap-2">
                              <span className="truncate font-medium">
                                {tenant.name}
                              </span>
                              <Badge variant="outline" className="text-[11px]">
                                {tenant.slug}
                              </Badge>
                            </div>
                            <p className="truncate text-xs text-muted-foreground">
                              {tenant.description || tenant.type}
                            </p>
                          </div>
                          <Plus className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
                        </button>
                      ))
                    ) : (
                      <div className="px-4 py-8 text-center text-sm text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.tenant_access.empty",
                          "검색 결과가 없습니다.",
                        )}
                      </div>
                    )}
                  </div>
                )}
              </div>
              <div className="rounded-xl border border-dashed border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
                {tenantAccessRestricted
                  ? t(
                      "ui.dev.clients.general.tenant_access.autocomplete_hint",
                      "테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다.",
                    )
                  : t(
                      "ui.dev.clients.general.tenant_access.disabled",
                      "제한 없음",
                    )}
              </div>
            </div>

            <div className="space-y-3">
              <Label className="text-sm font-semibold">
                {t(
                  "ui.dev.clients.general.tenant_access.selected_title",
                  "허용 테넌트",
                )}
              </Label>
              <div className="min-h-72 rounded-xl border border-border bg-muted/20 p-3">
                {tenantAccessRestricted && allowedTenantIds.length > 0 ? (
                  <div className="flex flex-wrap gap-2">
                    {selectedAllowedTenants.map((tenant) => (
                      <AllowedTenantBadge
                        key={tenant.id}
                        tenant={tenant}
                        onRemove={() => toggleAllowedTenant(tenant.id)}
                        disabled={isGeneralSettingsReadOnly}
                      />
                    ))}
                    {allowedTenantIds
                      .filter(
                        (tenantId) =>
                          !selectedAllowedTenants.some(
                            (tenant) => tenant.id === tenantId,
                          ),
                      )
                      .map((tenantId) => (
                        <AllowedTenantBadge
                          key={tenantId}
                          tenant={{ id: tenantId, name: tenantId }}
                          onRemove={() => toggleAllowedTenant(tenantId)}
                          disabled={isGeneralSettingsReadOnly}
                        />
                      ))}
                  </div>
                ) : (
                  <div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground">
                    {tenantAccessRestricted
                      ? t(
                          "ui.dev.clients.general.tenant_access.selected_empty",
                          "아직 선택된 테넌트가 없습니다.",
                        )
                      : t(
                          "ui.dev.clients.general.tenant_access.disabled",
                          "제한 없음",
                        )}
                  </div>
                )}
              </div>
            </div>
          </div>
        </CardContent>
      </Card>

      <Card className="glass-panel">
        <CardHeader className="pb-4">
          <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
            <div className="space-y-1">
              <CardTitle className="text-xl font-bold">
                {t(
                  "ui.dev.clients.general.id_token_claims.title",
                  "Custom Claims",
                )}
              </CardTitle>
              <CardDescription>
                {t(
                  "msg.dev.clients.general.id_token_claims.subtitle",
                  "공통 claim과 RP 전용 확장 claim을 구분해서 관리합니다.",
                )}
              </CardDescription>
            </div>
            <Button
              onClick={addIdTokenClaim}
              className="gap-2"
              disabled={isGeneralSettingsReadOnly}
            >
              <Plus className="h-4 w-4" />
              {t("ui.dev.clients.general.id_token_claims.add", "Claim 추가")}
            </Button>
          </div>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="grid gap-4 xl:grid-cols-[1.3fr_0.7fr]">
            <div className="space-y-3">
              <div className="rounded-md border border-border overflow-hidden">
                <table className="w-full text-sm">
                  <thead className="bg-muted/50 border-b border-border text-xs uppercase tracking-wider text-muted-foreground">
                    <tr>
                      <th className="px-4 py-3 text-left font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.key",
                          "Claim Key",
                        )}
                      </th>
                      <th className="px-4 py-3 text-left font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.namespace",
                          "Namespace",
                        )}
                      </th>
                      <th className="px-4 py-3 text-left font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.value_type",
                          "Value Type",
                        )}
                      </th>
                      <th className="px-4 py-3 text-left font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.read_permission",
                          "Read",
                        )}
                      </th>
                      <th className="px-4 py-3 text-left font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.write_permission",
                          "Write",
                        )}
                      </th>
                      <th className="px-4 py-3 text-left font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.value",
                          "Value",
                        )}
                      </th>
                      <th className="px-4 py-3 text-right font-bold">
                        {t(
                          "ui.dev.clients.general.id_token_claims.table.delete",
                          "Delete",
                        )}
                      </th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-border">
                    {idTokenClaims.map((claim) => (
                      <tr key={claim.id} className="hover:bg-muted/20">
                        <td className="px-4 py-3 align-top">
                          <Input
                            value={claim.key}
                            onChange={(e) =>
                              updateIdTokenClaim(
                                claim.id,
                                "key",
                                e.target.value,
                              )
                            }
                            className="h-9 font-mono text-xs"
                            placeholder={t(
                              "ui.dev.clients.general.id_token_claims.key_placeholder",
                              "e.g. locale",
                            )}
                            disabled={isGeneralSettingsReadOnly}
                          />
                        </td>
                        <td className="px-4 py-3 align-top">
                          <Badge
                            variant="muted"
                            className="h-9 rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs"
                          >
                            {t(
                              "ui.dev.clients.general.id_token_claims.namespace_rp_claims",
                              "rp_claims",
                            )}
                          </Badge>
                        </td>
                        <td className="px-4 py-3 align-top">
                          <select
                            value={claim.valueType}
                            onChange={(e) =>
                              updateIdTokenClaim(
                                claim.id,
                                "valueType",
                                e.target.value as ClaimValueType,
                              )
                            }
                            aria-label={t(
                              "ui.dev.clients.general.id_token_claims.value_type_label",
                              "Claim value type",
                            )}
                            className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
                            disabled={isGeneralSettingsReadOnly}
                          >
                            <option value="text">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_text",
                                "Text",
                              )}
                            </option>
                            <option value="number">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_number",
                                "Number",
                              )}
                            </option>
                            <option value="boolean">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_boolean",
                                "Boolean",
                              )}
                            </option>
                            <option value="array">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_array",
                                "Array",
                              )}
                            </option>
                            <option value="object">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_object",
                                "Object",
                              )}
                            </option>
                            <option value="date">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_date",
                                "Date",
                              )}
                            </option>
                            <option value="datetime">
                              {t(
                                "ui.dev.clients.general.id_token_claims.value_type_datetime",
                                "Datetime",
                              )}
                            </option>
                          </select>
                        </td>
                        <td className="px-4 py-3 align-top">
                          <select
                            value={claim.readPermission}
                            onChange={(e) =>
                              updateIdTokenClaim(
                                claim.id,
                                "readPermission",
                                e.target.value as CustomClaimPermission,
                              )
                            }
                            aria-label={t(
                              "ui.dev.clients.general.id_token_claims.read_permission_label",
                              "읽기 권한",
                            )}
                            className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
                            disabled={isGeneralSettingsReadOnly}
                          >
                            <option value="admin_only">
                              {t(
                                "ui.common.custom_claim_permission.admin_only",
                                "관리자만 가능",
                              )}
                            </option>
                            <option value="user_and_admin">
                              {t(
                                "ui.common.custom_claim_permission.user_and_admin",
                                "사용자 및 관리자 가능",
                              )}
                            </option>
                          </select>
                        </td>
                        <td className="px-4 py-3 align-top">
                          <select
                            value={claim.writePermission}
                            onChange={(e) =>
                              updateIdTokenClaim(
                                claim.id,
                                "writePermission",
                                e.target.value as CustomClaimPermission,
                              )
                            }
                            aria-label={t(
                              "ui.dev.clients.general.id_token_claims.write_permission_label",
                              "쓰기 권한",
                            )}
                            className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
                            disabled={isGeneralSettingsReadOnly}
                          >
                            <option value="admin_only">
                              {t(
                                "ui.common.custom_claim_permission.admin_only",
                                "관리자만 가능",
                              )}
                            </option>
                            <option value="user_and_admin">
                              {t(
                                "ui.common.custom_claim_permission.user_and_admin",
                                "사용자 및 관리자 가능",
                              )}
                            </option>
                          </select>
                        </td>
                        <td className="px-4 py-3 align-top">
                          <Input
                            value={claim.value}
                            onChange={(e) =>
                              updateIdTokenClaim(
                                claim.id,
                                "value",
                                e.target.value,
                              )
                            }
                            className="h-9 font-mono text-xs"
                            placeholder={t(
                              "ui.dev.clients.general.id_token_claims.value_placeholder",
                              "Enter the claim value",
                            )}
                            disabled={isGeneralSettingsReadOnly}
                          />
                        </td>
                        <td className="px-4 py-3 text-right align-top">
                          <Button
                            variant="ghost"
                            size="icon"
                            onClick={() => removeIdTokenClaim(claim.id)}
                            className="h-9 w-9 text-muted-foreground hover:text-destructive"
                            disabled={isGeneralSettingsReadOnly}
                          >
                            <Trash2 className="h-4 w-4" />
                          </Button>
                        </td>
                      </tr>
                    ))}
                    {idTokenClaims.length === 0 && (
                      <tr>
                        <td
                          colSpan={7}
                          className="px-4 py-8 text-center text-muted-foreground"
                        >
                          {t(
                            "msg.dev.clients.general.id_token_claims.empty",
                            "아직 추가된 ID Token claim이 없습니다.",
                          )}
                        </td>
                      </tr>
                    )}
                  </tbody>
                </table>
              </div>
              <p className="text-xs leading-6 text-muted-foreground">
                {t(
                  "msg.dev.clients.general.id_token_claims.hint",
                  "RP 전용 확장 claim만 관리합니다. 배열은 JSON 또는 콤마 구분 문자열, 객체는 JSON을 입력하면 됩니다.",
                )}
              </p>
            </div>

            <div className="space-y-3">
              <div className="rounded-xl border border-border bg-muted/20 p-4">
                <div className="flex items-center gap-2">
                  <Info className="h-4 w-4 text-primary" />
                  <div>
                    <p className="text-sm font-semibold">
                      {t(
                        "ui.dev.clients.general.id_token_claims.preview_title",
                        "Saved JSON Preview",
                      )}
                    </p>
                    <p className="text-xs text-muted-foreground">
                      {t(
                        "msg.dev.clients.general.id_token_claims.preview_hint",
                        "저장될 metadata.id_token_claims 구조를 미리 확인할 수 있습니다.",
                      )}
                    </p>
                  </div>
                </div>
                <Textarea
                  readOnly
                  value={idTokenClaimPreviewJson}
                  className="mt-4 min-h-72 font-mono text-xs"
                />
              </div>
            </div>
          </div>
        </CardContent>
      </Card>

      {/* 3. Security Settings */}
      <Card className="glass-panel">
        <CardHeader className="pb-3">
          <CardTitle className="text-xl font-bold">
            {t("ui.dev.clients.general.security.title", "보안 설정")}
          </CardTitle>
          <CardDescription>
            {t(
              "msg.dev.clients.general.security.subtitle",
              "클라이언트 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다.",
            )}
          </CardDescription>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="grid gap-4 md:grid-cols-2">
            <label
              className={cn(
                "relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
                securityProfile === "private"
                  ? "border-primary bg-primary/5"
                  : "border-border bg-card hover:border-muted-foreground/40",
              )}
            >
              <input
                className="sr-only"
                type="radio"
                name="security-profile"
                checked={securityProfile === "private"}
                onChange={() => handleSecurityProfileChange("private")}
              />
              <span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
                <Shield className="h-4 w-4 text-primary" />
                {t(
                  "ui.dev.clients.general.security.private",
                  "Server side App",
                )}
              </span>
              <span className="text-xs text-muted-foreground">
                {t(
                  "msg.dev.clients.general.security.private_help",
                  "서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우.",
                )}
              </span>
              <span className="absolute right-4 top-4 text-primary">
                {securityProfile === "private" ? "✓" : ""}
              </span>

              {securityProfile === "private" && (
                <fieldset
                  className="mt-4 flex items-center justify-between border-t border-primary/20 pt-4"
                  onClick={(e) => e.stopPropagation()}
                  onKeyDown={(e) => e.stopPropagation()}
                >
                  <div className="space-y-0.5">
                    <Label
                      className="cursor-pointer text-xs font-bold"
                      htmlFor="headless-login-toggle"
                    >
                      {t(
                        "ui.dev.clients.general.security.headless_login_enable",
                        "Headless Login (자체 로그인 UI 사용)",
                      )}
                    </Label>
                    <p className="text-[10px] text-muted-foreground">
                      {t(
                        "msg.dev.clients.general.security.headless_login_enable_help",
                        "Baron SSO 로그인 창 대신 RP 자체 로그인 UI를 사용하고, RP backend의 서명 키로 클라이언트를 검증하려는 경우 활성화합니다.",
                      )}
                    </p>
                  </div>
                  <Switch
                    id="headless-login-toggle"
                    checked={headlessLoginEnabled}
                    onCheckedChange={handleHeadlessToggle}
                    disabled={isGeneralSettingsReadOnly}
                  />
                </fieldset>
              )}
            </label>

            <label
              className={cn(
                "relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
                securityProfile === "pkce"
                  ? "border-primary bg-primary/5"
                  : "border-border bg-card hover:border-muted-foreground/40",
              )}
            >
              <input
                className="sr-only"
                type="radio"
                name="security-profile"
                checked={securityProfile === "pkce"}
                onChange={() => handleSecurityProfileChange("pkce")}
              />
              <span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
                <Sparkles className="h-4 w-4" />
                {t("ui.dev.clients.general.security.pkce", "PKCE")}
              </span>
              <span className="text-xs text-muted-foreground">
                {t(
                  "msg.dev.clients.general.security.pkce_help",
                  "SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다.",
                )}
              </span>
              <span className="absolute right-4 top-4 text-primary">
                {securityProfile === "pkce" ? "✓" : ""}
              </span>
            </label>
          </div>
        </CardContent>
      </Card>

      {/* 4. Public Key Registration (Headless Login) */}
      {headlessLoginEnabled && (
        <Card className="glass-panel border-primary/20">
          <CardHeader className="pb-3">
            <div className="flex flex-wrap items-start justify-between gap-3">
              <div>
                <CardTitle className="text-xl font-bold flex items-center gap-2">
                  {t(
                    "ui.dev.clients.general.public_key.title",
                    "Public Key Registration",
                  )}
                </CardTitle>
                <CardDescription>
                  {t(
                    "msg.dev.clients.general.public_key.subtitle",
                    "Server side App의 Headless Login capability에 필요한 공개키와 검증 정보를 관리합니다.",
                  )}
                </CardDescription>
              </div>
            </div>
          </CardHeader>
          <CardContent className="space-y-6">
            <div className={cn("rounded-xl border p-4", publicKeyStatusTone)}>
              <div className="flex items-center justify-between">
                <div>
                  <Label className="text-sm font-bold text-foreground">
                    {t(
                      "ui.dev.clients.general.public_key.headless_toggle",
                      "Headless Login 허용 여부",
                    )}
                  </Label>
                  <p className="mt-1 text-xs text-muted-foreground">
                    {t(
                      "msg.dev.clients.general.public_key.headless_help",
                      "RP가 자체 로그인 UI를 제공하더라도 실제 인증 흐름은 Baron API와 RP backend의 signed key 검증을 통해 이어집니다.",
                    )}
                  </p>
                </div>
                <Badge
                  variant="default"
                  className="bg-primary/20 text-primary border-primary/30"
                >
                  {t("ui.common.enabled", "Enabled")}
                </Badge>
              </div>
            </div>

            <div className="space-y-4">
              <div className="space-y-3 rounded-xl border border-border bg-muted/5 p-4">
                <div className="space-y-2">
                  <div className="flex items-center gap-2">
                    <Label className="text-sm font-semibold" htmlFor="jwks-uri">
                      {t(
                        "ui.dev.clients.general.public_key.jwks_uri",
                        "JWKS URI",
                      )}
                      <span className="text-destructive ml-1">*</span>
                    </Label>
                    <Button
                      type="button"
                      variant="ghost"
                      size="icon"
                      className="h-7 w-7 text-muted-foreground"
                      title={allowedHeadlessAlgorithmsTooltip}
                      aria-label={t(
                        "ui.dev.clients.general.public_key.allowed_algorithms_info",
                        "Headless Login 허용 알고리즘 정보",
                      )}
                    >
                      <Info className="h-4 w-4" />
                    </Button>
                  </div>
                  <Input
                    id="jwks-uri"
                    value={jwksUri}
                    onChange={(e) => setJwksUri(e.target.value)}
                    placeholder={t(
                      "ui.dev.clients.general.public_key.jwks_uri_placeholder",
                      "https://rp.example.com/.well-known/jwks.json",
                    )}
                  />
                  <p className="text-xs text-muted-foreground">
                    {t(
                      "msg.dev.clients.general.public_key.jwks_uri_help",
                      "RP backend가 제공하는 공개키 endpoint URL을 입력하세요.",
                    )}
                  </p>
                </div>
              </div>

              <div className="space-y-3 rounded-xl border border-border bg-card p-4">
                <div className="flex flex-wrap items-start justify-between gap-3">
                  <div>
                    <Label className="text-sm font-bold">
                      {t(
                        "ui.dev.clients.general.public_key.cache.title",
                        "JWKS Cache",
                      )}
                    </Label>
                    <p className="mt-1 text-xs text-muted-foreground">
                      {t(
                        "msg.dev.clients.general.public_key.cache_help",
                        "백엔드가 마지막으로 검증한 공개키 캐시 상태입니다.",
                      )}
                    </p>
                  </div>
                  <div className="flex gap-2">
                    <Button
                      type="button"
                      size="sm"
                      variant="outline"
                      onClick={() => refreshHeadlessJwksCacheMutation.mutate()}
                      disabled={refreshHeadlessJwksCacheMutation.isPending}
                    >
                      {refreshHeadlessJwksCacheMutation.isPending
                        ? t("msg.common.requesting", "요청 중...")
                        : t("ui.common.refresh", "Refresh")}
                    </Button>
                    <Button
                      type="button"
                      size="sm"
                      variant="destructive"
                      onClick={() => {
                        if (
                          !currentHeadlessJwksCache ||
                          revokeHeadlessJwksCacheMutation.isPending
                        ) {
                          return;
                        }
                        const confirmed = window.confirm(
                          t(
                            "msg.dev.clients.general.public_key.cache_revoke_confirm",
                            "JWKS 캐시를 삭제하면 다음 검증 전에 다시 갱신해야 합니다. 계속할까요?",
                          ),
                        );
                        if (confirmed) {
                          revokeHeadlessJwksCacheMutation.mutate();
                        }
                      }}
                      disabled={
                        !currentHeadlessJwksCache ||
                        revokeHeadlessJwksCacheMutation.isPending
                      }
                    >
                      {revokeHeadlessJwksCacheMutation.isPending
                        ? t("msg.common.requesting", "요청 중...")
                        : t(
                            "ui.dev.clients.general.public_key.revoke_cache",
                            "Revoke Cache",
                          )}
                    </Button>
                  </div>
                </div>

                {currentHeadlessJwksCache ? (
                  <div className="grid gap-3 text-sm md:grid-cols-2">
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.status",
                          "Status",
                        )}
                      </p>
                      <Badge variant="info" className="w-fit capitalize">
                        {currentHeadlessJwksCache.lastRefreshStatus ||
                          t("ui.common.unknown", "Unknown")}
                      </Badge>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.uri",
                          "JWKS URI",
                        )}
                      </p>
                      <p className="break-all font-mono text-xs">
                        {currentHeadlessJwksCache.jwksUri}
                      </p>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.cached_at",
                          "Cached At",
                        )}
                      </p>
                      <p>{formatDateTime(currentHeadlessJwksCache.cachedAt)}</p>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.expires_at",
                          "Expires At",
                        )}
                      </p>
                      <p>
                        {formatDateTime(currentHeadlessJwksCache.expiresAt)}
                      </p>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.last_checked_at",
                          "Last Checked",
                        )}
                      </p>
                      <p>
                        {formatDateTime(currentHeadlessJwksCache.lastCheckedAt)}
                      </p>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.last_success",
                          "Last Successful Verification",
                        )}
                      </p>
                      <p>
                        {formatDateTime(
                          currentHeadlessJwksCache.lastSuccessfulVerificationAt,
                        )}
                      </p>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.failures",
                          "Consecutive Failures",
                        )}
                      </p>
                      <p>{currentHeadlessJwksCache.consecutiveFailures ?? 0}</p>
                    </div>
                    <div className="space-y-1">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.kids",
                          "Cached KIDs",
                        )}
                      </p>
                      <p className="font-mono text-xs">
                        {currentHeadlessJwksCache.cachedKids?.length
                          ? currentHeadlessJwksCache.cachedKids.join(", ")
                          : "-"}
                      </p>
                    </div>
                    <div className="space-y-1 md:col-span-2">
                      <p className="text-xs font-semibold uppercase text-muted-foreground">
                        {t(
                          "ui.dev.clients.general.public_key.cache.error",
                          "Last Error",
                        )}
                      </p>
                      <p className="break-words text-xs text-muted-foreground">
                        {currentHeadlessJwksCache.lastError || "-"}
                      </p>
                    </div>
                    {(unsupportedParsedAlgorithms.length > 0 ||
                      missingParsedAlgorithms.length > 0) && (
                      <div className="space-y-2 rounded-lg border border-destructive/40 bg-destructive/5 p-3 md:col-span-2">
                        <p className="text-sm font-semibold text-destructive">
                          {unsupportedParsedAlgorithms.length > 0
                            ? t(
                                "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title",
                                "지원하지 않는 알고리즘이 감지되었습니다.",
                              )
                            : t(
                                "msg.dev.clients.general.public_key.cache.missing_algorithms_title",
                                "알고리즘이 선언되지 않았습니다.",
                              )}
                        </p>
                        <p className="text-xs text-destructive">
                          {unsupportedParsedAlgorithms.length > 0
                            ? t(
                                "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help",
                                "저장 전 JWKS를 수정해 주세요: {{details}}",
                                { details: unsupportedParsedAlgorithmSummary },
                              )
                            : t(
                                "msg.dev.clients.general.public_key.cache.missing_algorithms_help",
                                "저장 전 JWKS 각 키에 `alg`를 명시해 주세요: {{details}}",
                                { details: missingParsedAlgorithmSummary },
                              )}
                        </p>
                      </div>
                    )}
                    <div className="space-y-3 md:col-span-2">
                      <div className="flex items-center justify-between gap-2">
                        <p className="text-xs font-semibold uppercase text-muted-foreground">
                          {t(
                            "ui.dev.clients.general.public_key.cache.parsed_keys",
                            "Parsed Keys",
                          )}
                        </p>
                        <p className="text-[11px] text-muted-foreground">
                          {t(
                            "msg.dev.clients.general.public_key.cache.parsed_keys_help",
                            "Raw JWKS stays hidden. Only parsed key metadata is shown here.",
                          )}
                        </p>
                      </div>
                      {currentHeadlessJwksCache.parsedKeys?.length ? (
                        <div className="space-y-3">
                          {currentHeadlessJwksCache.parsedKeys.map((key) => {
                            const normalizedAlgorithm = key.alg?.trim() ?? "";
                            const isMissingAlgorithm =
                              normalizedAlgorithm === "";
                            const isUnsupportedAlgorithm =
                              !isMissingAlgorithm &&
                              !HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(
                                normalizedAlgorithm,
                              );

                            return (
                              <div
                                key={`${key.kid ?? "missing-kid"}-${key.kty ?? ""}-${key.alg ?? ""}-${key.n ?? ""}`}
                                className={cn(
                                  "rounded-xl border bg-muted/30 p-3",
                                  isUnsupportedAlgorithm || isMissingAlgorithm
                                    ? "border-destructive/50 bg-destructive/5"
                                    : "border-border",
                                )}
                              >
                                <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
                                  <div className="space-y-1">
                                    <p className="text-[11px] font-semibold uppercase text-muted-foreground">
                                      KID
                                    </p>
                                    <p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
                                      {key.kid || "-"}
                                    </p>
                                  </div>
                                  <div className="space-y-1">
                                    <p className="text-[11px] font-semibold uppercase text-muted-foreground">
                                      KTY
                                    </p>
                                    <p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
                                      {key.kty || "-"}
                                    </p>
                                  </div>
                                  <div className="space-y-1">
                                    <p className="text-[11px] font-semibold uppercase text-muted-foreground">
                                      USE
                                    </p>
                                    <p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
                                      {key.use || "-"}
                                    </p>
                                  </div>
                                  <div className="space-y-1">
                                    <p className="text-[11px] font-semibold uppercase text-muted-foreground">
                                      ALG
                                    </p>
                                    <p
                                      className={cn(
                                        "break-all rounded-lg border bg-background px-3 py-2 font-mono text-[11px]",
                                        isUnsupportedAlgorithm ||
                                          isMissingAlgorithm
                                          ? "border-destructive/50 text-destructive"
                                          : "border-border",
                                      )}
                                    >
                                      {key.alg ||
                                        t(
                                          "msg.dev.clients.general.public_key.cache.missing_algorithm_badge",
                                          "알고리즘 미선언",
                                        )}
                                    </p>
                                    {isMissingAlgorithm && (
                                      <p className="text-[11px] text-destructive">
                                        {t(
                                          "msg.dev.clients.general.public_key.cache.missing_algorithm_reason",
                                          "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.",
                                        )}
                                      </p>
                                    )}
                                    {isUnsupportedAlgorithm && (
                                      <p className="text-[11px] text-destructive">
                                        {t(
                                          "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason",
                                          "이 알고리즘은 Headless Login에서 지원되지 않습니다.",
                                        )}
                                      </p>
                                    )}
                                  </div>
                                </div>
                                <div className="mt-3 space-y-1">
                                  <p className="text-[11px] font-semibold uppercase text-muted-foreground">
                                    {t(
                                      "ui.dev.clients.general.public_key.cache.parsed_key_n",
                                      "N",
                                    )}
                                  </p>
                                  <p className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
                                    {key.n || "-"}
                                  </p>
                                </div>
                              </div>
                            );
                          })}
                        </div>
                      ) : (
                        <div className="rounded-lg border border-dashed border-border px-4 py-5 text-sm text-muted-foreground">
                          {t(
                            "msg.dev.clients.general.public_key.cache.parsed_keys_empty",
                            "No parsed JWKS keys are available yet.",
                          )}
                        </div>
                      )}
                    </div>
                  </div>
                ) : (
                  <div className="rounded-lg border border-dashed border-border px-4 py-6 text-sm text-muted-foreground">
                    {t(
                      "msg.dev.clients.general.public_key.cache_empty",
                      "아직 캐시된 JWKS가 없습니다. Refresh를 눌러 백엔드 캐시 상태를 조회하세요.",
                    )}
                  </div>
                )}
              </div>
            </div>

            {hasValidationErrors && (
              <div className="rounded-xl border border-destructive/40 bg-destructive/5 p-4 animate-in fade-in">
                <p className="text-sm font-semibold text-destructive flex items-center gap-2">
                  <span>⚠️</span>
                  {t(
                    "ui.dev.clients.general.public_key.validation_title",
                    "저장 전 확인 필요",
                  )}
                </p>
                <ul className="mt-2 list-disc space-y-1 pl-6 text-sm text-destructive">
                  {validationErrors.map((errorMessage) => (
                    <li key={errorMessage}>{errorMessage}</li>
                  ))}
                </ul>
              </div>
            )}
          </CardContent>
        </Card>
      )}

      <div className="flex items-center justify-between border-t border-border pt-4">
        <div>
          {!isCreate && (
            <Button
              variant="destructive"
              className="gap-2"
              onClick={handleDelete}
              disabled={deleteMutation.isPending}
            >
              <Trash2 className="h-4 w-4" />
              {deleteMutation.isPending
                ? t("msg.common.requesting", "요청 중...")
                : t("ui.common.delete", "삭제")}
            </Button>
          )}
        </div>
        <div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
          <Button variant="outline" onClick={() => navigate("/clients")}>
            {t("ui.common.cancel", "취소")}
          </Button>
          <Button
            onClick={() => mutation.mutate()}
            disabled={
              isGeneralSettingsReadOnly ||
              mutation.isPending ||
              isLoading ||
              name.trim() === "" ||
              (isCreate && redirectUris.trim() === "") ||
              hasValidationErrors
            }
            className="shadow-lg shadow-primary/20"
          >
            {mutation.isPending ? (
              <div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
            ) : (
              <Save size={16} className="mr-2" />
            )}
            {mutation.isPending
              ? t("msg.common.saving", "저장 중...")
              : isCreate
                ? t("ui.dev.clients.general.create", "클라이언트 생성")
                : t("ui.common.save", "저장")}
          </Button>
        </div>
      </div>
    </div>
  );
}

export default ClientGeneralPage;
