import {
  ApiOutlined,
  BellOutlined,
  ClockCircleOutlined,
  CopyOutlined,
  FileMarkdownOutlined,
  LoadingOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  ProfileOutlined,
  ReloadOutlined,
  SettingOutlined,
} from '@ant-design/icons';
import {
  Alert,
  Button,
  Checkbox,
  Drawer,
  Dropdown,
  Grid,
  Input,
  InputNumber,
  Layout,
  Modal,
  Select,
  Segmented,
  Space,
  Typography,
} from 'antd';
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { fetchPlanItems } from '../../features/planBoard/api';
import { isAutomationFailedItem, isReleasePendingMainItem, isWorkingPlanItem } from '../../features/planBoard/quickFilters';
import { fetchServerCommands, restartServerCommand } from '../../features/serverCommand/api';
import type { ServerCommandItem } from '../../features/serverCommand/types';
import {
  DEFAULT_APP_CONFIG,
  saveAutomationNotificationPreferenceToServer,
  saveAppConfigToServer,
  setStoredAppConfig,
  syncAppConfigFromServer,
  useAppConfig,
  type AppConfig,
  type PlanCostTimeUnit,
} from './appConfig';
import {
  fetchWebPushConfig,
  registerPwaNotificationToken,
  registerWebPushSubscription,
  unregisterPwaNotificationToken,
  unregisterWebPushSubscription,
  type WebPushSubscriptionPayload,
} from './notificationApi';
import {
  clearNotificationIdentity,
  getSavedNotificationDeviceId,
  getSavedPwaNotificationToken,
  setSavedPwaNotificationToken,
} from './notificationIdentity';
import { resetNonAuthClientState } from './appMaintenance';
import {
  ALLOWED_REGISTRATION_TOKEN,
  setRegisteredAccessToken,
  useTokenAccess,
} from './tokenAccess';
import { chatConnectionGateway, chatGateway } from './chatV2';
import { HeaderMessageCenter } from './HeaderMessageCenter';
import { fetchChatRuntimeJobDetail } from './mainChatPanel';
import {
  getSharedActiveConversationSnapshot,
  subscribeSharedActiveConversation,
} from './mainChatPanel/sharedActiveConversation';
import { buildChatPath } from './routes';
import type { MainHeaderProps } from './types';
import type { ChatRuntimeJobDetail, ChatRuntimeSnapshot } from './mainChatPanel/types';

const { Header } = Layout;
const { Paragraph, Text } = Typography;
const { useBreakpoint } = Grid;
const APP_SETTINGS_CATEGORIES = [
  { value: 'automation', label: '작업' },
  { value: 'workspace', label: '작업 환경' },
] as const;

const APP_SETTINGS_SECTIONS: Array<{
  value: 'chatSettings' | 'planDefaults' | 'planCost' | 'worklogAutomation' | 'automationNotifications' | 'gestureShortcuts';
  label: string;
  category: (typeof APP_SETTINGS_CATEGORIES)[number]['value'];
}> = [
  { value: 'chatSettings', label: '채팅 문맥 설정', category: 'workspace' },
  { value: 'planDefaults', label: '자동화 기본값', category: 'automation' },
  { value: 'planCost', label: '비용 표시', category: 'automation' },
  { value: 'worklogAutomation', label: '업무일지 자동화 설정', category: 'automation' },
  { value: 'automationNotifications', label: '자동화 알림', category: 'automation' },
  { value: 'gestureShortcuts', label: '제스처 / 단축키', category: 'workspace' },
];

const PLAN_COST_TIME_UNIT_LABELS: Record<PlanCostTimeUnit, string> = {
  hour: '시간',
  minute: '분',
  second: '초',
};

type ClientNotificationPermissionState = 'unsupported' | 'default' | 'granted' | 'denied';
type SettingsModalKey = 'appSettings' | 'notification' | 'token' | 'update';
type AppSettingsCategoryKey = (typeof APP_SETTINGS_CATEGORIES)[number]['value'];
type AppSettingsSectionKey = (typeof APP_SETTINGS_SECTIONS)[number]['value'];

let lastPushSwRegisterAttempts: Array<{ url: string; mode: string; error: string }> = [];
type FeedbackTone = 'success' | 'info' | 'warning' | 'error';
type InlineFeedback = {
  tone: FeedbackTone;
  message: string;
};

function areChatSettingsEqual(left: AppConfig['chat'], right: AppConfig['chat']) {
  return left.maxContextMessages === right.maxContextMessages && left.maxContextChars === right.maxContextChars;
}

function getChatSettingsDiffLabels(saved: AppConfig['chat'], draft: AppConfig['chat']) {
  const changedLabels: string[] = [];

  if (saved.maxContextMessages !== draft.maxContextMessages) {
    changedLabels.push('최근 문맥 메시지 수');
  }

  if (saved.maxContextChars !== draft.maxContextChars) {
    changedLabels.push('최근 문맥 글자 수');
  }

  return changedLabels;
}

function areWorklogAutomationSettingsEqual(left: AppConfig['worklogAutomation'], right: AppConfig['worklogAutomation']) {
  return (
    left.autoCreateDailyWorklog === right.autoCreateDailyWorklog &&
    left.dailyCreateTime === right.dailyCreateTime &&
    left.includeScreenshots === right.includeScreenshots &&
    left.includeChangedFiles === right.includeChangedFiles &&
    left.includeCommandLogs === right.includeCommandLogs &&
    left.template === right.template
  );
}

function getWorklogAutomationDiffLabels(saved: AppConfig['worklogAutomation'], draft: AppConfig['worklogAutomation']) {
  const changedLabels: string[] = [];

  if (saved.autoCreateDailyWorklog !== draft.autoCreateDailyWorklog) {
    changedLabels.push('일일 작업일지 자동 생성');
  }

  if (saved.dailyCreateTime !== draft.dailyCreateTime) {
    changedLabels.push('자동 생성 시각');
  }

  if (saved.includeScreenshots !== draft.includeScreenshots) {
    changedLabels.push('스크린샷 포함');
  }

  if (saved.includeChangedFiles !== draft.includeChangedFiles) {
    changedLabels.push('변경 파일 목록 포함');
  }

  if (saved.includeCommandLogs !== draft.includeCommandLogs) {
    changedLabels.push('실행 커맨드 포함');
  }

  if (saved.template !== draft.template) {
    changedLabels.push('기본 템플릿');
  }

  return changedLabels;
}

function arePlanDefaultSettingsEqual(left: AppConfig['planDefaults'], right: AppConfig['planDefaults']) {
  return (
    left.jangsingProcessingRequired === right.jangsingProcessingRequired &&
    left.autoDeployToMain === right.autoDeployToMain &&
    left.openEditorAfterCreate === right.openEditorAfterCreate
  );
}

function getPlanDefaultDiffLabels(saved: AppConfig['planDefaults'], draft: AppConfig['planDefaults']) {
  const changedLabels: string[] = [];

  if (saved.jangsingProcessingRequired !== draft.jangsingProcessingRequired) {
    changedLabels.push('기능동작확인 기본값');
  }

  if (saved.autoDeployToMain !== draft.autoDeployToMain) {
    changedLabels.push('메인까지 자동등록');
  }

  if (saved.openEditorAfterCreate !== draft.openEditorAfterCreate) {
    changedLabels.push('등록 후 편집기 열기');
  }

  return changedLabels;
}

function arePlanCostSettingsEqual(left: AppConfig['planCost'], right: AppConfig['planCost']) {
  return (
    left.baseCostPerMillionTokens === right.baseCostPerMillionTokens &&
    left.retryCostMultiplierPercent === right.retryCostMultiplierPercent &&
    left.hourlyCostMultiplierPercent === right.hourlyCostMultiplierPercent &&
    left.timeCostUnit === right.timeCostUnit &&
    left.attentionCostThresholdMultiplier === right.attentionCostThresholdMultiplier &&
    left.warningCostThresholdMultiplier === right.warningCostThresholdMultiplier &&
    left.highCostThresholdMultiplier === right.highCostThresholdMultiplier
  );
}

function getPlanCostDiffLabels(saved: AppConfig['planCost'], draft: AppConfig['planCost']) {
  const changedLabels: string[] = [];

  if (saved.baseCostPerMillionTokens !== draft.baseCostPerMillionTokens) {
    changedLabels.push('백만 토큰당 기준 비용');
  }

  if (saved.retryCostMultiplierPercent !== draft.retryCostMultiplierPercent) {
    changedLabels.push('재처리 가산 비율');
  }

  if (saved.hourlyCostMultiplierPercent !== draft.hourlyCostMultiplierPercent) {
    changedLabels.push('시간 가산 비율');
  }

  if (saved.timeCostUnit !== draft.timeCostUnit) {
    changedLabels.push('시간 가산 기준 단위');
  }

  if (saved.attentionCostThresholdMultiplier !== draft.attentionCostThresholdMultiplier) {
    changedLabels.push('관심 구간 기준');
  }

  if (saved.warningCostThresholdMultiplier !== draft.warningCostThresholdMultiplier) {
    changedLabels.push('주의 구간 기준');
  }

  if (saved.highCostThresholdMultiplier !== draft.highCostThresholdMultiplier) {
    changedLabels.push('높음 구간 기준');
  }

  return changedLabels;
}

function formatCostAmount(value: number) {
  return `${new Intl.NumberFormat('ko-KR').format(value)}원`;
}

function formatPlanCostTimeMultiplierLabel(planCost: AppConfig['planCost']) {
  return `${PLAN_COST_TIME_UNIT_LABELS[planCost.timeCostUnit]}당 ${planCost.hourlyCostMultiplierPercent}%`;
}

function getPlanCostThresholdPreview(planCost: AppConfig['planCost']) {
  const attentionThreshold = planCost.baseCostPerMillionTokens * Math.max(0.1, planCost.attentionCostThresholdMultiplier);
  const warningThreshold =
    planCost.baseCostPerMillionTokens *
    Math.max(planCost.attentionCostThresholdMultiplier, planCost.warningCostThresholdMultiplier);
  const highThreshold =
    planCost.baseCostPerMillionTokens * Math.max(planCost.warningCostThresholdMultiplier, planCost.highCostThresholdMultiplier);

  return `안정 ${formatCostAmount(attentionThreshold)} 이하 · 관심 ${formatCostAmount(
    warningThreshold,
  )} 이하 · 주의 ${formatCostAmount(highThreshold)} 이하 · 높음 ${formatCostAmount(highThreshold)} 초과`;
}

function getAppSettingsSectionCategory(section: AppSettingsSectionKey): AppSettingsCategoryKey {
  return APP_SETTINGS_SECTIONS.find((entry) => entry.value === section)?.category ?? 'automation';
}

function getAppSettingsSectionOptions(category: AppSettingsCategoryKey) {
  return APP_SETTINGS_SECTIONS.filter((entry) => entry.category === category).map((entry) => ({
    label: entry.label,
    value: entry.value,
  }));
}

function formatRuntimeTimestamp(value: string | null | undefined) {
  if (!value) {
    return '-';
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return value;
  }

  return date.toLocaleTimeString('ko-KR', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  });
}

function formatRuntimeRelativeLabel(value: string | null | undefined) {
  if (!value) {
    return '-';
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return formatRuntimeTimestamp(value);
  }

  const diffMs = Date.now() - date.getTime();

  if (diffMs <= 15_000) {
    return '방금';
  }

  const diffSeconds = Math.floor(diffMs / 1000);

  if (diffSeconds < 60) {
    return `${diffSeconds}초 전`;
  }

  const diffMinutes = Math.floor(diffSeconds / 60);

  if (diffMinutes < 60) {
    return `${diffMinutes}분 전`;
  }

  const diffHours = Math.floor(diffMinutes / 60);

  if (diffHours < 24) {
    return `${diffHours}시간 전`;
  }

  const diffDays = Math.floor(diffHours / 24);
  return `${diffDays}일 전`;
}

function buildRuntimeTerminalLabel(terminalStatus: ChatRuntimeJobDetail['terminalStatus']) {
  if (!terminalStatus) {
    return '-';
  }

  if (terminalStatus === 'cancelled') {
    return '취소됨';
  }

  if (terminalStatus === 'removed') {
    return '대기열 제거됨';
  }

  if (terminalStatus === 'failed') {
    return '실패';
  }

  return '완료';
}

const AUTOMATION_NOTIFICATION_OPTIONS: Array<{
  key: keyof Pick<
    AppConfig['automation'],
    | 'notifyOnAutomationStart'
    | 'notifyOnAutomationProgress'
    | 'notifyOnAutomationCompletion'
    | 'notifyOnAutomationRelease'
    | 'notifyOnAutomationMain'
    | 'notifyOnAutomationFailure'
    | 'notifyOnAutomationRestart'
    | 'notifyOnAutomationIssueResolved'
  >;
  label: string;
  description: string;
}> = [
  {
    key: 'notifyOnAutomationStart',
    label: '자동화 시작',
    description: '작업 자동화가 시작될 때 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationProgress',
    label: '자동화 진행',
    description: '자동 작업이 오래 걸릴 때 진행 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationCompletion',
    label: '자동 작업 완료',
    description: '작업완료, 변경 없음 완료, 수동 완료 처리 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationRelease',
    label: 'release 반영 완료',
    description: 'release 반영 완료 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationMain',
    label: 'main 반영 완료',
    description: 'main 반영과 메인 프로젝트 동기화 완료 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationFailure',
    label: '자동화 실패',
    description: '브랜치, 자동 작업, release, main 반영 실패 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationRestart',
    label: '작업 재시작',
    description: '재시도나 이슈 조치로 작업이 다시 큐에 들어갈 때 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationIssueResolved',
    label: '이슈 해결 처리',
    description: '최신 이슈가 해결 처리될 때 알림을 받습니다.',
  },
];

function areAutomationNotificationSettingsEqual(left: AppConfig['automation'], right: AppConfig['automation']) {
  return AUTOMATION_NOTIFICATION_OPTIONS.every((option) => left[option.key] === right[option.key]);
}

function getAutomationNotificationDiffLabels(saved: AppConfig['automation'], draft: AppConfig['automation']) {
  return AUTOMATION_NOTIFICATION_OPTIONS.filter((option) => saved[option.key] !== draft[option.key]).map(
    (option) => option.label,
  );
}

function areGestureShortcutSettingsEqual(
  left: AppConfig['gestureShortcuts'],
  right: AppConfig['gestureShortcuts'],
) {
  return left.openSearch === right.openSearch && left.openWindowSearch === right.openWindowSearch;
}

function getGestureShortcutDiffLabels(saved: AppConfig['gestureShortcuts'], draft: AppConfig['gestureShortcuts']) {
  const changedLabels: string[] = [];

  if (saved.openSearch !== draft.openSearch) {
    changedLabels.push('통합 검색 열기');
  }

  if (saved.openWindowSearch !== draft.openWindowSearch) {
    changedLabels.push('Window UI 검색 열기');
  }

  return changedLabels;
}

function isSettingsModalAllowed(modal: SettingsModalKey, hasAccess: boolean) {
  if (hasAccess) {
    return true;
  }

  return modal === 'token';
}

function getSettingsModalTitle(modal: SettingsModalKey) {
  switch (modal) {
    case 'notification':
      return '알림';
    case 'token':
      return '권한';
    case 'update':
      return '업데이트';
    default:
      return '앱 설정';
  }
}

function formatDateTimeLabel(value: string | null) {
  if (!value) {
    return '-';
  }

  const parsed = new Date(value);

  if (Number.isNaN(parsed.getTime())) {
    return value;
  }

  return new Intl.DateTimeFormat('ko-KR', {
    timeZone: 'Asia/Seoul',
    dateStyle: 'medium',
    timeStyle: 'short',
  }).format(parsed);
}

function getServerVersionStatusClassName(item: ServerCommandItem | null) {
  if (!item) {
    return 'app-header__server-version-indicator--unknown';
  }

  if (item.buildRequired) {
    return 'app-header__server-version-indicator--build-required';
  }

  if (item.updateAvailable) {
    return 'app-header__server-version-indicator--update-available';
  }

  return 'app-header__server-version-indicator--latest';
}

function getServerLastSourceChangedDateLabel(item: ServerCommandItem | null) {
  return formatDateTimeLabel(item?.latestSourceChangeAt ?? null);
}

function getServerVersionStatusTitle(item: ServerCommandItem | null, label: string) {
  if (!item) {
    return `${label} 최신 버전 확인 전`;
  }

  if (item.buildRequired) {
    return `${label} 커밋 미반영 상태`;
  }

  if (item.updateAvailable) {
    return `${label} 운영 반영 대기 상태`;
  }

  return `${label} 최신 버전`;
}

function getClientNotificationPermission(): ClientNotificationPermissionState {
  if (
    typeof window === 'undefined' ||
    typeof Notification === 'undefined' ||
    typeof navigator === 'undefined' ||
    !('serviceWorker' in navigator) ||
    !('PushManager' in window)
  ) {
    return 'unsupported';
  }

  if (Notification.permission === 'granted') {
    return 'granted';
  }

  if (Notification.permission === 'denied') {
    return 'denied';
  }

  return 'default';
}

function getClientNotificationPermissionLabel(permission: ClientNotificationPermissionState) {
  switch (permission) {
    case 'granted':
      return '허용됨';
    case 'denied':
      return '차단됨';
    case 'unsupported':
      return '미지원';
    default:
      return '미확인';
  }
}

function isStandaloneDisplayMode() {
  if (typeof window === 'undefined') {
    return false;
  }

  return (
    window.matchMedia?.('(display-mode: standalone)').matches === true ||
    (window.navigator as Navigator & { standalone?: boolean }).standalone === true
  );
}

function isAppleMobileDevice() {
  if (typeof navigator === 'undefined') {
    return false;
  }

  return /iPhone|iPad|iPod/i.test(navigator.userAgent);
}

function hasSecureOrigin() {
  if (typeof window === 'undefined') {
    return false;
  }

  return window.isSecureContext || window.location.hostname === 'localhost';
}

function urlBase64ToUint8Array(base64String: string) {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let index = 0; index < rawData.length; index += 1) {
    outputArray[index] = rawData.charCodeAt(index);
  }

  return outputArray;
}

function isSamePushApplicationServerKey(
  leftKey: ArrayBuffer | null | undefined,
  rightKey: Uint8Array,
) {
  if (!leftKey) {
    return false;
  }

  const leftBytes = new Uint8Array(leftKey);

  if (leftBytes.byteLength !== rightKey.byteLength) {
    return false;
  }

  for (let index = 0; index < leftBytes.byteLength; index += 1) {
    if (leftBytes[index] !== rightKey[index]) {
      return false;
    }
  }

  return true;
}

function serializePushSubscription(subscription: PushSubscription): WebPushSubscriptionPayload {
  const json = subscription.toJSON();

  return {
    endpoint: subscription.endpoint,
    expirationTime: subscription.expirationTime,
    keys: {
      p256dh: json.keys?.p256dh ?? '',
      auth: json.keys?.auth ?? '',
    },
  };
}

async function getPushServiceWorkerRegistration() {
  if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
    return null;
  }

  const serviceWorkerUrl = import.meta.env.DEV ? '/dev-sw.js?dev-sw' : '/sw.js';
  const resolvedServiceWorkerUrl =
    typeof window !== 'undefined' ? new URL(serviceWorkerUrl, window.location.href).toString() : serviceWorkerUrl;
  const timeoutMs = 30000;
  const startedAt = Date.now();
  let registeredRegistration: ServiceWorkerRegistration | null = null;

  const waitForServiceWorkerReady = async (remainingTimeoutMs: number) => {
    const readyStartedAt = Date.now();

    while (Date.now() - readyStartedAt < remainingTimeoutMs) {
      try {
        const readyRegistration = await Promise.race([
          navigator.serviceWorker.ready,
          new Promise<null>((resolve) => {
            window.setTimeout(() => resolve(null), 1000);
          }),
        ]);

        if (readyRegistration) {
          return readyRegistration;
        }
      } catch {
        // keep polling until timeout
      }

      await new Promise((resolve) => {
        window.setTimeout(resolve, 300);
      });
    }

    return null;
  };

  const resolveUsableRegistration = async (registration: ServiceWorkerRegistration | null | undefined) => {
    if (!registration) {
      return null;
    }

    if (registration.active || registration.waiting) {
      return registration;
    }

    const installingWorker = registration.installing;

    if (!installingWorker) {
      return registration;
    }

    const remainingTimeoutMs = Math.max(1500, timeoutMs - (Date.now() - startedAt));

    await new Promise<void>((resolve) => {
      let completed = false;

      const finish = () => {
        if (completed) {
          return;
        }

        completed = true;
        window.clearTimeout(timeoutId);
        installingWorker.removeEventListener('statechange', onStateChange);
        resolve();
      };

      const onStateChange = () => {
        if (
          installingWorker.state === 'activated' ||
          installingWorker.state === 'installed' ||
          Boolean(registration.active) ||
          Boolean(registration.waiting)
        ) {
          finish();
        }
      };

      const timeoutId = window.setTimeout(finish, remainingTimeoutMs);
      installingWorker.addEventListener('statechange', onStateChange);
      onStateChange();
    });

    if (registration.active || registration.waiting) {
      return registration;
    }

    const readyRegistration = await waitForServiceWorkerReady(remainingTimeoutMs);
    return readyRegistration ?? registration;
  };

  const tryRegister = async (url: string, mode: string, options?: RegistrationOptions) => {
    try {
      registeredRegistration = await navigator.serviceWorker.register(url, options);
      return true;
    } catch (error) {
      lastPushSwRegisterAttempts = [
        ...lastPushSwRegisterAttempts,
        {
          url,
          mode,
          error: error instanceof Error ? error.message : 'unknown error',
        },
      ];
      return false;
    }
  };

  const devOptionsModule = { type: 'module', scope: '/', updateViaCache: 'none' } as const;
  const devOptionsClassic = { scope: '/', updateViaCache: 'none' } as const;

  if (import.meta.env.DEV) {
    lastPushSwRegisterAttempts = [];
    const registered =
      (await tryRegister(resolvedServiceWorkerUrl, 'module', devOptionsModule)) ||
      (await tryRegister(resolvedServiceWorkerUrl, 'classic', devOptionsClassic));

    if (!registered) {
      // registerSW helper in main.tsx may already be handling this path; if manual registration fails,
      // continue and try to resolve an existing active registration below.
    }
  } else {
    await tryRegister(resolvedServiceWorkerUrl, 'default');
  }

  const usableRegisteredRegistration = await resolveUsableRegistration(registeredRegistration);

  if (usableRegisteredRegistration) {
    return usableRegisteredRegistration;
  }

  while (Date.now() - startedAt < timeoutMs) {
    const existingRegistration = await navigator.serviceWorker.getRegistration();
    const usableExistingRegistration = await resolveUsableRegistration(existingRegistration);

    if (usableExistingRegistration) {
      return usableExistingRegistration;
    }

    const scopedRegistration = await navigator.serviceWorker.getRegistration('/');
    const usableScopedRegistration = await resolveUsableRegistration(scopedRegistration);

    if (usableScopedRegistration) {
      return usableScopedRegistration;
    }

    const registrations = await navigator.serviceWorker.getRegistrations();

    for (const registration of registrations) {
      const usableRegistration = await resolveUsableRegistration(registration);

      if (usableRegistration) {
        return usableRegistration;
      }
    }

    try {
      const remainingTimeoutMs = Math.max(1500, timeoutMs - (Date.now() - startedAt));
      const readyRegistration = await waitForServiceWorkerReady(remainingTimeoutMs);
      const usableReadyRegistration = await resolveUsableRegistration(readyRegistration);

      if (usableReadyRegistration) {
        return usableReadyRegistration;
      }
    } catch {
      // keep polling until timeout
    }

    await new Promise((resolve) => {
      window.setTimeout(resolve, 250);
    });
  }

  const registrations = await navigator.serviceWorker.getRegistrations();

  for (const registration of registrations) {
    const usableRegistration = await resolveUsableRegistration(registration);

    if (usableRegistration) {
      return usableRegistration;
    }
  }

  if (import.meta.env.DEV) {
    try {
      await Promise.all(registrations.map((registration) => registration.unregister()));
      await waitForDuration(300);

      await tryRegister(resolvedServiceWorkerUrl, 'module', devOptionsModule);
      if (!registeredRegistration) {
        await tryRegister(resolvedServiceWorkerUrl, 'classic', devOptionsClassic);
      }

      const remainingTimeoutMs = Math.max(1500, timeoutMs - (Date.now() - startedAt));
      const readyRegistration = await waitForServiceWorkerReady(remainingTimeoutMs);
      const usableReadyRegistration = await resolveUsableRegistration(readyRegistration ?? registeredRegistration);

      if (usableReadyRegistration) {
        return usableReadyRegistration;
      }
    } catch {
      // ignore and fallthrough to null
    }
  }

  return null;
}

function waitForDuration(durationMs: number) {
  return new Promise<void>((resolve) => {
    window.setTimeout(resolve, durationMs);
  });
}

async function copyText(text: string) {
  if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }

  if (typeof document === 'undefined') {
    throw new Error('클립보드 API를 사용할 수 없습니다.');
  }

  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.setAttribute('readonly', 'true');
  textarea.style.position = 'fixed';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);
  textarea.select();
  document.execCommand('copy');
  document.body.removeChild(textarea);
}

export function MainHeader({
  activeTopMenu,
  sidebarCollapsed,
  contentExpanded,
  isMobileViewport,
  onToggleSidebar,
  onToggleContentExpanded,
  onChangeTopMenu,
  onOpenPlanQuickFilter,
}: MainHeaderProps) {
  void contentExpanded;
  void onToggleContentExpanded;
  const screens = useBreakpoint();
  const [modalApi, modalContextHolder] = Modal.useModal();
  const navigate = useNavigate();
  const location = useLocation();
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [settingsModalOpen, setSettingsModalOpen] = useState(false);
  const [activeSettingsModal, setActiveSettingsModal] = useState<SettingsModalKey>('appSettings');
  const [activeAppSettingsCategory, setActiveAppSettingsCategory] = useState<AppSettingsCategoryKey>('automation');
  const [activeAppSettingsSection, setActiveAppSettingsSection] = useState<AppSettingsSectionKey>('planDefaults');
  const [notificationEnabled, setNotificationEnabled] = useState(false);
  const [notificationLoading, setNotificationLoading] = useState(false);
  const [notificationFeedback, setNotificationFeedback] = useState<InlineFeedback | null>(null);
  const [notificationCopyFeedback, setNotificationCopyFeedback] = useState<InlineFeedback | null>(null);
  const [registeredPwaNotificationToken, setRegisteredPwaNotificationToken] = useState(() =>
    getSavedPwaNotificationToken(),
  );
  const [pwaNotificationTokenInput, setPwaNotificationTokenInput] = useState(() => getSavedPwaNotificationToken());
  const [pwaNotificationTokenSaving, setPwaNotificationTokenSaving] = useState(false);
  const [pwaNotificationTokenFeedback, setPwaNotificationTokenFeedback] = useState<InlineFeedback | null>(null);
  const [pwaNotificationTokenCopyFeedback, setPwaNotificationTokenCopyFeedback] = useState<InlineFeedback | null>(null);
  const [clientNotificationPermission, setClientNotificationPermission] = useState<ClientNotificationPermissionState>(
    () => getClientNotificationPermission(),
  );
  const [webPushConfigured, setWebPushConfigured] = useState(false);
  const [isStandaloneMode, setIsStandaloneMode] = useState(false);
  const [chatConnection, setChatConnection] = useState(() => chatConnectionGateway.getSnapshot());
  const [chatRuntimeSnapshot, setChatRuntimeSnapshot] = useState<ChatRuntimeSnapshot | null>(() =>
    chatConnectionGateway.getSharedRuntimeSnapshot(),
  );
  const [activeConversationSnapshot, setActiveConversationSnapshot] = useState(() =>
    getSharedActiveConversationSnapshot(),
  );
  const [isRuntimeModalOpen, setIsRuntimeModalOpen] = useState(false);
  const [runtimeConversationTitles, setRuntimeConversationTitles] = useState<Record<string, string>>({});
  const [isRuntimeLogDrawerOpen, setIsRuntimeLogDrawerOpen] = useState(false);
  const [runtimeLogLoading, setRuntimeLogLoading] = useState(false);
  const [runtimeLogError, setRuntimeLogError] = useState('');
  const [runtimeLogDetail, setRuntimeLogDetail] = useState<ChatRuntimeJobDetail | null>(null);
  const [updateCheckFeedback, setUpdateCheckFeedback] = useState<InlineFeedback | null>(null);
  const [updateCheckCopyFeedback, setUpdateCheckCopyFeedback] = useState<InlineFeedback | null>(null);
  const [clientResetting, setClientResetting] = useState(false);
  const [clientResetFeedback, setClientResetFeedback] = useState<InlineFeedback | null>(null);
  const [clientResetCopyFeedback, setClientResetCopyFeedback] = useState<InlineFeedback | null>(null);
  const [testServerStatus, setTestServerStatus] = useState<ServerCommandItem | null>(null);
  const [prodServerStatus, setProdServerStatus] = useState<ServerCommandItem | null>(null);
  const [workServerStatus, setWorkServerStatus] = useState<ServerCommandItem | null>(null);
  const [workServerStatusLoading, setWorkServerStatusLoading] = useState(false);
  const [serverRestartingKey, setServerRestartingKey] = useState<'test' | 'prod' | 'work-server' | 'all' | null>(null);
  const [serverRestartFeedback, setServerRestartFeedback] = useState<InlineFeedback | null>(null);
  const [serverRestartCopyFeedback, setServerRestartCopyFeedback] = useState<InlineFeedback | null>(null);
  const { registeredToken, hasAccess } = useTokenAccess();
  const appConfig = useAppConfig();
  const [appConfigDraft, setAppConfigDraft] = useState<AppConfig>(appConfig);
  const [appConfigFeedback, setAppConfigFeedback] = useState<InlineFeedback | null>(null);
  const [appConfigSaving, setAppConfigSaving] = useState(false);
  const [appConfigCopyFeedback, setAppConfigCopyFeedback] = useState<InlineFeedback | null>(null);
  const [tokenInput, setTokenInput] = useState('');
  const [tokenFeedback, setTokenFeedback] = useState<InlineFeedback | null>(null);
  const [planShortcutCounts, setPlanShortcutCounts] = useState({
    working: 0,
    releasePendingMain: 0,
    automationFailed: 0,
  });
  const headerTopMenu = !hasAccess ? 'docs' : activeTopMenu === 'apis' ? 'docs' : activeTopMenu === 'chat' ? 'plans' : activeTopMenu;
  const notificationStatusClassName = notificationEnabled
    ? 'app-header__status-dot--active'
    : 'app-header__status-dot--inactive';
  const chatConnectionStatusClassName =
    chatConnection.connectionState === 'connected'
      ? 'app-header__status-dot--active'
      : chatConnection.connectionState === 'connecting'
        ? 'app-header__status-dot--progress'
        : 'app-header__status-dot--inactive';
  const testServerPendingUpdateCount =
    testServerStatus && (testServerStatus.updateAvailable || testServerStatus.buildRequired) ? 1 : 0;
  const prodServerPendingUpdateCount =
    prodServerStatus && (prodServerStatus.updateAvailable || prodServerStatus.buildRequired) ? 1 : 0;
  const workServerPendingUpdateCount =
    workServerStatus && (workServerStatus.updateAvailable || workServerStatus.buildRequired) ? 1 : 0;
  const totalPendingUpdateCount = testServerPendingUpdateCount + prodServerPendingUpdateCount + workServerPendingUpdateCount;
  const settingsStatusClassName =
    totalPendingUpdateCount >= 2
      ? 'app-header__status-dot--inactive'
      : totalPendingUpdateCount === 1
        ? 'app-header__status-dot--warning'
        : 'app-header__status-dot--active';
  const settingsStatusLabel =
    totalPendingUpdateCount >= 2 ? '모든 업데이트 존재' : totalPendingUpdateCount === 1 ? '업데이트 1건 존재' : '최신 상태';
  const runningRuntimeCount = chatRuntimeSnapshot?.runningCount ?? 0;
  const queuedRuntimeCount = chatRuntimeSnapshot?.queuedCount ?? 0;
  const runtimeSessionCount = chatRuntimeSnapshot?.sessionCount ?? 0;
  const runningRuntimeBadgeLabel = runningRuntimeCount > 99 ? '99+' : String(runningRuntimeCount);
  const hasPendingRuntimeWork = runningRuntimeCount > 0 || queuedRuntimeCount > 0;
  const chatConnectionLabelParts = [
    chatConnection.connectionState === 'connected'
      ? 'Codex 채팅 실시간 연결됨'
      : chatConnection.connectionState === 'connecting'
        ? 'Codex 채팅 연결 중'
        : 'Codex 채팅 오프라인',
  ];

  if (runningRuntimeCount > 0) {
    chatConnectionLabelParts.push(`실행 ${runningRuntimeCount}건`);
  }

  if (queuedRuntimeCount > 0) {
    chatConnectionLabelParts.push(`대기 ${queuedRuntimeCount}건`);
  }

  const chatConnectionLabel = chatConnectionLabelParts.join(' · ');
  const connectionIndicatorClassName = `app-header__connection-indicator app-header__connection-indicator--${chatConnection.connectionState}${
    hasPendingRuntimeWork ? ' app-header__connection-indicator--busy' : ''
  }`;
  const connectionCountBadgeClassName = `app-header__connection-count-badge app-header__connection-count-badge--${chatConnection.connectionState}`;
  const getConversationLabel = (sessionId: string) => {
    if (sessionId === activeConversationSnapshot.sessionId && activeConversationSnapshot.title) {
      return activeConversationSnapshot.title;
    }

    return runtimeConversationTitles[sessionId] || sessionId;
  };
  const navigateToConversation = (sessionId: string) => {
    const searchParams = new URLSearchParams(location.search);
    searchParams.set('topMenu', 'chat');
    searchParams.set('sessionId', sessionId);
    searchParams.delete('chatView');
    searchParams.delete('runtimeRequestId');
    navigate({
      pathname: buildChatPath('live'),
      search: `?${searchParams.toString()}`,
    });
    setIsRuntimeModalOpen(false);
  };
  const openRuntimeLog = async (requestId: string) => {
    setIsRuntimeModalOpen(false);
    setIsRuntimeLogDrawerOpen(true);
    setRuntimeLogLoading(true);
    setRuntimeLogError('');

    try {
      const detail = await fetchChatRuntimeJobDetail(requestId);
      setRuntimeLogDetail(detail);
    } catch (error) {
      setRuntimeLogDetail(null);
      setRuntimeLogError(error instanceof Error ? error.message : '실행 로그를 불러오지 못했습니다.');
    } finally {
      setRuntimeLogLoading(false);
    }
  };
  const canRefreshWorkServerStatus = hasAccess && !workServerStatusLoading && !serverRestartingKey;
  const canResetClientState = !clientResetting;
  const canRestartServers =
    hasAccess &&
    !workServerStatusLoading &&
    !serverRestartingKey;
  const chatSettingsDirty = !areChatSettingsEqual(appConfig.chat, appConfigDraft.chat);
  const worklogAutomationSettingsDirty = !areWorklogAutomationSettingsEqual(
    appConfig.worklogAutomation,
    appConfigDraft.worklogAutomation,
  );
  const chatSettingsDiffLabels = getChatSettingsDiffLabels(appConfig.chat, appConfigDraft.chat);
  const planDefaultSettingsDirty = !arePlanDefaultSettingsEqual(appConfig.planDefaults, appConfigDraft.planDefaults);
  const planDefaultDiffLabels = getPlanDefaultDiffLabels(appConfig.planDefaults, appConfigDraft.planDefaults);
  const planCostSettingsDirty = !arePlanCostSettingsEqual(appConfig.planCost, appConfigDraft.planCost);
  const planCostDiffLabels = getPlanCostDiffLabels(appConfig.planCost, appConfigDraft.planCost);
  const worklogAutomationDiffLabels = getWorklogAutomationDiffLabels(
    appConfig.worklogAutomation,
    appConfigDraft.worklogAutomation,
  );
  const automationNotificationSettingsDirty = !areAutomationNotificationSettingsEqual(
    appConfig.automation,
    appConfigDraft.automation,
  );
  const automationNotificationDiffLabels = getAutomationNotificationDiffLabels(
    appConfig.automation,
    appConfigDraft.automation,
  );
  const gestureShortcutSettingsDirty = !areGestureShortcutSettingsEqual(
    appConfig.gestureShortcuts,
    appConfigDraft.gestureShortcuts,
  );
  const gestureShortcutDiffLabels = getGestureShortcutDiffLabels(
    appConfig.gestureShortcuts,
    appConfigDraft.gestureShortcuts,
  );
  const activeAppSettingsSectionOptions = getAppSettingsSectionOptions(activeAppSettingsCategory);
  const syncRegisteredWebPushStatus = async () => {
    const permission = getClientNotificationPermission();
    setClientNotificationPermission(permission);
    setIsStandaloneMode(isStandaloneDisplayMode());

    if (permission === 'unsupported') {
      setNotificationEnabled(false);
      return;
    }

    try {
      const config = await fetchWebPushConfig();
      setWebPushConfigured(Boolean(config.enabled && config.publicKey));

      if (!config.enabled || !config.publicKey) {
        setNotificationEnabled(false);
        return;
      }

      if (Notification.permission !== 'granted') {
        setNotificationEnabled(false);
        return;
      }

      const registration = await getPushServiceWorkerRegistration();

      if (!registration) {
        setNotificationEnabled(false);
        return;
      }

      let subscription = await registration.pushManager.getSubscription();

      if (!subscription) {
        setNotificationEnabled(false);
        return;
      }

      const expectedApplicationServerKey = urlBase64ToUint8Array(config.publicKey);

      if (!isSamePushApplicationServerKey(subscription.options.applicationServerKey, expectedApplicationServerKey)) {
        await unregisterWebPushSubscription(subscription.endpoint).catch(() => undefined);
        await subscription.unsubscribe().catch(() => undefined);

        subscription = await registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: expectedApplicationServerKey,
        });
      }

      await registerWebPushSubscription(serializePushSubscription(subscription), getSavedNotificationDeviceId());
      setNotificationEnabled(true);
      setNotificationFeedback(null);
    } catch {
      setNotificationEnabled(false);
    }
  };

  useEffect(() => {
    void syncRegisteredWebPushStatus();
  }, []);

  useEffect(() => {
    const handleSync = () => {
      void syncRegisteredWebPushStatus();
    };

    window.addEventListener('focus', handleSync);
    window.addEventListener('pageshow', handleSync);
    document.addEventListener('visibilitychange', handleSync);
    if ('serviceWorker' in navigator) {
      void navigator.serviceWorker.ready
        .then(() => {
          void syncRegisteredWebPushStatus();
        })
        .catch(() => undefined);
    }

    return () => {
      window.removeEventListener('focus', handleSync);
      window.removeEventListener('pageshow', handleSync);
      document.removeEventListener('visibilitychange', handleSync);
    };
  }, []);

  useEffect(() => {
    return chatConnectionGateway.subscribe(() => {
      setChatConnection(chatConnectionGateway.getSnapshot());
      setChatRuntimeSnapshot(chatConnectionGateway.getSharedRuntimeSnapshot());
    });
  }, []);

  useEffect(() => {
    return subscribeSharedActiveConversation(() => {
      setActiveConversationSnapshot(getSharedActiveConversationSnapshot());
    });
  }, []);

  useEffect(() => {
    if (!isRuntimeModalOpen) {
      return;
    }

    let cancelled = false;
    void chatGateway.listConversations()
      .then((items) => {
        if (cancelled) {
          return;
        }

        setRuntimeConversationTitles(
          items.reduce<Record<string, string>>((acc, item) => {
            acc[item.sessionId] = item.title?.trim() || item.sessionId;
            return acc;
          }, {}),
        );
      })
      .catch(() => {
        if (!cancelled) {
          setRuntimeConversationTitles({});
        }
      });

    return () => {
      cancelled = true;
    };
  }, [isRuntimeModalOpen]);

  useEffect(() => {
    setTokenInput(registeredToken);
  }, [registeredToken]);

  useEffect(() => {
    setAppConfigDraft(appConfig);
  }, [appConfig]);

  useEffect(() => {
    if (getAppSettingsSectionCategory(activeAppSettingsSection) === activeAppSettingsCategory) {
      return;
    }

    const firstSection = APP_SETTINGS_SECTIONS.find((entry) => entry.category === activeAppSettingsCategory);

    if (firstSection) {
      setActiveAppSettingsSection(firstSection.value);
    }
  }, [activeAppSettingsCategory, activeAppSettingsSection]);

  useEffect(() => {
    if (!isSettingsModalAllowed(activeSettingsModal, hasAccess)) {
      setActiveSettingsModal('token');
    }
  }, [activeSettingsModal, hasAccess]);

  useEffect(() => {
    if (!hasAccess) {
      setTestServerStatus(null);
      setWorkServerStatus(null);
      return;
    }

    void refreshUpdateTargets(true);
  }, [hasAccess]);

  useEffect(() => {
    if (!settingsModalOpen || activeSettingsModal !== 'update' || !hasAccess) {
      return;
    }

    void refreshUpdateTargets(true);
  }, [activeSettingsModal, hasAccess, settingsModalOpen]);

  const ensureClientNotificationPermission = async () => {
    const currentPermission = getClientNotificationPermission();
    setClientNotificationPermission(currentPermission);

    if (currentPermission === 'unsupported') {
      setNotificationFeedback({ tone: 'error', message: '현재 브라우저에서는 Web Push를 지원하지 않습니다.' });
      return false;
    }

    if (!hasSecureOrigin()) {
      setNotificationFeedback({ tone: 'error', message: '알림은 HTTPS 또는 localhost 환경에서만 사용할 수 있습니다.' });
      return false;
    }

    if (isAppleMobileDevice() && !isStandaloneDisplayMode()) {
      setNotificationFeedback({
        tone: 'warning',
        message: '아이폰에서는 홈 화면에 추가한 뒤 실행한 PWA에서만 웹 푸시를 사용할 수 있습니다.',
      });
      return false;
    }

    if (currentPermission === 'denied') {
      setNotificationFeedback({
        tone: 'warning',
        message: '브라우저 또는 기기 설정에서 알림 권한을 먼저 허용해 주세요.',
      });
      return false;
    }

    if (currentPermission === 'granted') {
      setNotificationFeedback(null);
      return true;
    }

    try {
      const permission = await Notification.requestPermission();
      const nextPermission =
        permission === 'granted' ? 'granted' : permission === 'denied' ? 'denied' : 'default';
      setClientNotificationPermission(nextPermission);

      if (nextPermission !== 'granted') {
        setNotificationFeedback({ tone: 'warning', message: '알림 권한이 허용되지 않아 알림을 켤 수 없습니다.' });
        return false;
      }

      setNotificationFeedback(null);
      return true;
    } catch {
      setNotificationFeedback({ tone: 'error', message: '알림 권한 요청 중 오류가 발생했습니다.' });
      return false;
    }
  };

  const syncNotificationEnabled = async (nextEnabled: boolean) => {
    if (notificationLoading) {
      return;
    }

    const previousEnabled = notificationEnabled;
    setNotificationCopyFeedback(null);
    setNotificationLoading(true);
    setNotificationEnabled(nextEnabled);

    if (nextEnabled) {
      setNotificationFeedback({ tone: 'info', message: '알림 권한과 Web Push 등록 상태를 확인하는 중입니다.' });
      const permissionGranted = await ensureClientNotificationPermission();

      if (!permissionGranted) {
        setNotificationEnabled(false);
        setNotificationLoading(false);
        return;
      }
    }

    try {
      let registration = await getPushServiceWorkerRegistration();

      if (!registration) {
        await waitForDuration(1500);
        registration = await getPushServiceWorkerRegistration();
      }

      if (!registration) {
        if (nextEnabled) {
          if (import.meta.env.DEV) {
            const serviceWorkerUrl = '/dev-sw.js?dev-sw';
            let swFetchStatus = 'unknown';

            try {
              const response = await fetch(serviceWorkerUrl, { cache: 'no-store' });
              swFetchStatus = `${response.status} ${response.statusText}`;
            } catch {
              swFetchStatus = 'fetch failed';
            }

            let registrationSummary = 'none';

            try {
              const registrations = await navigator.serviceWorker.getRegistrations();
              registrationSummary =
                registrations.length === 0
                  ? 'none'
                  : registrations
                      .map((item) => {
                        const states = [
                          item.active ? `active:${item.active.state}` : 'active:none',
                          item.waiting ? `waiting:${item.waiting.state}` : 'waiting:none',
                          item.installing ? `installing:${item.installing.state}` : 'installing:none',
                        ];
                        return `${item.scope} [${states.join(', ')}]`;
                      })
                      .join(' | ');
            } catch {
              registrationSummary = 'read failed';
            }

            throw new Error(
              [
                '알림 서비스워커 준비가 아직 끝나지 않았습니다.',
                `dev-sw 응답: ${swFetchStatus}`,
                `등록 상태: ${registrationSummary}`,
                `등록 시도: ${
                  lastPushSwRegisterAttempts.length === 0
                    ? 'none'
                    : lastPushSwRegisterAttempts
                        .map((attempt) => `${attempt.mode} ${attempt.url} -> ${attempt.error}`)
                        .join(' | ')
                }`,
                `secureContext: ${window.isSecureContext ? 'yes' : 'no'}`,
                `permission: ${Notification.permission}`,
              ].join(' '),
            );
          }

          throw new Error('알림 서비스워커 준비가 아직 끝나지 않았습니다. 잠시 후 다시 시도해 주세요.');
        }

        setNotificationEnabled(false);
        setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 해제했습니다.' });
        return;
      }

      if (nextEnabled) {
        const config = await fetchWebPushConfig();
        setWebPushConfigured(Boolean(config.enabled && config.publicKey));

        if (!config.enabled || !config.publicKey) {
          throw new Error('서버 Web Push 설정이 비어 있습니다. VAPID 설정을 먼저 확인해 주세요.');
        }

        let subscription = await registration.pushManager.getSubscription();
        const expectedApplicationServerKey = urlBase64ToUint8Array(config.publicKey);

        if (
          subscription &&
          !isSamePushApplicationServerKey(subscription.options.applicationServerKey, expectedApplicationServerKey)
        ) {
          await unregisterWebPushSubscription(subscription.endpoint).catch(() => undefined);
          await subscription.unsubscribe().catch(() => undefined);
          subscription = null;
        }

        if (!subscription) {
          subscription = await registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: expectedApplicationServerKey,
          });
        }

        await registerWebPushSubscription(serializePushSubscription(subscription), getSavedNotificationDeviceId());
        setNotificationEnabled(true);
        setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 서버에 등록했습니다.' });
        return;
      }

      const subscription = await registration.pushManager.getSubscription();

      if (subscription) {
        await unregisterWebPushSubscription(subscription.endpoint);
        await subscription.unsubscribe();
      }

      setNotificationEnabled(false);
      setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 해제했습니다.' });
    } catch (error) {
      setNotificationEnabled(previousEnabled);
      setNotificationFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '알림 설정 저장에 실패했습니다.',
      });
    } finally {
      setNotificationLoading(false);
    }
  };

  const handleRegisterPwaNotificationToken = async () => {
    const trimmedToken = pwaNotificationTokenInput.trim();
    setPwaNotificationTokenCopyFeedback(null);

    if (!trimmedToken) {
      setPwaNotificationTokenFeedback({ tone: 'warning', message: '등록할 PWA 알림 토큰을 입력해 주세요.' });
      return;
    }

    setPwaNotificationTokenSaving(true);

    try {
      await registerPwaNotificationToken({
        token: trimmedToken,
        deviceId: getSavedNotificationDeviceId(),
      });

      if (registeredPwaNotificationToken && registeredPwaNotificationToken !== trimmedToken) {
        await unregisterPwaNotificationToken(registeredPwaNotificationToken);
      }

      setSavedPwaNotificationToken(trimmedToken);
      setRegisteredPwaNotificationToken(trimmedToken);
      void syncAppConfigFromServer();
      setPwaNotificationTokenFeedback({ tone: 'success', message: 'PWA 알림 토큰을 서버에 등록했습니다.' });
    } catch (error) {
      setPwaNotificationTokenFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : 'PWA 알림 토큰 등록에 실패했습니다.',
      });
    } finally {
      setPwaNotificationTokenSaving(false);
    }
  };

  const handleClearPwaNotificationToken = async () => {
    const tokenToRemove = registeredPwaNotificationToken || pwaNotificationTokenInput.trim();
    setPwaNotificationTokenCopyFeedback(null);

    if (!tokenToRemove) {
      setPwaNotificationTokenFeedback({ tone: 'info', message: '제거할 PWA 알림 토큰이 없습니다.' });
      return;
    }

    setPwaNotificationTokenSaving(true);

    try {
      await unregisterPwaNotificationToken(tokenToRemove);
      setSavedPwaNotificationToken('');
      setRegisteredPwaNotificationToken('');
      setPwaNotificationTokenInput('');
      void syncAppConfigFromServer();
      setPwaNotificationTokenFeedback({ tone: 'success', message: 'PWA 알림 토큰을 제거했습니다.' });
    } catch (error) {
      setPwaNotificationTokenFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : 'PWA 알림 토큰 제거에 실패했습니다.',
      });
    } finally {
      setPwaNotificationTokenSaving(false);
    }
  };

  const refreshServerStatuses = async () => {
    const items = await fetchServerCommands();
    const nextTestServerStatus = items.find((item) => item.key === 'test') ?? null;
    const nextProdServerStatus = items.find((item) => item.key === 'prod') ?? null;
    const nextWorkServerStatus = items.find((item) => item.key === 'work-server') ?? null;
    setTestServerStatus(nextTestServerStatus);
    setProdServerStatus(nextProdServerStatus);
    setWorkServerStatus(nextWorkServerStatus);
    return {
      test: nextTestServerStatus,
      prod: nextProdServerStatus,
      'work-server': nextWorkServerStatus,
    } satisfies Record<'test' | 'prod' | 'work-server', ServerCommandItem | null>;
  };

  const refreshUpdateTargets = async (silent = false) => {
    if (!hasAccess) {
      setTestServerStatus(null);
      setProdServerStatus(null);
      setWorkServerStatus(null);
      if (!silent) {
        setUpdateCheckFeedback({ tone: 'warning', message: '업데이트 확인은 권한 토큰 등록 후 사용할 수 있습니다.' });
      }
      return null;
    }

    setWorkServerStatusLoading(true);

    try {
      const nextStatuses = await refreshServerStatuses();

      if (!silent) {
        setUpdateCheckFeedback(null);
      }

      return nextStatuses;
    } catch (error) {
      if (!silent) {
        setUpdateCheckFeedback({
          tone: 'error',
          message: error instanceof Error ? error.message : 'TEST/PROD/WORK 서버 업데이트 상태를 불러오지 못했습니다.',
        });
      }
      return null;
    } finally {
      setWorkServerStatusLoading(false);
    }
  };

  const waitForServerRestart = async (key: 'test' | 'prod' | 'work-server', baseline: ServerCommandItem | null) => {
    for (let attempt = 0; attempt < 16; attempt += 1) {
      await waitForDuration(2500);

      try {
        const nextStatuses = await refreshServerStatuses();
        const nextStatus = nextStatuses[key];

        if (!nextStatus) {
          continue;
        }

        const restarted =
          baseline == null ||
          nextStatus.startedAt !== baseline.startedAt ||
          nextStatus.checkedAt !== baseline.checkedAt;

        if (nextStatus.availability === 'online' && restarted) {
          return { ok: true, item: nextStatus };
        }
      } catch {
        // 서버 재기동 중에는 일시적으로 조회가 실패할 수 있습니다.
      }
    }

    return {
      ok: false,
      item: key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus,
    };
  };

  const handleResetClientState = async () => {
    if (clientResetting) {
      return;
    }

    setClientResetCopyFeedback(null);
    setClientResetFeedback(null);
    setClientResetting(true);

    try {
      const result = await resetNonAuthClientState();
      const changedCount =
        result.removedLocalStorageKeys.length +
        result.removedSessionStorageKeys.length +
        result.removedCacheKeys.length +
        result.unregisteredServiceWorkerCount;

      setClientResetFeedback({
        tone: 'success',
        message:
          changedCount > 0
            ? `토큰/권한 정보는 유지하고 캐시·스토리지를 초기화했습니다. 변경 ${changedCount}건을 정리한 뒤 화면을 새로고침합니다.`
            : '토큰/권한 정보는 유지하고 초기화 대상 캐시·스토리지를 확인했으며, 화면을 새로고침합니다.',
      });
      window.setTimeout(() => {
        window.location.replace(window.location.href);
      }, 700);
    } catch (error) {
      setClientResetFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '캐시·스토리지 초기화에 실패했습니다.',
      });
    } finally {
      setClientResetting(false);
    }
  };

  const restartServerWithVerification = async (
    key: 'test' | 'prod' | 'work-server',
    busyKey: 'test' | 'prod' | 'work-server' | 'all',
  ) => {
    const baseline = key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus;
    const targetLabel = key === 'test' ? 'TEST 서버' : key === 'prod' ? 'PROD 컨테이너' : 'WORK 서버';

    const result = await restartServerCommand(key);

    if (key === 'test') {
      setTestServerStatus(result.item);
    } else if (key === 'prod') {
      setProdServerStatus(result.item);
    } else {
      setWorkServerStatus(result.item);
    }

    setServerRestartFeedback({
      tone: 'info',
      message:
        result.restartState === 'accepted'
          ? `${targetLabel} 재기동 요청을 접수했습니다. 실제 정상 응답 여부를 확인하는 중입니다.`
          : `${targetLabel} 재기동을 실행했습니다. 실제 정상 응답 여부를 확인하는 중입니다.`,
    });
    setServerRestartingKey(busyKey);

    const verified = await waitForServerRestart(key, baseline);

    if (!verified.ok || !verified.item || verified.item.availability !== 'online') {
      setServerRestartFeedback({
        tone: 'error',
        message: `${targetLabel} 재기동 후 정상 응답을 확인하지 못했습니다. 상태를 다시 확인해 주세요.`,
      });
      return false;
    }

    setServerRestartFeedback({
      tone: 'success',
      message: `${targetLabel} 재기동 성공을 확인했습니다. 확인 시각 ${formatDateTimeLabel(verified.item.checkedAt)}`,
    });
    return true;
  };

  const handleRestartSingleServer = async (key: 'test' | 'prod' | 'work-server') => {
    if (!hasAccess || serverRestartingKey) {
      return false;
    }

    setServerRestartCopyFeedback(null);
    setServerRestartFeedback(null);
    setServerRestartingKey(key);

    try {
      return await restartServerWithVerification(key, key);
    } catch (error) {
      const targetLabel = key === 'test' ? 'TEST 서버' : key === 'prod' ? 'PROD 컨테이너' : 'WORK 서버';
      setServerRestartFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : `${targetLabel} 재기동에 실패했습니다.`,
      });
      return false;
    } finally {
      setServerRestartingKey(null);
    }
  };

  const handleConfirmRestartProdServer = () => {
    if (!hasAccess || serverRestartingKey) {
      return;
    }

    modalApi.confirm({
      title: 'PROD 빌드 반영',
      content: 'PROD 컨테이너를 빌드 후 재기동합니다. 진행할까요?',
      okText: '빌드 및 재기동',
      cancelText: '취소',
      okButtonProps: { danger: true },
      onOk: async () => {
        await handleRestartSingleServer('prod');
      },
    });
  };

  const handleRestartBothServers = async () => {
    if (!hasAccess || serverRestartingKey) {
      return;
    }

    setServerRestartCopyFeedback(null);
    setServerRestartFeedback(null);
    setServerRestartingKey('all');

    try {
      const testOk = await restartServerWithVerification('test', 'all');

      if (!testOk) {
        return;
      }

      const workServerOk = await restartServerWithVerification('work-server', 'all');

      if (!workServerOk) {
        return;
      }

      setServerRestartFeedback({
        tone: 'success',
        message: 'TEST 서버와 WORK 서버 모두 재기동 성공을 확인했습니다.',
      });
    } catch (error) {
      setServerRestartFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '서버 재기동에 실패했습니다.',
      });
    } finally {
      setServerRestartingKey(null);
    }
  };

  const openSettingsModal = (
    modal: SettingsModalKey,
    nextAppSettingsSection: AppSettingsSectionKey = 'planDefaults',
  ) => {
    setSettingsOpen(false);
    setActiveSettingsModal(isSettingsModalAllowed(modal, hasAccess) ? modal : 'token');
    setActiveAppSettingsCategory(getAppSettingsSectionCategory(nextAppSettingsSection));
    setActiveAppSettingsSection(nextAppSettingsSection);
    setAppConfigDraft(appConfig);
    setAppConfigFeedback(null);
    setSettingsModalOpen(true);
  };

  const updateAppConfigDraft = (updater: (current: AppConfig) => AppConfig) => {
    setAppConfigDraft((current) => updater(current));
  };

  const handleSaveAppConfig = async () => {
    if (appConfigSaving) {
      return;
    }

    setAppConfigSaving(true);
    setAppConfigFeedback(null);
    setAppConfigCopyFeedback(null);

    try {
      const savedConfig =
        activeAppSettingsSection === 'automationNotifications'
          ? await saveAutomationNotificationPreferenceToServer(appConfigDraft)
          : await saveAppConfigToServer(appConfigDraft);
      setStoredAppConfig(savedConfig);
      setAppConfigFeedback({
        tone: 'success',
        message:
          activeAppSettingsSection === 'automationNotifications'
            ? '자동화 알림 설정을 알림 토큰과 클라이언트별로 저장했습니다.'
            : '앱 설정을 DB에 저장했습니다.',
      });
    } catch (error) {
      setAppConfigDraft(appConfig);
      setAppConfigFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '앱 설정 저장에 실패했습니다.',
      });
    } finally {
      setAppConfigSaving(false);
    }
  };

  const handleResetNotificationIdentity = () => {
    modalApi.confirm({
      title: '알림 클라이언트 초기화',
      content: '현재 클라이언트의 알림 토큰/클라이언트 ID를 초기화합니다. 다시 접속하면 새로 등록됩니다.',
      okText: '초기화',
      cancelText: '취소',
      onOk: () => {
        clearNotificationIdentity();
        window.location.reload();
      },
    });
  };

  const handleResetAppConfig = () => {
    setAppConfigDraft(DEFAULT_APP_CONFIG);
    setAppConfigFeedback({ tone: 'info', message: '추천 기본값으로 되돌렸습니다. 저장 후 반영됩니다.' });
    setAppConfigCopyFeedback(null);
  };

  useEffect(() => {
    void syncAppConfigFromServer().then((synced) => {
      if (!synced) {
        setAppConfigFeedback({ tone: 'warning', message: '서버 설정을 불러오지 못해 로컬 설정을 사용합니다.' });
      }
    });
  }, []);

  useEffect(() => {
    if (!hasAccess || !settingsOpen) {
      setPlanShortcutCounts({
        working: 0,
        releasePendingMain: 0,
        automationFailed: 0,
      });
      return;
    }

    let cancelled = false;

    void fetchPlanItems('all')
      .then((items) => {
        if (cancelled) {
          return;
        }

        setPlanShortcutCounts({
          working: items.filter(isWorkingPlanItem).length,
          releasePendingMain: items.filter(isReleasePendingMainItem).length,
          automationFailed: items.filter(isAutomationFailedItem).length,
        });
      })
      .catch(() => {
        if (!cancelled) {
          setPlanShortcutCounts({
            working: 0,
            releasePendingMain: 0,
            automationFailed: 0,
          });
        }
      });

    return () => {
      cancelled = true;
    };
  }, [hasAccess, settingsOpen]);

  const renderFeedback = (
    feedback: InlineFeedback | null,
    copyFeedback: InlineFeedback | null,
    setCopyFeedback: (feedback: InlineFeedback | null) => void,
  ) => {
    if (!feedback) {
      return null;
    }

    return (
      <Space direction="vertical" size={8} style={{ width: '100%' }}>
        <Alert
          showIcon
          type={feedback.tone}
          message={feedback.message}
          action={
            <Button
              type="text"
              size="small"
              aria-label="메시지 복사"
              icon={<CopyOutlined />}
              onClick={() => {
                void copyText(feedback.message)
                  .then(() => {
                    setCopyFeedback({ tone: 'success', message: '메시지를 복사했습니다.' });
                  })
                  .catch(() => {
                    setCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                  });
              }}
            />
          }
        />
        {copyFeedback ? <Alert showIcon type={copyFeedback.tone} message={copyFeedback.message} /> : null}
      </Space>
    );
  };

  const handleRegisterToken = () => {
    const trimmedToken = tokenInput.trim();

    if (!trimmedToken) {
      setTokenFeedback({ tone: 'warning', message: '등록할 토큰을 입력해 주세요.' });
      return;
    }

    if (trimmedToken !== ALLOWED_REGISTRATION_TOKEN) {
      setTokenFeedback({ tone: 'error', message: '허용되지 않은 토큰입니다.' });
      return;
    }

    setRegisteredAccessToken(trimmedToken);
    setTokenFeedback({ tone: 'success', message: '권한 토큰을 등록했습니다.' });
  };

  const handleClearToken = () => {
    setRegisteredAccessToken('');
    setTokenInput('');
    setTokenFeedback({ tone: 'info', message: '권한 토큰을 제거했습니다.' });
  };

  const settingsTriggerButton = (
    <Button
      type="text"
      aria-label={`설정 · ${settingsStatusLabel}`}
      icon={
        <span className="app-header__settings-icon" aria-hidden="true">
          <SettingOutlined />
          <span className={`app-header__status-dot ${settingsStatusClassName}`} />
        </span>
      }
    />
  );

  const tokenTriggerButton = (
    <Button
      type="text"
      aria-label={`권한 · ${hasAccess ? '허용됨' : '차단됨'}`}
      icon={
        <span className="app-header__settings-icon" aria-hidden="true">
          <ProfileOutlined />
          <span
            className={`app-header__status-dot ${
              hasAccess ? 'app-header__status-dot--active' : 'app-header__status-dot--inactive'
            }`}
          />
        </span>
      }
      onClick={() => {
        openSettingsModal('token');
      }}
    />
  );

  const worklogAutomationPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="업무일지 자동화 기본값을 설정합니다."
        description="현재는 앱 설정의 기준값을 저장하는 단계이며, 자동 생성/캡처 흐름에서 공통 기본값으로 사용하기 위한 설정입니다."
      />
      <Alert
        showIcon
        type={worklogAutomationSettingsDirty ? 'warning' : 'success'}
        message={
          worklogAutomationSettingsDirty
            ? 'DB 저장값과 편집 중인 업무일지 자동화 설정이 다릅니다.'
            : '업무일지 자동화 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          worklogAutomationSettingsDirty
            ? `변경 항목: ${worklogAutomationDiffLabels.join(', ')} / DB 저장값 기준: ${
                appConfig.worklogAutomation.autoCreateDailyWorklog ? '자동 생성 On' : '자동 생성 Off'
              }, 실행 시각 ${appConfig.worklogAutomation.dailyCreateTime}, 템플릿 ${
                appConfig.worklogAutomation.template === 'detailed' ? '상세형' : '간단형'
              } / 편집 중: ${
                appConfigDraft.worklogAutomation.autoCreateDailyWorklog ? '자동 생성 On' : '자동 생성 Off'
              }, 실행 시각 ${appConfigDraft.worklogAutomation.dailyCreateTime}, 템플릿 ${
                appConfigDraft.worklogAutomation.template === 'detailed' ? '상세형' : '간단형'
              }`
            : `DB 저장값 기준: ${
                appConfig.worklogAutomation.autoCreateDailyWorklog ? '자동 생성 On' : '자동 생성 Off'
              }, 실행 시각 ${appConfig.worklogAutomation.dailyCreateTime}, 템플릿 ${
                appConfig.worklogAutomation.template === 'detailed' ? '상세형' : '간단형'
              }`
        }
      />
      {appConfigFeedback ? (
        <Space direction="vertical" size={8} style={{ width: '100%' }}>
          <Alert
            showIcon
            type={appConfigFeedback.tone}
            message={appConfigFeedback.message}
            action={
              <Button
                type="text"
                size="small"
                aria-label="메시지 복사"
                icon={<CopyOutlined />}
                onClick={() => {
                  void copyText(appConfigFeedback.message)
                    .then(() => {
                      setAppConfigCopyFeedback({ tone: 'success', message: '메시지를 복사했습니다.' });
                    })
                    .catch(() => {
                      setAppConfigCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                    });
                }}
              />
            }
          />
          {appConfigCopyFeedback ? (
            <Alert showIcon type={appConfigCopyFeedback.tone} message={appConfigCopyFeedback.message} />
          ) : null}
        </Space>
      ) : null}
      <Checkbox
        checked={appConfigDraft.worklogAutomation.autoCreateDailyWorklog}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              autoCreateDailyWorklog: event.target.checked,
            },
          }));
        }}
      >
        일일 업무일지 파일 자동 생성
      </Checkbox>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>자동 생성 시각</Text>
        <Input
          type="time"
          value={appConfigDraft.worklogAutomation.dailyCreateTime}
          onChange={(event) => {
            updateAppConfigDraft((current) => ({
              ...current,
              worklogAutomation: {
                ...current.worklogAutomation,
                dailyCreateTime: event.target.value || DEFAULT_APP_CONFIG.worklogAutomation.dailyCreateTime,
              },
            }));
          }}
        />
        <Text type="secondary">설정한 시각 이후 서버 워커가 오늘자 업무일지 요청을 자동 등록합니다.</Text>
      </Space>
      <Text type="secondary">반복 등록은 별도 서버 작업으로 관리합니다.</Text>
      <Checkbox
        checked={appConfigDraft.worklogAutomation.includeScreenshots}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              includeScreenshots: event.target.checked,
            },
          }));
        }}
      >
        자동 캡처 스크린샷 포함
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.worklogAutomation.includeChangedFiles}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              includeChangedFiles: event.target.checked,
            },
          }));
        }}
      >
        변경 파일 목록 자동 포함
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.worklogAutomation.includeCommandLogs}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              includeCommandLogs: event.target.checked,
            },
          }));
        }}
      >
        실행 커맨드 로그 자동 포함
      </Checkbox>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>기본 템플릿</Text>
        <Segmented
          block
          value={appConfigDraft.worklogAutomation.template}
          options={[
            { label: '상세형', value: 'detailed' },
            { label: '간단형', value: 'simple' },
          ]}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              worklogAutomation: {
                ...current.worklogAutomation,
                template: value as AppConfig['worklogAutomation']['template'],
              },
            }));
          }}
        />
        <Text type="secondary">
          상세형은 스크린샷, 소스, 실행 커맨드, 변경 파일 중심으로 남기고 간단형은 핵심 작업 요약 위주로 정리합니다.
        </Text>
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
        <Button onClick={handleResetNotificationIdentity}>클라이언트 초기화</Button>
      </Space>
    </Space>
  );

  const chatSettingsPanel = (
    <Space direction="vertical" size={16} style={{ width: '100%' }}>
      <Alert
        type={chatSettingsDirty ? 'warning' : 'success'}
        showIcon
        message={chatSettingsDirty ? '채팅 문맥 설정에 저장되지 않은 변경이 있습니다.' : '채팅 문맥 설정이 저장되었습니다.'}
        description={
          chatSettingsDirty
            ? `변경 항목: ${chatSettingsDiffLabels.join(', ')} / DB 저장값 기준: 최근 ${appConfig.chat.maxContextMessages}개, ${appConfig.chat.maxContextChars}자 / 편집 중: 최근 ${appConfigDraft.chat.maxContextMessages}개, ${appConfigDraft.chat.maxContextChars}자`
            : `현재 저장값: 최근 ${appConfig.chat.maxContextMessages}개 메시지, 최대 ${appConfig.chat.maxContextChars}자까지 채팅 문맥으로 참조`
        }
      />

      <div>
        <Text strong>최근 문맥 메시지 수</Text>
        <Paragraph type="secondary">최근 대화 중 몇 개의 메시지를 채팅 문맥으로 참조할지 설정합니다.</Paragraph>
        <InputNumber
          min={1}
          max={50}
          value={appConfigDraft.chat.maxContextMessages}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                maxContextMessages:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(50, Math.max(1, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.maxContextMessages,
              },
            }));
          }}
        />
      </div>

      <div>
        <Text strong>최근 문맥 글자 수</Text>
        <Paragraph type="secondary">최근 대화 전체에서 참조할 최대 글자 수입니다. 초과하면 전송 전에 확인 모달이 표시됩니다.</Paragraph>
        <InputNumber
          min={500}
          max={20000}
          step={100}
          value={appConfigDraft.chat.maxContextChars}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                maxContextChars:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(20_000, Math.max(500, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.maxContextChars,
              },
            }));
          }}
        />
      </div>
    </Space>
  );

  const planDefaultsPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="작업 자동화 기본값을 설정합니다."
        description="새 작업 요청을 등록할 때 기본으로 채워지는 자동화 옵션입니다."
      />
      <Alert
        showIcon
        type={planDefaultSettingsDirty ? 'warning' : 'success'}
        message={
          planDefaultSettingsDirty
            ? 'DB 저장값과 편집 중인 자동화 기본값이 다릅니다.'
            : '자동화 기본값이 현재 DB 저장값과 같습니다.'
        }
        description={
          planDefaultSettingsDirty
            ? `변경 항목: ${planDefaultDiffLabels.join(', ')} / DB 저장값 기준: 기능동작확인 ${
                appConfig.planDefaults.jangsingProcessingRequired ? '완료' : '오동작'
              }, ${appConfig.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'}, 등록 후 편집기 ${
                appConfig.planDefaults.openEditorAfterCreate ? '열기' : '닫기'
              } / 편집 중: 기능동작확인 ${
                appConfigDraft.planDefaults.jangsingProcessingRequired ? '완료' : '오동작'
              }, ${
                appConfigDraft.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'
              }, 등록 후 편집기 ${appConfigDraft.planDefaults.openEditorAfterCreate ? '열기' : '닫기'}`
            : `DB 저장값 기준: 기능동작확인 ${
                appConfig.planDefaults.jangsingProcessingRequired ? '완료' : '오동작'
              }, ${appConfig.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'}, 등록 후 편집기 ${
                appConfig.planDefaults.openEditorAfterCreate ? '열기' : '닫기'
              }`
        }
      />
      {renderFeedback(appConfigFeedback, appConfigCopyFeedback, setAppConfigCopyFeedback)}
      <Checkbox
        checked={appConfigDraft.planDefaults.jangsingProcessingRequired}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            planDefaults: {
              ...current.planDefaults,
              jangsingProcessingRequired: event.target.checked,
            },
          }));
        }}
      >
        기능동작확인 기본값을 완료로 설정
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.planDefaults.autoDeployToMain}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            planDefaults: {
              ...current.planDefaults,
              autoDeployToMain: event.target.checked,
            },
          }));
        }}
      >
        메인까지 자동등록
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.planDefaults.openEditorAfterCreate}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            planDefaults: {
              ...current.planDefaults,
              openEditorAfterCreate: event.target.checked,
            },
          }));
        }}
      >
        새 항목 등록 후 편집기 자동 열기
      </Checkbox>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const planCostPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="자동화 비용 표시 기준을 설정합니다."
        description="진행 중 작업과 완료된 작업 모두 누적 토큰 사용량, 재처리 횟수, 종료 시각 기준 처리 시간을 반영해 예상 비용을 계산합니다. 재처리/시간 가산 비율과 상태 구간 기준값은 아래에서 직접 조정할 수 있습니다."
      />
      <Alert
        showIcon
        type={planCostSettingsDirty ? 'warning' : 'success'}
        message={
          planCostSettingsDirty ? 'DB 저장값과 편집 중인 비용 설정이 다릅니다.' : '비용 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          planCostSettingsDirty
            ? `변경 항목: ${planCostDiffLabels.join(', ')} / DB 저장값 기준: 백만 토큰당 ${formatCostAmount(
                appConfig.planCost.baseCostPerMillionTokens,
              )}, 재처리 ${appConfig.planCost.retryCostMultiplierPercent}%, ${formatPlanCostTimeMultiplierLabel(appConfig.planCost)}, 관심 ${appConfig.planCost.attentionCostThresholdMultiplier}배, 주의 ${appConfig.planCost.warningCostThresholdMultiplier}배, 높음 ${appConfig.planCost.highCostThresholdMultiplier}배 / 편집 중: 백만 토큰당 ${formatCostAmount(appConfigDraft.planCost.baseCostPerMillionTokens)}, 재처리 ${appConfigDraft.planCost.retryCostMultiplierPercent}%, ${formatPlanCostTimeMultiplierLabel(appConfigDraft.planCost)}, 관심 ${appConfigDraft.planCost.attentionCostThresholdMultiplier}배, 주의 ${appConfigDraft.planCost.warningCostThresholdMultiplier}배, 높음 ${appConfigDraft.planCost.highCostThresholdMultiplier}배`
            : `DB 저장값 기준: 백만 토큰당 ${formatCostAmount(appConfig.planCost.baseCostPerMillionTokens)}, 재처리 ${appConfig.planCost.retryCostMultiplierPercent}%, ${formatPlanCostTimeMultiplierLabel(appConfig.planCost)}, 관심 ${appConfig.planCost.attentionCostThresholdMultiplier}배, 주의 ${appConfig.planCost.warningCostThresholdMultiplier}배, 높음 ${appConfig.planCost.highCostThresholdMultiplier}배`
        }
      />
      {renderFeedback(appConfigFeedback, appConfigCopyFeedback, setAppConfigCopyFeedback)}
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>백만 토큰당 기준 비용</Text>
        <InputNumber
          min={100}
          max={1000000}
          step={100}
          style={{ width: '100%' }}
          addonAfter="원"
          value={appConfigDraft.planCost.baseCostPerMillionTokens}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                baseCostPerMillionTokens: typeof value === 'number' ? value : current.planCost.baseCostPerMillionTokens,
              },
            }));
          }}
        />
        <Text type="secondary">
          예: {formatCostAmount(appConfigDraft.planCost.baseCostPerMillionTokens)} 기준이면 누적 토큰 100만개에서 재처리
          가산 전 기본 비용이 동일하게 계산됩니다.
        </Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>재처리 가산 비율</Text>
        <InputNumber
          min={0}
          max={500}
          step={1}
          style={{ width: '100%' }}
          addonAfter="%"
          value={appConfigDraft.planCost.retryCostMultiplierPercent}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                retryCostMultiplierPercent:
                  typeof value === 'number' ? value : current.planCost.retryCostMultiplierPercent,
              },
            }));
          }}
        />
        <Text type="secondary">재처리 1회마다 기본 비용에 가산할 비율입니다.</Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>시간 가산 비율</Text>
        <Select
          style={{ width: '100%' }}
          value={appConfigDraft.planCost.timeCostUnit}
          options={[
            { value: 'hour', label: '시간 기준' },
            { value: 'minute', label: '분 기준' },
            { value: 'second', label: '초 기준' },
          ]}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                timeCostUnit: value,
              },
            }));
          }}
        />
        <InputNumber
          min={0}
          max={500}
          step={1}
          style={{ width: '100%' }}
          addonAfter="%"
          value={appConfigDraft.planCost.hourlyCostMultiplierPercent}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                hourlyCostMultiplierPercent:
                  typeof value === 'number' ? value : current.planCost.hourlyCostMultiplierPercent,
              },
            }));
          }}
        />
        <Text type="secondary">
          종료 시각 기준 처리시간 {PLAN_COST_TIME_UNIT_LABELS[appConfigDraft.planCost.timeCostUnit]}마다 기본 비용에 가산할
          비율입니다.
        </Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>상태 구간 기준 배수</Text>
        <Space direction={screens.sm ? 'horizontal' : 'vertical'} size={8} style={{ width: '100%' }}>
          <InputNumber
            min={0.1}
            max={100}
            step={0.1}
            style={{ width: screens.sm ? '33.33%' : '100%' }}
            addonBefore="관심"
            addonAfter="배"
            value={appConfigDraft.planCost.attentionCostThresholdMultiplier}
            onChange={(value) => {
              updateAppConfigDraft((current) => ({
                ...current,
                planCost: {
                  ...current.planCost,
                  attentionCostThresholdMultiplier:
                    typeof value === 'number' ? value : current.planCost.attentionCostThresholdMultiplier,
                },
              }));
            }}
          />
          <InputNumber
            min={0.1}
            max={100}
            step={0.1}
            style={{ width: screens.sm ? '33.33%' : '100%' }}
            addonBefore="주의"
            addonAfter="배"
            value={appConfigDraft.planCost.warningCostThresholdMultiplier}
            onChange={(value) => {
              updateAppConfigDraft((current) => ({
                ...current,
                planCost: {
                  ...current.planCost,
                  warningCostThresholdMultiplier:
                    typeof value === 'number' ? value : current.planCost.warningCostThresholdMultiplier,
                },
              }));
            }}
          />
          <InputNumber
            min={0.1}
            max={100}
            step={0.1}
            style={{ width: screens.sm ? '33.33%' : '100%' }}
            addonBefore="높음"
            addonAfter="배"
            value={appConfigDraft.planCost.highCostThresholdMultiplier}
            onChange={(value) => {
              updateAppConfigDraft((current) => ({
                ...current,
                planCost: {
                  ...current.planCost,
                  highCostThresholdMultiplier:
                    typeof value === 'number' ? value : current.planCost.highCostThresholdMultiplier,
                },
              }));
            }}
          />
        </Space>
        <Text type="secondary">기준 비용 대비 몇 배부터 관심, 주의, 높음 상태로 표시할지 설정합니다.</Text>
        <Text type="secondary">현재 편집값 기준: {getPlanCostThresholdPreview(appConfigDraft.planCost)}</Text>
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const automationNotificationPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="자동화 알림을 항목별로 설정합니다."
        description="알림 사용이 켜진 기기에서만 Web Push를 받을 수 있습니다."
      />
      <Alert
        showIcon
        type={automationNotificationSettingsDirty ? 'warning' : 'success'}
        message={
          automationNotificationSettingsDirty
            ? 'DB 저장값과 편집 중인 자동화 알림 설정이 다릅니다.'
            : '자동화 알림 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          automationNotificationSettingsDirty
            ? `변경 항목: ${automationNotificationDiffLabels.join(', ')}`
            : `알림 항목: ${AUTOMATION_NOTIFICATION_OPTIONS.map((option) => option.label).join(', ')}`
        }
      />
      {appConfigFeedback ? (
        <Space direction="vertical" size={8} style={{ width: '100%' }}>
          <Alert
            showIcon
            type={appConfigFeedback.tone}
            message={appConfigFeedback.message}
            action={
              <Button
                type="text"
                size="small"
                aria-label="메시지 복사"
                icon={<CopyOutlined />}
                onClick={() => {
                  void copyText(appConfigFeedback.message)
                    .then(() => {
                      setAppConfigCopyFeedback({ tone: 'success', message: '메시지를 복사했습니다.' });
                    })
                    .catch(() => {
                      setAppConfigCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                    });
                }}
              />
            }
          />
          {appConfigCopyFeedback ? (
            <Alert showIcon type={appConfigCopyFeedback.tone} message={appConfigCopyFeedback.message} />
          ) : null}
        </Space>
      ) : null}
      <Space direction="vertical" size={10} style={{ width: '100%' }}>
        {AUTOMATION_NOTIFICATION_OPTIONS.map((option) => (
          <Checkbox
            key={option.key}
            checked={appConfigDraft.automation[option.key]}
            onChange={(event) => {
              updateAppConfigDraft((current) => ({
                ...current,
                automation: {
                  ...current.automation,
                  [option.key]: event.target.checked,
                },
              }));
            }}
          >
            <Space direction="vertical" size={0}>
              <Text>{option.label}</Text>
              <Text type="secondary">{option.description}</Text>
            </Space>
          </Checkbox>
        ))}
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const gestureShortcutsPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="모바일 제스처와 같은 액션을 키보드 단축키로도 실행합니다."
        description="저장 후 즉시 반영됩니다. `Mod`는 macOS에서는 Command, Windows/Linux에서는 Ctrl로 처리합니다."
      />
      <Alert
        showIcon
        type={gestureShortcutSettingsDirty ? 'warning' : 'success'}
        message={
          gestureShortcutSettingsDirty
            ? 'DB 저장값과 편집 중인 제스처 단축키 설정이 다릅니다.'
            : '제스처 단축키 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          gestureShortcutSettingsDirty
            ? `변경 항목: ${gestureShortcutDiffLabels.join(', ')} / DB 저장값 기준: 통합 검색 ${appConfig.gestureShortcuts.openSearch}, Window UI 검색 ${appConfig.gestureShortcuts.openWindowSearch} / 편집 중: 통합 검색 ${appConfigDraft.gestureShortcuts.openSearch}, Window UI 검색 ${appConfigDraft.gestureShortcuts.openWindowSearch}`
            : `DB 저장값 기준: 통합 검색 ${appConfig.gestureShortcuts.openSearch}, Window UI 검색 ${appConfig.gestureShortcuts.openWindowSearch}`
        }
      />
      {appConfigFeedback ? (
        <Space direction="vertical" size={8} style={{ width: '100%' }}>
          <Alert
            showIcon
            type={appConfigFeedback.tone}
            message={appConfigFeedback.message}
            action={
              <Button
                type="text"
                size="small"
                aria-label="메시지 복사"
                icon={<CopyOutlined />}
                onClick={() => {
                  void copyText(appConfigFeedback.message)
                    .then(() => {
                      setAppConfigCopyFeedback({ tone: 'success', message: '메시지를 복사했습니다.' });
                    })
                    .catch(() => {
                      setAppConfigCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                    });
                }}
              />
            }
          />
          {appConfigCopyFeedback ? (
            <Alert showIcon type={appConfigCopyFeedback.tone} message={appConfigCopyFeedback.message} />
          ) : null}
        </Space>
      ) : null}
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>상단 우측 당기기 액션</Text>
        <Input
          value={appConfigDraft.gestureShortcuts.openSearch}
          placeholder={DEFAULT_APP_CONFIG.gestureShortcuts.openSearch}
          onChange={(event) => {
            updateAppConfigDraft((current) => ({
              ...current,
              gestureShortcuts: {
                ...current.gestureShortcuts,
                openSearch: event.target.value,
              },
            }));
          }}
        />
        <Text type="secondary">통합 검색을 엽니다. 예: `Mod+K`, `Alt+/`</Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>오른쪽 가운데 왼쪽 당기기 액션</Text>
        <Input
          value={appConfigDraft.gestureShortcuts.openWindowSearch}
          placeholder={DEFAULT_APP_CONFIG.gestureShortcuts.openWindowSearch}
          onChange={(event) => {
            updateAppConfigDraft((current) => ({
              ...current,
              gestureShortcuts: {
                ...current.gestureShortcuts,
                openWindowSearch: event.target.value,
              },
            }));
          }}
        />
        <Text type="secondary">선택한 항목을 `Window UI`로 여는 검색을 엽니다. 예: `Mod+Shift+K`</Text>
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const settingsMenu = (
    <div className="app-header__settings-menu">
      {hasAccess ? (
        <>
          <button
            type="button"
            className="app-header__settings-item"
            onClick={() => {
              setSettingsOpen(false);
              onOpenPlanQuickFilter('working');
            }}
          >
            <span className="app-header__settings-icon">
              <ProfileOutlined />
              <span
                className={`app-header__status-dot ${
                  planShortcutCounts.working > 0
                    ? 'app-header__status-dot--active'
                    : 'app-header__status-dot--inactive'
                }`}
                aria-hidden="true"
              />
            </span>
            <span className="app-header__settings-label">
              작업중 항목
              <Text type="secondary"> {planShortcutCounts.working}건</Text>
            </span>
          </button>
          <button
            type="button"
            className="app-header__settings-item"
            onClick={() => {
              setSettingsOpen(false);
              onOpenPlanQuickFilter('release-pending-main');
            }}
          >
            <span className="app-header__settings-icon">
              <ProfileOutlined />
              <span
                className={`app-header__status-dot ${
                  planShortcutCounts.releasePendingMain > 0
                    ? 'app-header__status-dot--warning'
                    : 'app-header__status-dot--inactive'
                }`}
                aria-hidden="true"
              />
            </span>
            <span className="app-header__settings-label">
              release 상태 작업
              <Text type="secondary"> {planShortcutCounts.releasePendingMain}건</Text>
            </span>
          </button>
          <button
            type="button"
            className="app-header__settings-item"
            onClick={() => {
              setSettingsOpen(false);
              onOpenPlanQuickFilter('automation-failed');
            }}
          >
            <span className="app-header__settings-icon">
              <ReloadOutlined />
              <span
                className={`app-header__status-dot ${
                  planShortcutCounts.automationFailed > 0
                    ? 'app-header__status-dot--warning'
                    : 'app-header__status-dot--inactive'
                }`}
                aria-hidden="true"
              />
            </span>
            <span className="app-header__settings-label">
              자동화 실패
              <Text type="secondary"> {planShortcutCounts.automationFailed}건</Text>
            </span>
          </button>
        </>
      ) : null}
      <button
        type="button"
        className="app-header__settings-item"
        disabled={!hasAccess}
        onClick={() => {
          openSettingsModal('appSettings', 'planDefaults');
        }}
        >
        <span className="app-header__settings-icon">
          <SettingOutlined />
          <span className={`app-header__status-dot ${settingsStatusClassName}`} aria-hidden="true" />
        </span>
        <span className="app-header__settings-label">앱 설정</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        disabled={!hasAccess}
        onClick={() => {
          openSettingsModal('notification');
        }}
      >
        <span className="app-header__settings-icon">
          <BellOutlined />
          <span
            className={`app-header__status-dot ${notificationStatusClassName}`}
            aria-hidden="true"
          />
        </span>
        <span className="app-header__settings-label">알림</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        onClick={() => {
          openSettingsModal('token');
        }}
      >
        <span className="app-header__settings-icon">
          <ProfileOutlined />
          <span
            className={`app-header__status-dot ${
              hasAccess ? 'app-header__status-dot--active' : 'app-header__status-dot--inactive'
            }`}
            aria-hidden="true"
          />
        </span>
        <span className="app-header__settings-label">권한</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        onClick={() => {
          openSettingsModal('update');
        }}
      >
        <span className="app-header__settings-icon">
          <ReloadOutlined />
          <span className={`app-header__status-dot ${settingsStatusClassName}`} aria-hidden="true" />
        </span>
        <span className="app-header__settings-label">업데이트</span>
      </button>
    </div>
  );

  return (
    <>
      {modalContextHolder}
      <Header className="app-header">
        <Space size={12} className="app-header__row">
          <Space size={12} className="app-header__menu-side">
            <Button
              type="text"
              icon={sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
              onClick={onToggleSidebar}
            />
            <Segmented
              value={headerTopMenu}
              options={
                hasAccess
                  ? [
                      { label: 'Docs', value: 'docs', icon: <FileMarkdownOutlined /> },
                      { label: '작업', value: 'plans', icon: <ProfileOutlined /> },
                    ]
                  : [{ label: 'Docs', value: 'docs', icon: <FileMarkdownOutlined /> }]
              }
              onChange={(value) => {
                onChangeTopMenu(value as 'docs' | 'plans');
              }}
            />
          </Space>

          <Space size={4} className="app-header__actions">
            {hasAccess ? (
              <>
                <button
                  type="button"
                  className={connectionIndicatorClassName}
                  aria-label={chatConnectionLabel}
                  title={chatConnectionLabel}
                  onClick={() => {
                    setIsRuntimeModalOpen(true);
                  }}
                >
                  <span className="app-header__settings-icon" aria-hidden="true">
                    <ApiOutlined />
                    <span className={`app-header__status-dot ${chatConnectionStatusClassName}`} />
                  </span>
                  {runningRuntimeCount > 0 ? (
                    <span
                      className={connectionCountBadgeClassName}
                      aria-label={`실행 중 요청 ${runningRuntimeCount}건`}
                      title={`실행 중 요청 ${runningRuntimeCount}건`}
                    >
                      {runningRuntimeBadgeLabel}
                    </span>
                  ) : null}
                </button>
                <HeaderMessageCenter isMobileViewport={isMobileViewport} />
                <Dropdown
                  trigger={['click']}
                  open={settingsOpen}
                  onOpenChange={setSettingsOpen}
                  placement="bottomRight"
                  popupRender={() => settingsMenu}
                >
                  {settingsTriggerButton}
                </Dropdown>
              </>
            ) : (
              tokenTriggerButton
            )}
          </Space>
        </Space>
      </Header>

      <Modal
        title="Codex 런타임"
        open={isRuntimeModalOpen}
        footer={null}
        onCancel={() => {
          setIsRuntimeModalOpen(false);
        }}
      >
        <Space direction="vertical" size={12} style={{ width: '100%' }}>
          <div className="app-header__runtime-summary">
            <div className="app-header__runtime-summary-card" title="실행 중 요청">
              <LoadingOutlined />
              <Text type="secondary">실행</Text>
              <Text strong>{runningRuntimeCount}</Text>
            </div>
            <div className="app-header__runtime-summary-card" title="대기 요청">
              <ClockCircleOutlined />
              <Text type="secondary">대기</Text>
              <Text strong>{queuedRuntimeCount}</Text>
            </div>
            <div className="app-header__runtime-summary-card" title="활성 세션">
              <ProfileOutlined />
              <Text type="secondary">활성</Text>
              <Text strong>{runtimeSessionCount}</Text>
            </div>
          </div>
          <Alert
            type={
              chatConnection.connectionState === 'connected'
                ? 'success'
                : chatConnection.connectionState === 'connecting'
                  ? 'info'
                  : 'warning'
            }
            showIcon
            message={
              chatConnection.connectionState === 'connected'
                ? '웹소켓으로 실시간 반영 중입니다.'
                : chatConnection.connectionState === 'connecting'
                  ? '웹소켓을 다시 연결하는 중입니다.'
                  : '웹소켓 연결이 끊겨 최신 상태가 지연될 수 있습니다.'
            }
            description={
              chatRuntimeSnapshot?.generatedAt
                ? `마지막 반영 ${formatRuntimeRelativeLabel(chatRuntimeSnapshot.generatedAt)}`
                : '아직 수신된 런타임 정보가 없습니다.'
            }
          />
          <div className="app-header__runtime-list">
            <Text strong>실행 중 요청</Text>
            {chatRuntimeSnapshot && chatRuntimeSnapshot.running.length > 0 ? (
              chatRuntimeSnapshot.running.map((item) => (
                <div key={item.requestId} className="app-header__runtime-list-item">
                  <div className="app-header__runtime-list-row">
                    <div className="app-header__runtime-list-copy">
                      <Text strong>{getConversationLabel(item.sessionId)}</Text>
                      <Text className="app-header__runtime-summary-text">{item.summary || '요약 없음'}</Text>
                      <div className="app-header__runtime-meta">
                        <span className="app-header__runtime-badge">
                          <LoadingOutlined />
                          실행
                        </span>
                        <span className="app-header__runtime-badge">{formatRuntimeRelativeLabel(item.startedAt)}</span>
                      </div>
                    </div>
                    <Space size={8} wrap>
                      <Button
                        size="small"
                        type="default"
                        onClick={() => {
                          void openRuntimeLog(item.requestId);
                        }}
                      >
                        로그 보기
                      </Button>
                      <Button size="small" type="default" onClick={() => navigateToConversation(item.sessionId)}>
                        이동
                      </Button>
                    </Space>
                  </div>
                </div>
              ))
            ) : (
              <Text type="secondary">현재 실행 중인 요청이 없습니다.</Text>
            )}
          </div>
          <div className="app-header__runtime-list">
            <Text strong>대기 요청</Text>
            {chatRuntimeSnapshot && chatRuntimeSnapshot.queued.length > 0 ? (
              chatRuntimeSnapshot.queued.map((item) => (
                <div key={item.requestId} className="app-header__runtime-list-item">
                  <div className="app-header__runtime-list-row">
                    <div className="app-header__runtime-list-copy">
                      <Text strong>{getConversationLabel(item.sessionId)}</Text>
                      <Text className="app-header__runtime-summary-text">{item.summary || '요약 없음'}</Text>
                      <div className="app-header__runtime-meta">
                        <span className="app-header__runtime-badge">
                          <ClockCircleOutlined />
                          대기
                        </span>
                        <span className="app-header__runtime-badge">{formatRuntimeRelativeLabel(item.enqueuedAt)}</span>
                      </div>
                    </div>
                    <Button size="small" type="default" onClick={() => navigateToConversation(item.sessionId)}>
                      이동
                    </Button>
                  </div>
                </div>
              ))
            ) : (
              <Text type="secondary">현재 대기 중인 요청이 없습니다.</Text>
            )}
          </div>
          <div className="app-header__runtime-list">
            <Text strong>최근 작업</Text>
            {chatRuntimeSnapshot && chatRuntimeSnapshot.recent.length > 0 ? (
              chatRuntimeSnapshot.recent.map((item) => (
                <div key={`recent-${item.requestId}-${item.lastUpdatedAt}`} className="app-header__runtime-list-item">
                  <div className="app-header__runtime-list-row">
                    <div className="app-header__runtime-list-copy">
                      <Text strong>{getConversationLabel(item.sessionId)}</Text>
                      <Text className="app-header__runtime-summary-text">{item.summary || '요약 없음'}</Text>
                      <div className="app-header__runtime-meta">
                        <span className="app-header__runtime-badge">{buildRuntimeTerminalLabel(item.terminalStatus)}</span>
                        <span className="app-header__runtime-badge">
                          {formatRuntimeRelativeLabel(item.lastUpdatedAt)}
                        </span>
                      </div>
                    </div>
                    <Space size={8} wrap>
                      <Button
                        size="small"
                        type="default"
                        onClick={() => {
                          void openRuntimeLog(item.requestId);
                        }}
                      >
                        로그 보기
                      </Button>
                      <Button size="small" type="default" onClick={() => navigateToConversation(item.sessionId)}>
                        이동
                      </Button>
                    </Space>
                  </div>
                </div>
              ))
            ) : (
              <Text type="secondary">최근 종료된 작업이 없습니다.</Text>
            )}
          </div>
        </Space>
      </Modal>

      <Drawer
        open={isRuntimeLogDrawerOpen}
        title="실행 로그"
        placement="right"
        width="100vw"
        onClose={() => {
          setIsRuntimeLogDrawerOpen(false);
          setRuntimeLogDetail(null);
          setRuntimeLogError('');
        }}
        styles={{
          body: {
            padding: 24,
          },
        }}
      >
        {runtimeLogLoading ? (
          <Text type="secondary">로그를 불러오는 중입니다.</Text>
        ) : runtimeLogError ? (
          <Text type="danger">{runtimeLogError}</Text>
        ) : runtimeLogDetail ? (
          <Space direction="vertical" size={12} style={{ width: '100%' }}>
            {runtimeLogDetail.item?.sessionId ? (
              <Button
                size="small"
                type="default"
                style={{ alignSelf: 'flex-start' }}
                onClick={() => {
                  navigateToConversation(runtimeLogDetail.item?.sessionId ?? '');
                  setIsRuntimeLogDrawerOpen(false);
                }}
              >
                채팅방 이동
              </Button>
            ) : null}
            <div className="app-chat-runtime__job-meta">
              <Text type="secondary">요청: {runtimeLogDetail.item?.requestId ?? '-'}</Text>
              <Text type="secondary">세션: {runtimeLogDetail.item?.sessionId ?? '-'}</Text>
              <Text type="secondary">마지막 갱신: {formatRuntimeTimestamp(runtimeLogDetail.lastUpdatedAt)}</Text>
              <Text type="secondary">
                종료 상태: {buildRuntimeTerminalLabel(runtimeLogDetail.terminalStatus)}
              </Text>
            </div>
            <Paragraph className="app-chat-runtime__job-summary">
              {runtimeLogDetail.item?.summary ?? '요약 없음'}
            </Paragraph>
            <pre className="app-chat-runtime__log-viewer">
              {runtimeLogDetail.logs.length > 0 ? runtimeLogDetail.logs.join('\n') : '아직 기록된 로그가 없습니다.'}
            </pre>
          </Space>
        ) : (
          <Text type="secondary">표시할 로그가 없습니다.</Text>
        )}
      </Drawer>

      <Modal
        title={getSettingsModalTitle(activeSettingsModal)}
        open={settingsModalOpen}
        footer={null}
        confirmLoading={notificationLoading}
        onCancel={() => {
          setSettingsModalOpen(false);
        }}
      >
        <Space direction="vertical" size={12} style={{ width: '100%' }}>
          {activeSettingsModal === 'appSettings' ? (
            <>
              <Segmented
                block
                value={activeAppSettingsCategory}
                options={[...APP_SETTINGS_CATEGORIES]}
                onChange={(value) => {
                  setActiveAppSettingsCategory(value as AppSettingsCategoryKey);
                }}
              />
              <Select
                value={activeAppSettingsSection}
                options={activeAppSettingsSectionOptions}
                onChange={(value) => {
                  setActiveAppSettingsSection(value);
                }}
              />
              {activeAppSettingsSection === 'planDefaults' ? planDefaultsPanel : null}
              {activeAppSettingsSection === 'planCost' ? planCostPanel : null}
              {activeAppSettingsSection === 'chatSettings' ? chatSettingsPanel : null}
              {activeAppSettingsSection === 'worklogAutomation' ? worklogAutomationPanel : null}
              {activeAppSettingsSection === 'automationNotifications' ? automationNotificationPanel : null}
              {activeAppSettingsSection === 'gestureShortcuts' ? gestureShortcutsPanel : null}
            </>
          ) : null}
          {activeSettingsModal === 'notification' ? (
            <Space direction="vertical" size={12} style={{ width: '100%' }}>
              <Paragraph type="secondary" style={{ marginBottom: 0 }}>
                알림을 `On`으로 바꾸면 현재 기기의 브라우저 권한을 확인한 뒤 Web Push 구독을 생성하고 서버에 자동 등록합니다.
              </Paragraph>
              <Text type="secondary">클라이언트 알림 권한 상태: {getClientNotificationPermissionLabel(clientNotificationPermission)}</Text>
              <Text type="secondary">PWA 실행 상태: {isStandaloneMode ? '홈 화면 앱' : '브라우저 탭'}</Text>
              <Text type="secondary">서버 Web Push 설정: {webPushConfigured ? '준비됨' : '미설정'}</Text>
              {isAppleMobileDevice() && !isStandaloneMode ? (
                <Text type="warning">아이폰에서는 홈 화면에 추가한 뒤 연 PWA에서만 웹 푸시를 사용할 수 있습니다.</Text>
              ) : null}
              {clientNotificationPermission === 'denied' ? (
                <Text type="warning">브라우저 또는 기기 설정에서 이 사이트의 알림 권한을 허용한 뒤 다시 시도해 주세요.</Text>
              ) : null}
              {clientNotificationPermission === 'unsupported' ? (
                <Text type="warning">현재 환경에서는 Web Push 알림 등록을 지원하지 않습니다.</Text>
              ) : null}
              {!hasSecureOrigin() ? (
                <Text type="warning">알림은 HTTPS 또는 localhost 환경에서만 사용할 수 있습니다.</Text>
              ) : null}
              {renderFeedback(notificationFeedback, notificationCopyFeedback, setNotificationCopyFeedback)}
              <Checkbox
                checked={notificationEnabled}
                disabled={notificationLoading}
                onChange={(event) => {
                  void syncNotificationEnabled(event.target.checked);
                }}
              >
                알림 사용
              </Checkbox>
              <Space direction="vertical" size={6} style={{ width: '100%' }}>
                <Text strong>PWA 토큰 등록</Text>
                <Text type="secondary">
                  PWA에서 전달받은 알림 토큰은 권한 토큰과 별도로 등록합니다.
                </Text>
                <Text type="secondary">PWA 토큰 상태: {registeredPwaNotificationToken ? '등록됨' : '미등록'}</Text>
                <Input.TextArea
                  value={pwaNotificationTokenInput}
                  placeholder="PWA에서 받은 알림 토큰 입력"
                  autoSize={{ minRows: 2, maxRows: 4 }}
                  onChange={(event) => {
                    setPwaNotificationTokenInput(event.target.value);
                  }}
                />
                {renderFeedback(
                  pwaNotificationTokenFeedback,
                  pwaNotificationTokenCopyFeedback,
                  setPwaNotificationTokenCopyFeedback,
                )}
                <Space wrap>
                  <Button
                    type="primary"
                    loading={pwaNotificationTokenSaving}
                    onClick={() => {
                      void handleRegisterPwaNotificationToken();
                    }}
                  >
                    PWA 토큰 등록
                  </Button>
                  <Button
                    disabled={pwaNotificationTokenSaving || !registeredPwaNotificationToken}
                    onClick={() => {
                      void handleClearPwaNotificationToken();
                    }}
                  >
                    PWA 토큰 제거
                  </Button>
                </Space>
              </Space>
            </Space>
          ) : null}
          {activeSettingsModal === 'token' ? (
            <Space direction="vertical" size={6} style={{ width: '100%' }}>
              <Text strong>토큰 관리</Text>
              <Text type="secondary">
                관리자에게 전달받은 권한 토큰을 등록한 사용자만 Codex 채팅과 새 메모 등록을 사용할 수 있습니다.
              </Text>
              <Text type="secondary">권한 상태: {hasAccess ? '허용됨' : '차단됨'}</Text>
              <Input.Password
                value={tokenInput}
                placeholder="권한 토큰 입력"
                onChange={(event) => {
                  setTokenInput(event.target.value);
                }}
              />
              {tokenFeedback ? <Alert showIcon type={tokenFeedback.tone} message={tokenFeedback.message} /> : null}
              <Space wrap>
                <Button type="primary" onClick={handleRegisterToken}>
                  토큰 등록
                </Button>
                <Button onClick={handleClearToken} disabled={!registeredToken}>
                  토큰 제거
                </Button>
              </Space>
            </Space>
          ) : null}
          {activeSettingsModal === 'update' ? (
            <Space direction="vertical" size={6} style={{ width: '100%' }}>
              <Text strong>업데이트 확인</Text>
              <Text type="secondary">
                테스트
                <span
                  className={`app-header__server-version-indicator ${getServerVersionStatusClassName(testServerStatus)}`}
                  aria-label={getServerVersionStatusTitle(testServerStatus, '테스트')}
                  title={getServerVersionStatusTitle(testServerStatus, '테스트')}
                  style={{ marginInlineStart: 8, verticalAlign: 'middle' }}
                  aria-hidden="true"
                />
              </Text>
              <Text type="secondary">
                소스 수정일: {getServerLastSourceChangedDateLabel(testServerStatus)}
              </Text>
              <Text type="secondary">
                워크
                <span
                  className={`app-header__server-version-indicator ${getServerVersionStatusClassName(workServerStatus)}`}
                  aria-label={getServerVersionStatusTitle(workServerStatus, '워크')}
                  title={getServerVersionStatusTitle(workServerStatus, '워크')}
                  style={{ marginInlineStart: 8, verticalAlign: 'middle' }}
                  aria-hidden="true"
                />
              </Text>
              <Text type="secondary">
                소스 수정일: {getServerLastSourceChangedDateLabel(workServerStatus)}
              </Text>
              <Text type="secondary">
                운영
                <span
                  className={`app-header__server-version-indicator ${getServerVersionStatusClassName(prodServerStatus)}`}
                  aria-label={getServerVersionStatusTitle(prodServerStatus, '운영')}
                  title={getServerVersionStatusTitle(prodServerStatus, '운영')}
                  style={{ marginInlineStart: 8, verticalAlign: 'middle' }}
                  aria-hidden="true"
                />
              </Text>
              <Text type="secondary">{formatDateTimeLabel(prodServerStatus?.runningBuiltAt ?? null)}</Text>
              {renderFeedback(updateCheckFeedback, updateCheckCopyFeedback, setUpdateCheckCopyFeedback)}
              <Button
                block
                icon={workServerStatusLoading ? <ReloadOutlined spin /> : <ReloadOutlined />}
                loading={workServerStatusLoading}
                disabled={!canRefreshWorkServerStatus}
                onClick={() => {
                  void refreshUpdateTargets();
                }}
              >
                업데이트 확인
              </Button>
              <Text strong style={{ marginTop: 8 }}>
                캐시 / 스토리지 초기화
              </Text>
              <Text type="secondary">
                권한, 앱 설정, 푸시/서비스워커 등록은 유지하고 화면 상태와 앱 캐시만 초기화합니다.
              </Text>
              {renderFeedback(clientResetFeedback, clientResetCopyFeedback, setClientResetCopyFeedback)}
              <Button
                block
                danger
                icon={clientResetting ? <ReloadOutlined spin /> : <ReloadOutlined />}
                loading={clientResetting}
                disabled={!canResetClientState}
                onClick={() => {
                  void handleResetClientState();
                }}
                >
                  {clientResetting ? '초기화 진행 중' : '초기화'}
              </Button>
              <Text strong style={{ marginTop: 8 }}>
                서버 재기동
              </Text>
              <Text type="secondary">전체 재기동은 TEST와 WORK 서버만 순서대로 진행합니다.</Text>
              <Text type="secondary">
                테스트 마지막 확인: {formatDateTimeLabel(testServerStatus?.checkedAt ?? null)}
              </Text>
              <Text type="secondary">
                워크 마지막 확인: {formatDateTimeLabel(workServerStatus?.checkedAt ?? null)}
              </Text>
              {!hasAccess ? (
                <Text type="warning">서버 재기동은 권한 토큰 등록 후 사용할 수 있습니다.</Text>
              ) : null}
              {renderFeedback(serverRestartFeedback, serverRestartCopyFeedback, setServerRestartCopyFeedback)}
              <Space direction={screens.xs ? 'vertical' : 'horizontal'} style={{ width: '100%' }}>
                <Button
                  block={screens.xs}
                  icon={serverRestartingKey === 'test' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                  loading={serverRestartingKey === 'test'}
                  disabled={!canRestartServers}
                  onClick={() => {
                    void handleRestartSingleServer('test');
                  }}
                >
                  테스트 재기동
                </Button>
                <Button
                  block={screens.xs}
                  icon={serverRestartingKey === 'work-server' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                  loading={serverRestartingKey === 'work-server'}
                  disabled={!canRestartServers}
                  onClick={() => {
                    void handleRestartSingleServer('work-server');
                  }}
                >
                  워크 재기동
                </Button>
                <Button
                  type="primary"
                  block={screens.xs}
                  icon={serverRestartingKey === 'all' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                  loading={serverRestartingKey === 'all'}
                  disabled={!canRestartServers}
                  onClick={() => {
                    void handleRestartBothServers();
                  }}
                >
                  전체 재기동
                </Button>
              </Space>
              <Text strong style={{ marginTop: 8 }}>
                PROD 빌드 반영
              </Text>
              <Text type="secondary">운영 마지막 확인: {formatDateTimeLabel(prodServerStatus?.checkedAt ?? null)}</Text>
              <Text type="secondary">
                PROD 컨테이너는 전체 재기동에 포함하지 않고, 별도 확인 후 빌드와 재기동을 진행합니다.
              </Text>
              <Button
                type="primary"
                danger
                block
                icon={serverRestartingKey === 'prod' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                loading={serverRestartingKey === 'prod'}
                disabled={!canRestartServers}
                onClick={handleConfirmRestartProdServer}
              >
                PROD 빌드 반영
              </Button>
            </Space>
          ) : null}
        </Space>
      </Modal>
    </>
  );
}
