import {
  ArrowLeftOutlined,
  CloseOutlined,
  CopyOutlined,
  DownloadOutlined,
  DownOutlined,
  FullscreenExitOutlined,
  FullscreenOutlined,
  PaperClipOutlined,
  UpOutlined,
} from '@ant-design/icons';
import {
  Alert,
  Button,
  Card,
  Checkbox,
  Empty,
  Flex,
  Grid,
  Input,
  List,
  Modal,
  Segmented,
  Select,
  Space,
  Spin,
  Tag,
  Tabs,
  Typography,
  message,
} from 'antd';
import { memo, useEffect, useMemo, useRef, useState, type ChangeEvent, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppConfig, type AppConfig } from '../../app/main/appConfig';
import { confirmWithKeyboard } from '../../app/main/modalKeyboard';
import {
  buildAutomationTypeOptions,
  resolveAutomationTypeLabel,
  useAutomationTypeRegistry,
} from '../../app/main/automationTypeAccess';
import {
  buildAutomationContextOptions,
  resolveDefaultAutomationContextIds,
  useAutomationContextRegistry,
} from '../../app/main/automationContextAccess';
import { buildPlansPath } from '../../app/main/routes';
import { uploadChatComposerFile } from '../../app/main/mainChatPanel';
import { normalizeChatResourceUrl } from '../../app/main/mainChatPanel/chatResourceUrl';
import type { ChatComposerAttachment } from '../../app/main/mainChatPanel/types';
import { useTokenAccess } from '../../app/main/tokenAccess';
import './planBoard.css';
import {
  type EvidenceAttachmentItem,
  EvidenceAttachmentPreviewBody,
  EvidenceAttachmentStrip,
} from '../../components/evidenceAttachmentStrip';
import { CodexDiffPreviewer, PreviewerUI } from '../../components/previewer';
import {
  appendPlanActionHistory,
  appendPlanIssueAction,
  createPlanItem,
  deletePlanItem,
  type PlanApiRequestMeta,
  fetchPlanActionHistories,
  fetchPlanIssueHistories,
  fetchPlanItemsWithLatestSourceWorks,
  fetchReleaseReviewBoardItems,
  fetchPlanSourceWorkHistories,
  fetchPlanSourceWorkHistory,
  runPlanAction,
  setupPlanBoard,
  updatePlanItemJangsingProcessingRequired,
  updatePlanItem,
} from './api';
import {
  getPlanQuickFilterLabel,
  isAutomationFailedItem,
  isReleasePendingMainItem,
  isWorkingPlanItem,
  type PlanQuickFilter,
} from './quickFilters';
import { PlanListDetailLayout } from './PlanListDetailLayout';
import { maskNotePreviewByWord } from './noteMasking';
import type {
  PlanActionHistory,
  PlanActionType,
  PlanAutomationUsageSnapshot,
  PlanDraft,
  PlanFilterStatus,
  PlanIssueHistory,
  PlanItem,
  PlanReleaseReviewBoardItem,
  PlanSourceWorkHistory,
} from './types';

const { Paragraph, Text, Title } = Typography;
const { TextArea } = Input;

const statusTagColorMap: Record<PlanDraft['status'], string> = {
  등록: 'default',
  작업중: 'processing',
  작업완료: 'cyan',
  릴리즈완료: 'geekblue',
  완료: 'success',
};
const ACTIVE_WORKER_STATUSES = new Set([
  '브랜치생성중',
  '자동작업중',
  'release반영중',
  'main반영중',
]);
const AUTO_REFRESH_CANDIDATE_WORKER_STATUSES = new Set([
  ...ACTIVE_WORKER_STATUSES,
  '브랜치준비',
  'release반영대기',
  'main반영대기',
]);
const AUTO_REFRESH_LONG_PRESS_MS = 700;

const COLLAPSIBLE_DETAIL_MAX_LINES = 4;
const COLLAPSIBLE_DETAIL_MAX_CHARS = 180;
const PLAN_LIST_PAGE_SIZE = 10;
type WorkerStateFilter = 'all' | 'active' | 'waiting' | 'failed' | 'done' | 'idle';
type ReleaseStateFilter = 'all' | 'pending' | 'merged' | 'failed';
type MainStateFilter = 'all' | 'pending' | 'merged' | 'failed' | 'not-targeted';
type IssueStateFilter = 'all' | 'open' | 'none';
type CostStateFilter = 'all' | 'recorded' | 'none' | 'stable' | 'attention' | 'warning' | 'high';
const WORKER_STATE_FILTER_OPTIONS: Array<{ label: string; value: WorkerStateFilter }> = [
  { label: '작업자 전체', value: 'all' },
  { label: '진행중', value: 'active' },
  { label: '대기', value: 'waiting' },
  { label: '실패', value: 'failed' },
  { label: '완료', value: 'done' },
  { label: '미시작', value: 'idle' },
];
const RELEASE_STATE_FILTER_OPTIONS: Array<{ label: string; value: ReleaseStateFilter }> = [
  { label: 'release 전체', value: 'all' },
  { label: 'release 대기/진행', value: 'pending' },
  { label: 'release 완료', value: 'merged' },
  { label: 'release 실패', value: 'failed' },
];
const MAIN_STATE_FILTER_OPTIONS: Array<{ label: string; value: MainStateFilter }> = [
  { label: 'main 전체', value: 'all' },
  { label: 'main 대기/진행', value: 'pending' },
  { label: 'main 완료', value: 'merged' },
  { label: 'main 실패', value: 'failed' },
  { label: 'main 미대상', value: 'not-targeted' },
];
const ISSUE_STATE_FILTER_OPTIONS: Array<{ label: string; value: IssueStateFilter }> = [
  { label: '이슈 전체', value: 'all' },
  { label: '열린 이슈', value: 'open' },
  { label: '이슈 없음', value: 'none' },
];
const COST_STATE_FILTER_OPTIONS: Array<{ label: string; value: CostStateFilter }> = [
  { label: '비용상태 전체', value: 'all' },
  { label: '비용 기록 있음', value: 'recorded' },
  { label: '비용 기록 없음', value: 'none' },
  { label: '안정', value: 'stable' },
  { label: '관심', value: 'attention' },
  { label: '주의', value: 'warning' },
  { label: '높음', value: 'high' },
];
type ReviewListIndicator = {
  status: PlanReleaseReviewBoardItem['review']['status'];
  checkedCount: number;
  totalCount: number;
};

type PlanNoteResource = {
  id: string;
  label: string;
  sourcePath: string;
  publicUrl: string;
  previewType: 'image' | 'document' | 'link';
  isProbablyOpenable: boolean;
};

const PLAN_NOTE_RESOURCE_LINE_PATTERN =
  /^\s*-\s+(.+?):\s+((?:\/api\/chat\/resources\/[^\s)`]+)|(?:\/?(?:public\/)?\.codex_chat\/[^\s)`]+\/resource\/[^\s)`]+)|(?:public\/\.codex_chat\/[^\s)`]+\/resource\/[^\s)`]+))\s*$/;
const PLAN_NOTE_RESOURCE_GLOBAL_PATTERN =
  /(?:\/api\/chat\/resources\/[^\s)`]+)|(?:\/?(?:public\/)?\.codex_chat\/[^\s)`]+\/resource\/[^\s)`]+)|(?:public\/\.codex_chat\/[^\s)`]+\/resource\/[^\s)`]+)/g;
const PLAN_NOTE_IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp']);
const PLAN_NOTE_DOCUMENT_EXTENSIONS = new Set([
  'pdf',
  'txt',
  'md',
  'json',
  'ts',
  'tsx',
  'js',
  'jsx',
  'mjs',
  'cjs',
  'css',
  'html',
  'diff',
  'log',
]);

function createPlanNoteAttachmentSessionId() {
  return `plan-note-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}

function normalizePlanNoteResourceSourcePath(value: string) {
  return String(value ?? '')
    .trim()
    .replace(/[)>.,]+$/, '')
    .replace(/^\/+/, '/')
    .replace(/^public\/(?=\.codex_chat\/)/, '');
}

function normalizePlanNoteResourceUrl(value: string) {
  const normalizedSourcePath = normalizePlanNoteResourceSourcePath(value);
  if (!normalizedSourcePath) {
    return '';
  }

  if (normalizedSourcePath.startsWith('/api/chat/resources/')) {
    return normalizeChatResourceUrl(normalizedSourcePath);
  }

  if (normalizedSourcePath.startsWith('/.codex_chat/')) {
    return normalizeChatResourceUrl(normalizedSourcePath);
  }

  if (normalizedSourcePath.startsWith('.codex_chat/')) {
    return normalizeChatResourceUrl(`/${normalizedSourcePath}`);
  }

  return normalizeChatResourceUrl(normalizedSourcePath);
}

function getPlanNoteResourceBaseName(sourcePath: string) {
  const normalized = normalizePlanNoteResourceSourcePath(sourcePath).replace(/^\/+/, '');
  const segments = normalized.split('/').filter(Boolean);
  return segments.at(-1) ?? normalized;
}

function resolvePlanNoteResourcePreviewType(sourcePath: string): PlanNoteResource['previewType'] {
  const baseName = getPlanNoteResourceBaseName(sourcePath);
  const extension = baseName.includes('.') ? baseName.split('.').at(-1)?.toLowerCase() ?? '' : '';

  if (PLAN_NOTE_IMAGE_EXTENSIONS.has(extension)) {
    return 'image';
  }

  if (PLAN_NOTE_DOCUMENT_EXTENSIONS.has(extension)) {
    return 'document';
  }

  return 'link';
}

function isProbablyOpenablePlanNoteResource(sourcePath: string) {
  const normalized = normalizePlanNoteResourceSourcePath(sourcePath).replace(/^\/+/, '');

  if (!normalized || normalized.endsWith('/')) {
    return false;
  }

  const baseName = getPlanNoteResourceBaseName(normalized).trim().toLowerCase();

  if (!baseName || baseName === 'resource' || baseName === 'uploads' || baseName === '.codex_chat') {
    return false;
  }

  return baseName.includes('.');
}

function extractPlanNoteResources(note: string) {
  const normalizedNote = String(note ?? '');
  const lineEntries = normalizedNote
    .split(/\r?\n/)
    .map((line) => {
      const matched = line.match(PLAN_NOTE_RESOURCE_LINE_PATTERN);

      if (!matched) {
        return null;
      }

      return {
        label: matched[1]?.trim() || getPlanNoteResourceBaseName(matched[2] ?? ''),
        sourcePath: normalizePlanNoteResourceSourcePath(matched[2] ?? ''),
      };
    })
    .filter((item): item is { label: string; sourcePath: string } => Boolean(item?.sourcePath));

  const seen = new Set(lineEntries.map((item) => item.sourcePath));
  const genericEntries = Array.from(normalizedNote.matchAll(PLAN_NOTE_RESOURCE_GLOBAL_PATTERN))
    .map((matched) => normalizePlanNoteResourceSourcePath(matched[0] ?? ''))
    .filter(Boolean)
    .filter((sourcePath) => {
      if (seen.has(sourcePath)) {
        return false;
      }

      seen.add(sourcePath);
      return true;
    })
    .map((sourcePath) => ({
      label: getPlanNoteResourceBaseName(sourcePath),
      sourcePath,
    }));

  return [...lineEntries, ...genericEntries].map((item, index) => ({
    id: `${index}-${item.sourcePath}`,
    label: item.label,
    sourcePath: item.sourcePath,
    publicUrl: normalizePlanNoteResourceUrl(item.sourcePath),
    previewType: resolvePlanNoteResourcePreviewType(item.sourcePath),
    isProbablyOpenable: isProbablyOpenablePlanNoteResource(item.sourcePath),
  }));
}

function appendPlanNoteAttachments(note: string, attachments: ChatComposerAttachment[]) {
  if (attachments.length === 0) {
    return note;
  }

  const existingSourcePaths = new Set(extractPlanNoteResources(note).map((item) => item.sourcePath));
  const nextLines = attachments
    .map((attachment) => {
      const sourcePath = normalizePlanNoteResourceSourcePath(attachment.path);
      return {
        label: attachment.name.trim() || getPlanNoteResourceBaseName(sourcePath),
        sourcePath,
      };
    })
    .filter((item) => item.sourcePath)
    .filter((item) => {
      if (existingSourcePaths.has(item.sourcePath)) {
        return false;
      }

      existingSourcePaths.add(item.sourcePath);
      return true;
    })
    .map((item) => `- ${item.label}: ${item.sourcePath}`);

  if (nextLines.length === 0) {
    return note;
  }

  const currentNote = note.trimEnd();
  const attachmentSectionPattern = /(^|\n)첨부 파일:\n(?:- .+\n?)*/;
  const matchedSection = currentNote.match(attachmentSectionPattern);

  if (!matchedSection || matchedSection.index === undefined) {
    return `${currentNote}${currentNote ? '\n\n' : ''}첨부 파일:\n${nextLines.join('\n')}`;
  }

  const startIndex = matchedSection.index;
  const matchedText = matchedSection[0];
  const insertIndex = startIndex + matchedText.length;
  const prefix = currentNote.slice(0, insertIndex).replace(/\n*$/, '\n');
  const suffix = currentNote.slice(insertIndex).replace(/^\n+/, '\n');

  return `${prefix}${nextLines.join('\n')}${suffix}`;
}

function resolvePlanNoteAttachmentSessionId(
  draftId: number | null,
  fallbackSessionId: string,
) {
  if (draftId) {
    return `plan-note-${draftId}`;
  }

  return fallbackSessionId;
}

function isPlanItemRequestLocked(item: Pick<PlanItem, 'startedAt'> | null | undefined) {
  return Boolean(item?.startedAt);
}

const NON_RETRY_NOTIFICATION_PATTERNS = [
  '다시 실행하지 않았습니다.',
  '재처리는 요청하지 않았습니다.',
  '자동 재처리 대상은 없습니다.',
] as const;
const TOKEN_ACCESS_REQUIRED_MESSAGE = '설정 > 토큰 관리에서 권한 토큰을 등록한 사용자만 자동화 조회 외 기능을 사용할 수 있습니다.';

function formatPlanNotificationLabel(item: Pick<PlanItem, 'id' | 'workId'>) {
  const normalizedWorkId = item.workId.trim().replace(/^\[|\]$/g, '').replace(/\s+/g, '').toLowerCase();

  if (!normalizedWorkId || normalizedWorkId === '작업id' || normalizedWorkId === 'workid') {
    return `#${item.id}`;
  }

  return item.workId;
}

function formatPlanListLabel(item: Pick<PlanItem, 'id' | 'workId'>) {
  const normalizedWorkId = item.workId.trim().replace(/^\[|\]$/g, '').replace(/\s+/g, '').toLowerCase();

  if (!normalizedWorkId || normalizedWorkId === '작업id' || normalizedWorkId === 'workid') {
    return `#${item.id}`;
  }

  return `#${item.id} ${item.workId}`;
}

function formatPlanDateTime(value: string | null | undefined) {
  if (!value) {
    return '미기록';
  }

  return new Date(value).toLocaleString('ko-KR', {
    timeZone: 'Asia/Seoul',
  });
}

function formatResponseBytes(value: number | null | undefined) {
  const bytes = Number(value ?? 0);

  if (!Number.isFinite(bytes) || bytes <= 0) {
    return '0 B';
  }

  if (bytes >= 1024 * 1024) {
    return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
  }

  if (bytes >= 1024) {
    return `${(bytes / 1024).toFixed(1)} KB`;
  }

  return `${Math.round(bytes)} B`;
}

function getReleaseReviewListTagColor(status: PlanReleaseReviewBoardItem['review']['status']) {
  if (status === 'approved') {
    return 'success';
  }

  if (status === 'changes-requested') {
    return 'error';
  }

  if (status === 'reviewing') {
    return 'processing';
  }

  return 'default';
}

function getReleaseReviewListTagLabel(review: ReviewListIndicator) {
  const progressLabel = review.totalCount > 0 ? ` ${review.checkedCount}/${review.totalCount}` : '';

  if (review.status === 'approved') {
    return `검수완료${progressLabel}`;
  }

  if (review.status === 'changes-requested') {
    return `수정필요${progressLabel}`;
  }

  if (review.status === 'reviewing') {
    return `검수중${progressLabel}`;
  }

  return review.totalCount > 0 ? `검수대기 ${review.checkedCount}/${review.totalCount}` : '검수대기';
}

function resolvePlanHistorySuccessMessage(message: string | undefined, fallback: string) {
  if (!message) {
    return fallback;
  }

  if (NON_RETRY_NOTIFICATION_PATTERNS.some((pattern) => message.includes(pattern))) {
    return fallback;
  }

  return message;
}

function resolveProtectedText(text: string | null | undefined, masked: boolean) {
  if (!masked) {
    return text?.trim() ? text : '내용이 없습니다.';
  }

  return maskNotePreviewByWord(text ?? '');
}

function isCollapsibleDetailText(value: string) {
  const trimmedValue = value.trim();

  if (!trimmedValue) {
    return false;
  }

  return trimmedValue.length > COLLAPSIBLE_DETAIL_MAX_CHARS || trimmedValue.split(/\r?\n/).length > COLLAPSIBLE_DETAIL_MAX_LINES;
}

function ExpandableDetailText({
  text,
  type,
}: {
  text: string;
  type?: 'secondary' | 'success' | 'warning' | 'danger';
}) {
  const [expanded, setExpanded] = useState(false);
  const collapsible = isCollapsibleDetailText(text);

  return (
    <div className="plan-board-page__detail-text">
      <Paragraph
        className={`plan-board-page__detail-text-body${!expanded && collapsible ? ' plan-board-page__detail-text-body--collapsed' : ''}`}
        type={type}
      >
        {text}
      </Paragraph>
      {collapsible ? (
        <Button
          type="text"
          size="small"
          className="plan-board-page__detail-text-toggle"
          aria-label={expanded ? '접기' : '펼치기'}
          icon={expanded ? <UpOutlined /> : <DownOutlined />}
          onClick={() => {
            setExpanded((previous) => !previous);
          }}
        />
      ) : null}
    </div>
  );
}

function PlanNoteResourcePanel({ resources }: { resources: PlanNoteResource[] }) {
  const [hideInvalidResources, setHideInvalidResources] = useState(true);
  const visibleResources = useMemo(
    () => resources.filter((resource) => !hideInvalidResources || resource.isProbablyOpenable),
    [hideInvalidResources, resources],
  );
  const hiddenResourceCount = resources.length - visibleResources.length;

  return (
    <div className="plan-board-page__note-resources">
      <Flex justify="space-between" align="center" gap={8} wrap>
        <Space size={8} wrap>
          <Text strong>첨부 리소스</Text>
          <Text type="secondary">{visibleResources.length}건</Text>
          {hiddenResourceCount > 0 ? (
            <Text type="secondary" className="plan-board-page__note-resource-summary">
              숨김 {hiddenResourceCount}건
            </Text>
          ) : null}
        </Space>
        <Checkbox
          checked={hideInvalidResources}
          onChange={(event) => {
            setHideInvalidResources(event.target.checked);
          }}
        >
          열리지 않는 리소스 제외
        </Checkbox>
      </Flex>
      <div className="plan-board-page__note-resource-list">
        {visibleResources.map((resource) => (
          <div key={resource.id} className="plan-board-page__note-resource-card">
            <Flex justify="space-between" align="start" gap={12} wrap>
              <Space direction="vertical" size={2} style={{ minWidth: 0, flex: 1 }}>
                <Text strong ellipsis={{ tooltip: resource.label }}>
                  {resource.label}
                </Text>
                <Text type="secondary" className="plan-board-page__note-resource-path">
                  {resource.sourcePath}
                </Text>
              </Space>
              <Button
                size="small"
                type="primary"
                href={resource.publicUrl}
                target="_blank"
                rel="noreferrer"
              >
                열기
              </Button>
            </Flex>
            {resource.previewType === 'image' ? (
              <img
                className="plan-board-page__note-resource-image"
                src={resource.publicUrl}
                alt={resource.label}
                loading="lazy"
              />
            ) : null}
            {resource.previewType === 'document' ? (
              <iframe
                className="plan-board-page__note-resource-frame"
                src={resource.publicUrl}
                title={resource.label}
                loading="lazy"
                referrerPolicy="no-referrer"
              />
            ) : null}
          </div>
        ))}
      </div>
      {visibleResources.length === 0 ? (
        <Text type="secondary">표시할 수 있는 리소스가 없습니다.</Text>
      ) : null}
    </div>
  );
}

type ActionButton = {
  key: PlanActionType;
  label: string;
};

function createEmptyDraft(appConfig: AppConfig): PlanDraft {
  return {
    id: null,
    workId: '',
    note: '',
    automationType: 'none',
    automationContextIds: [],
    status: '등록',
    jangsingProcessingRequired: appConfig.planDefaults.jangsingProcessingRequired,
    autoDeployToMain: appConfig.planDefaults.autoDeployToMain,
    suppressWebPush: false,
    repeatRequestEnabled: false,
    repeatIntervalMinutes: 60,
  };
}

export type PlanBoardPageProps = {
  statusFilter: PlanFilterStatus;
  quickFilter?: PlanQuickFilter | null;
  quickFilterRequestKey?: number;
  initialSelectedPlanId?: number | null;
  initialSelectedWorkId?: string | null;
};

export function PlanBoardPage({
  statusFilter,
  quickFilter = null,
  quickFilterRequestKey = 0,
  initialSelectedPlanId = null,
  initialSelectedWorkId = null,
}: PlanBoardPageProps) {
  const navigate = useNavigate();
  const { hasAccess } = useTokenAccess();
  const { automationTypes } = useAutomationTypeRegistry();
  const { automationContexts } = useAutomationContextRegistry();
  const appConfig = useAppConfig();
  const autoRefreshIntervalMs = appConfig.automation.autoRefreshIntervalSeconds * 1000;
  const [messageApi, contextHolder] = message.useMessage();
  const [modalApi, modalContextHolder] = Modal.useModal();
  const screens = Grid.useBreakpoint();
  const [items, setItems] = useState<PlanItem[]>([]);
  const [reviewIndicatorsByPlanId, setReviewIndicatorsByPlanId] = useState<Record<number, ReviewListIndicator>>({});
  const [listRequestMeta, setListRequestMeta] = useState<PlanApiRequestMeta | null>(null);
  const [issueHistories, setIssueHistories] = useState<PlanIssueHistory[]>([]);
  const [actionHistories, setActionHistories] = useState<PlanActionHistory[]>([]);
  const [sourceWorkHistories, setSourceWorkHistories] = useState<PlanSourceWorkHistory[]>([]);
  const [selectedSourceWork, setSelectedSourceWork] = useState<PlanSourceWorkHistory | null>(null);
  const [draft, setDraft] = useState<PlanDraft>(() => createEmptyDraft(appConfig));
  const [noteInputValue, setNoteInputValue] = useState('');
  const [noteAttachmentUploading, setNoteAttachmentUploading] = useState(false);
  const [actionNote, setActionNote] = useState('');
  const [issueActionNote, setIssueActionNote] = useState('');
  const [resolveLatestIssue, setResolveLatestIssue] = useState(false);
  const [retryLatestIssue, setRetryLatestIssue] = useState(false);
  const [searchKeyword, setSearchKeyword] = useState('');
  const [workerStateFilter, setWorkerStateFilter] = useState<WorkerStateFilter>('all');
  const [releaseStateFilter, setReleaseStateFilter] = useState<ReleaseStateFilter>('all');
  const [mainStateFilter, setMainStateFilter] = useState<MainStateFilter>('all');
  const [issueStateFilter, setIssueStateFilter] = useState<IssueStateFilter>('all');
  const [costStateFilter, setCostStateFilter] = useState<CostStateFilter>('all');
  const [currentListPage, setCurrentListPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [jangsingProcessingSavingId, setJangsingProcessingSavingId] = useState<number | null>(null);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const [editorOpen, setEditorOpen] = useState(false);
  const [suppressAutoOpen, setSuppressAutoOpen] = useState(false);
  const [sourceViewerOpen, setSourceViewerOpen] = useState(false);
  const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(appConfig.automation.autoRefreshEnabled);
  const [autoRefreshRemainingMs, setAutoRefreshRemainingMs] = useState(autoRefreshIntervalMs);
  const [pendingSelection, setPendingSelection] = useState<{
    planId: number | null;
    workId: string | null;
  }>({
    planId: initialSelectedPlanId,
    workId: initialSelectedWorkId,
  });
  const draftRef = useRef(draft);
  const noteAttachmentInputRef = useRef<HTMLInputElement | null>(null);
  const noteAttachmentSessionIdRef = useRef(createPlanNoteAttachmentSessionId());
  const savingRef = useRef(saving);
  const previousWorkerStatusMapRef = useRef<Map<number, string | null>>(new Map());
  const notifiedAutomationStartKeysRef = useRef<Set<string>>(new Set());
  const notifiedAutomationFailureKeysRef = useRef<Set<string>>(new Set());
  const hasLoadedItemsRef = useRef(false);
  const hasPendingAutomation = useMemo(
    () => items.some((item) => shouldAutoRefreshAutomation(item.workerStatus)),
    [items],
  );
  const isAutoRefreshRunning = hasPendingAutomation && autoRefreshEnabled;
  const autoRefreshCountdownSeconds = Math.max(1, Math.ceil(autoRefreshRemainingMs / 1000));
  const noteResources = useMemo(
    () => (hasAccess ? extractPlanNoteResources(noteInputValue) : []),
    [hasAccess, noteInputValue],
  );

  draftRef.current = draft;
  savingRef.current = saving;

  useEffect(() => {
    setNoteInputValue(draft.note);
  }, [draft.id, draft.note]);

  function updateDraft(nextDraft: PlanDraft | ((previous: PlanDraft) => PlanDraft)) {
    setDraft((previous) => {
      const resolvedDraft = typeof nextDraft === 'function' ? nextDraft(previous) : nextDraft;
      draftRef.current = resolvedDraft;
      return resolvedDraft;
    });
  }

  async function notifyAutomationStarted(item: PlanItem) {
    if (!appConfig.automation.notifyOnAutomationStart) {
      return;
    }

    const presentation = getAutomationStatusPresentation(item);
    const body = `${formatPlanNotificationLabel(item)} 자동화 시작: ${presentation.description ?? presentation.label}`;

    messageApi.info(body);
  }

  async function notifyAutomationFailed(item: PlanItem) {
    if (!appConfig.automation.notifyOnAutomationFailure) {
      return;
    }

    messageApi.error(`${formatPlanNotificationLabel(item)} 자동화 실패`);
  }

  function isAutomationFailureStatus(status: PlanItem['workerStatus']) {
    return (
      status === '브랜치실패' ||
      status === '자동작업실패' ||
      status === 'release반영실패' ||
      status === 'main반영실패'
    );
  }

  function syncAutomationStartNotifications(nextItems: PlanItem[]) {
    const nextWorkerStatusMap = new Map<number, string | null>();

    nextItems.forEach((item) => {
      nextWorkerStatusMap.set(item.id, item.workerStatus);
    });

    if (!hasLoadedItemsRef.current) {
      previousWorkerStatusMapRef.current = nextWorkerStatusMap;
      hasLoadedItemsRef.current = true;
      return;
    }

    nextItems.forEach((item) => {
      const previousWorkerStatus = previousWorkerStatusMapRef.current.get(item.id) ?? null;
      const automationBecameActive =
        isAutomationInProgress(item.workerStatus) && !isAutomationInProgress(previousWorkerStatus);
      const notificationKey = `${item.id}:${item.startedAt ?? item.updatedAt}:${item.workerStatus ?? 'idle'}`;
      const failureBecameActive =
        isAutomationFailureStatus(item.workerStatus) && !isAutomationFailureStatus(previousWorkerStatus);
      const failureNotificationKey = `fail:${item.id}:${item.updatedAt}:${item.workerStatus ?? 'idle'}`;

      if (!automationBecameActive || notifiedAutomationStartKeysRef.current.has(notificationKey)) {
        if (!failureBecameActive || notifiedAutomationFailureKeysRef.current.has(failureNotificationKey)) {
          return;
        }
      }

      if (automationBecameActive && !notifiedAutomationStartKeysRef.current.has(notificationKey)) {
        notifiedAutomationStartKeysRef.current.add(notificationKey);
        void notifyAutomationStarted(item);
      }

      if (failureBecameActive && !notifiedAutomationFailureKeysRef.current.has(failureNotificationKey)) {
        notifiedAutomationFailureKeysRef.current.add(failureNotificationKey);
        void notifyAutomationFailed(item);
      }
    });

    previousWorkerStatusMapRef.current = nextWorkerStatusMap;
  }

  const filteredItems = useMemo(() => {
    const keyword = searchKeyword.trim().toLocaleLowerCase('ko-KR');

    return items
      .filter((item) => {
        if (quickFilter === 'release-pending-main') {
          return isReleasePendingMainItem(item);
        }

        if (quickFilter === 'automation-failed') {
          return isAutomationFailedItem(item);
        }

        if (quickFilter === 'working') {
          return isWorkingPlanItem(item);
        }

        if (statusFilter === 'all') {
          return true;
        }

        if (statusFilter === 'in-progress') {
          return !isReleaseCompletedPlanItem(item);
        }

        if (statusFilter === 'done') {
          return isReleaseCompletedPlanItem(item);
        }

        return item.hasOpenIssues && !isReleaseCompletedPlanItem(item);
      })
      .filter((item) => matchesWorkerStateFilter(item, workerStateFilter))
      .filter((item) => matchesReleaseStateFilter(item, releaseStateFilter))
      .filter((item) => matchesMainStateFilter(item, mainStateFilter))
      .filter((item) => matchesIssueStateFilter(item, issueStateFilter))
      .filter((item) =>
        matchesCostStateFilter(item, costStateFilter, buildAutomationUsageSummaryFromSnapshot(item.usageSnapshot, appConfig)),
      )
      .filter((item) => {
        if (!keyword) {
          return true;
        }

        const searchableText = [
          item.workId,
          item.note,
          item.status,
          item.assignedBranch,
          item.releaseTarget,
          item.workerStatus,
          ...(item.issueTags ?? []),
        ]
          .filter(Boolean)
          .join(' ')
          .toLocaleLowerCase('ko-KR');

        return searchableText.includes(keyword);
      })
      .sort((left, right) => {
        if (statusFilter === 'in-progress') {
          const automationPriorityDiff = comparePlanItemsByAutomationPriority(left, right);

          if (automationPriorityDiff !== 0) {
            return automationPriorityDiff;
          }
        }

        return comparePlanItemsByRecent(left, right);
      });
  }, [
    items,
    costStateFilter,
    issueStateFilter,
    mainStateFilter,
    quickFilter,
    releaseStateFilter,
    searchKeyword,
    statusFilter,
    workerStateFilter,
  ]);

  const selectedItem = useMemo(
    () => items.find((item) => item.id === draft.id) ?? null,
    [draft.id, items],
  );
  const selectedReleaseReviewNote = typeof selectedItem?.releaseReviewNote === 'string' ? selectedItem.releaseReviewNote : '';

  const actionButtons = useMemo<ActionButton[]>(() => {
    if (!selectedItem) {
      return [];
    }

    const sourceWorkCount = Number(selectedItem.usageSnapshot?.sourceWorkCount ?? 0);
    const hasSourceWork = Number.isFinite(sourceWorkCount) && sourceWorkCount > 0;
    const needsWorkRetryBecauseBranchMissing =
      (selectedItem.status === '작업완료' || selectedItem.status === '릴리즈완료') &&
      (selectedItem.workerStatus === 'release반영실패' || selectedItem.workerStatus === 'main반영실패') &&
      Boolean(selectedItem.lastError?.includes('브랜치를 찾을 수 없습니다'));
    const canRetryWork =
      (selectedItem.status === '작업중' &&
        (selectedItem.workerStatus === '자동작업실패' || Boolean(selectedItem.lastError))) ||
      needsWorkRetryBecauseBranchMissing;
    const canRetryReleaseMerge =
      selectedItem.status === '작업완료' &&
      selectedItem.workerStatus === 'release반영실패' &&
      !needsWorkRetryBecauseBranchMissing;

    const buttons: ActionButton[] = [];

    if (selectedItem.workerStatus === '브랜치실패') {
      buttons.push({ key: 'retry-branch', label: '브랜치 재시도' });
    }

    if (selectedItem.status === '등록' && selectedItem.workerStatus !== '브랜치생성중') {
      buttons.push({ key: 'start-work', label: '작업시작' });
    }

    if (canRetryWork) {
      buttons.push({ key: 'retry-work', label: '작업 재처리' });
    }

    if (selectedItem.status === '작업중') {
      buttons.push({ key: 'complete-development', label: '작업완료 처리' });
    }

    if (canRetryReleaseMerge) {
      buttons.push({ key: 'retry-merge', label: 'release 반영 재시도' });
    }

    if (
      selectedItem.status === '릴리즈완료' ||
      (selectedItem.status === '작업완료' && selectedItem.workerStatus === 'release반영실패') ||
      (selectedItem.workerStatus === 'main반영실패' && !hasSourceWork)
    ) {
      buttons.push({ key: 'cancel-release', label: '취소완료 처리' });
    }

    buttons.push({
      key: 'request-main-merge',
      label:
        selectedItem.workerStatus === 'main반영실패'
          ? 'main 일괄 반영 재시도'
          : 'main 일괄 반영 요청',
    });

    return buttons;
  }, [selectedItem]);

  const combinedHistories = useMemo(() => {
    return [
      ...actionHistories.map((history) => ({
        id: `action-${history.id}`,
        createdAt: history.createdAt,
        kind: 'action' as const,
        action: history,
      })),
      ...issueHistories.map((history) => ({
        id: `issue-${history.id}`,
        createdAt: history.createdAt,
        kind: 'issue' as const,
        issue: history,
      })),
    ].sort((left, right) => {
      const timeDiff = new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime();

      if (timeDiff !== 0) {
        return timeDiff;
      }

      return left.id.localeCompare(right.id);
    });
  }, [actionHistories, issueHistories]);

  useEffect(() => {
    void loadItems(statusFilter);
  }, [hasAccess, statusFilter]);

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

    setSearchKeyword('');
    setCurrentListPage(1);
  }, [quickFilter, quickFilterRequestKey]);

  useEffect(() => {
    setCurrentListPage(1);
  }, [costStateFilter, issueStateFilter, mainStateFilter, releaseStateFilter, searchKeyword, statusFilter, workerStateFilter]);

  useEffect(() => {
    const totalPages = Math.max(1, Math.ceil(filteredItems.length / PLAN_LIST_PAGE_SIZE));

    setCurrentListPage((previous) => Math.min(previous, totalPages));
  }, [filteredItems.length]);

  useEffect(() => {
    if (!hasAccess) {
      setEditorOpen(false);
      setSourceViewerOpen(false);
      setSelectedSourceWork(null);
    }
  }, [hasAccess]);

  useEffect(() => {
    setPendingSelection({
      planId: initialSelectedPlanId,
      workId: initialSelectedWorkId,
    });
  }, [initialSelectedPlanId, initialSelectedWorkId]);

  useEffect(() => {
    setAutoRefreshEnabled(appConfig.automation.autoRefreshEnabled);
    setAutoRefreshRemainingMs(autoRefreshIntervalMs);
  }, [appConfig.automation.autoRefreshEnabled, autoRefreshIntervalMs]);

  useEffect(() => {
    if (!hasPendingAutomation || !autoRefreshEnabled) {
      setAutoRefreshRemainingMs(autoRefreshIntervalMs);
      return;
    }

    let cancelled = false;
    const startedAt = Date.now();

    setAutoRefreshRemainingMs(autoRefreshIntervalMs);

    const countdownTimer = window.setInterval(() => {
      const elapsed = Date.now() - startedAt;
      const remaining = Math.max(autoRefreshIntervalMs - elapsed, 0);
      setAutoRefreshRemainingMs(remaining);
    }, 1000);

    const refreshTimer = window.setTimeout(() => {
      if (cancelled || savingRef.current) {
        return;
      }

      setAutoRefreshRemainingMs(autoRefreshIntervalMs);
      void loadItems(statusFilter);
    }, autoRefreshIntervalMs);

    return () => {
      cancelled = true;
      window.clearInterval(countdownTimer);
      window.clearTimeout(refreshTimer);
    };
  }, [autoRefreshEnabled, autoRefreshIntervalMs, hasPendingAutomation, items, statusFilter]);

  useEffect(() => {
    if (!draft.id) {
      setIssueHistories([]);
      setActionHistories([]);
      setSourceWorkHistories([]);
      setActionNote('');
      setIssueActionNote('');
      setResolveLatestIssue(false);
      setRetryLatestIssue(false);
      return;
    }

    if (!hasAccess) {
      setIssueHistories([]);
      setActionHistories([]);
      setSourceWorkHistories([]);
      setActionNote('');
      setIssueActionNote('');
      setResolveLatestIssue(false);
      setRetryLatestIssue(false);
      return;
    }

    void Promise.all([
      loadIssueHistories(draft.id),
      loadActionHistories(draft.id),
      loadSourceWorkHistories(draft.id),
    ]);
  }, [draft.id, hasAccess]);

  useEffect(() => {
    if (pendingSelection.planId || pendingSelection.workId) {
      const matchedItem = items.find((item) => {
        if (pendingSelection.planId && item.id === pendingSelection.planId) {
          return true;
        }

        if (pendingSelection.workId && item.workId === pendingSelection.workId) {
          return true;
        }

        return false;
      });

      if (matchedItem) {
        if (draft.id !== matchedItem.id || !editorOpen) {
          handleSelectItem(matchedItem);
        }

        setPendingSelection({ planId: null, workId: null });
      }

      return;
    }

    if (suppressAutoOpen || searchKeyword.trim() || filteredItems.length === 0 || editorOpen) {
      return;
    }
  }, [
    draft.id,
    editorOpen,
    filteredItems,
    items,
    pendingSelection.planId,
    pendingSelection.workId,
    searchKeyword,
    suppressAutoOpen,
    hasAccess,
  ]);

  async function loadItems(filter: PlanFilterStatus) {
    setLoading(true);
    setErrorMessage(null);

    try {
      const {
        items: nextItems,
        meta,
      } = await fetchPlanItemsWithLatestSourceWorks(filter);
      let nextReleaseReviews: PlanReleaseReviewBoardItem[] = [];

      try {
        nextReleaseReviews = await fetchReleaseReviewBoardItems();
      } catch {
        nextReleaseReviews = [];
      }

      syncAutomationStartNotifications(nextItems);
      setItems(nextItems);
      setListRequestMeta(meta);
      setReviewIndicatorsByPlanId(
        Object.fromEntries(
          nextReleaseReviews.map((item) => {
            const targetIds = item.review.metadata.pageSelectionIds ?? [];
            const checkedIds = item.review.metadata.checkedPageSelectionIds ?? [];

            return [
              item.planItem.id,
              {
                status: item.review.status,
                checkedCount: checkedIds.filter((selectionId) => targetIds.includes(selectionId)).length,
                totalCount: targetIds.length,
              } satisfies ReviewListIndicator,
            ];
          }),
        ),
      );
      if (draft.id) {
        const nextSelectedItem = nextItems.find((item) => item.id === draft.id);
        const currentSelectedItem = items.find((item) => item.id === draft.id);
        const shouldPreserveEditableDraft = editorOpen && !isPlanItemRequestLocked(currentSelectedItem);

        if (nextSelectedItem) {
          if (!shouldPreserveEditableDraft) {
            setDraft(toDraft(nextSelectedItem));
          }
        } else {
          setDraft(createEmptyDraft(appConfig));
          setEditorOpen(false);
          setSourceViewerOpen(false);
        }
      }
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '자동화 목록을 불러오지 못했습니다.');
    } finally {
      setLoading(false);
    }
  }

  async function loadIssueHistories(planItemId: number) {
    try {
      setIssueHistories(await fetchPlanIssueHistories(planItemId));
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '이슈 이력을 불러오지 못했습니다.');
      setIssueHistories([]);
    }
  }

  async function loadActionHistories(planItemId: number) {
    try {
      setActionHistories(await fetchPlanActionHistories(planItemId));
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '조치 이력을 불러오지 못했습니다.');
      setActionHistories([]);
    }
  }

  async function loadSourceWorkHistories(planItemId: number) {
    try {
      setSourceWorkHistories(await fetchPlanSourceWorkHistories(planItemId));
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '소스 작업 이력을 불러오지 못했습니다.');
      setSourceWorkHistories([]);
    }
  }

  function handleCreateNew() {
    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    noteAttachmentSessionIdRef.current = createPlanNoteAttachmentSessionId();
    setDraft({
      ...createEmptyDraft(appConfig),
      automationContextIds: resolveDefaultAutomationContextIds(automationContexts),
    });
    setResolveLatestIssue(false);
    setRetryLatestIssue(true);
    setEditorOpen(true);
    setSuppressAutoOpen(false);
    setSourceViewerOpen(false);
  }

  function handleSelectItem(item: PlanItem) {
    noteAttachmentSessionIdRef.current = resolvePlanNoteAttachmentSessionId(item.id, noteAttachmentSessionIdRef.current);
    setDraft(toDraft(item));
    setResolveLatestIssue(false);
    setRetryLatestIssue(true);
    setEditorOpen(true);
    setSuppressAutoOpen(false);
    setSourceViewerOpen(false);
  }

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

    const params = new URLSearchParams(window.location.search);
    let changed = false;

    if (params.has('planId')) {
      params.delete('planId');
      changed = true;
    }

    if (params.has('workId')) {
      params.delete('workId');
      changed = true;
    }

    if (changed) {
      const nextSearch = params.toString();
      const nextUrl = `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ''}`;
      window.history.replaceState(window.history.state, '', nextUrl);
    }
  }

  function closeEditor() {
    setEditorOpen(false);
    setSourceViewerOpen(false);
    setSelectedSourceWork(null);
    setSuppressAutoOpen(true);
    clearNotificationSelectionParams();
    if (!draft.id) {
      setDraft(createEmptyDraft(appConfig));
    }
  }

  async function handleOpenSourceWork(sourceWorkId: number) {
    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    if (!draft.id) {
      return;
    }

    try {
      const item = await fetchPlanSourceWorkHistory(draft.id, sourceWorkId);
      setSelectedSourceWork(item);
      setSourceViewerOpen(true);
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '소스 작업 상세를 불러오지 못했습니다.');
    }
  }

  async function handleSetup() {
    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    try {
      await setupPlanBoard();
      messageApi.success('작업 테이블을 생성했습니다.');
      await loadItems(statusFilter);
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '테이블 생성에 실패했습니다.');
    }
  }

  async function handleSave() {
    if (savingRef.current) {
      return;
    }

    const nextDraft = {
      ...draftRef.current,
      note: noteInputValue,
    };

    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    if (isPlanItemRequestLocked(selectedItem)) {
      messageApi.warning('자동화 접수된 항목은 수정할 수 없습니다.');
      return;
    }

    savingRef.current = true;
    setSaving(true);

    try {
      const isNewItem = !nextDraft.id;
      const savedItem =
        nextDraft.id && selectedItem && isFunctionCheckEditableStatus(selectedItem.status)
          ? await updatePlanItemJangsingProcessingRequired(nextDraft.id, nextDraft.jangsingProcessingRequired)
          : nextDraft.id
            ? await updatePlanItem(nextDraft)
            : await createPlanItem(nextDraft);
      messageApi.success(
        nextDraft.id
          ? isFunctionCheckEditableStatus(savedItem.status)
            ? '기능동작확인을 수정했습니다.'
            : '작업 항목을 수정했습니다.'
          : '작업 항목을 등록했습니다.',
      );
      await loadItems(statusFilter);
      updateDraft(toDraft(savedItem));
      setEditorOpen(!isNewItem || appConfig.planDefaults.openEditorAfterCreate);
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '저장에 실패했습니다.');
    } finally {
      savingRef.current = false;
      setSaving(false);
    }
  }

  function handleNoteChange(note: string) {
    setNoteInputValue(note);
    draftRef.current = {
      ...draftRef.current,
      note,
    };
  }

  async function handleNoteAttachmentFilesPicked(files: File[]) {
    if (files.length === 0 || noteAttachmentUploading) {
      return;
    }

    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    if (isPlanItemRequestLocked(selectedItem)) {
      messageApi.warning('자동화 접수된 항목은 첨부 파일을 추가할 수 없습니다.');
      return;
    }

    setNoteAttachmentUploading(true);

    try {
      const sessionId = resolvePlanNoteAttachmentSessionId(draftRef.current.id, noteAttachmentSessionIdRef.current);
      const uploadResults = await Promise.allSettled(files.map((file) => uploadChatComposerFile(sessionId, file)));
      const uploadedItems: ChatComposerAttachment[] = [];
      const failedFileNames: string[] = [];

      uploadResults.forEach((result, index) => {
        if (result.status === 'fulfilled') {
          uploadedItems.push(result.value);
          return;
        }

        failedFileNames.push(files[index]?.name || `파일 ${index + 1}`);
      });

      if (uploadedItems.length > 0) {
        const nextNote = appendPlanNoteAttachments(noteInputValue, uploadedItems);
        handleNoteChange(nextNote);
        messageApi.success(`첨부 파일 ${uploadedItems.length}건을 메모에 추가했습니다.`);
      }

      if (failedFileNames.length > 0) {
        messageApi.error(`업로드 실패: ${failedFileNames.join(', ')}`);
      }
    } finally {
      setNoteAttachmentUploading(false);
    }
  }

  function handleNoteAttachmentInputChange(event: ChangeEvent<HTMLInputElement>) {
    const files = Array.from(event.target.files ?? []);
    event.target.value = '';
    void handleNoteAttachmentFilesPicked(files);
  }

  async function handleDelete() {
    if (savingRef.current || !draftRef.current.id) {
      return;
    }

    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    if (isPlanItemRequestLocked(selectedItem)) {
      messageApi.warning('자동화 접수된 항목은 삭제할 수 없습니다.');
      return;
    }

    const confirmed = await confirmWithKeyboard(modalApi, {
      title: '선택한 작업 메모를 삭제할까요?',
      okText: '삭제',
      cancelText: '취소',
      okButtonProps: { danger: true },
    });

    if (!confirmed) {
      return;
    }

    setSaving(true);

    try {
      await deletePlanItem(draftRef.current.id);
      messageApi.success('작업 항목을 삭제했습니다.');
      updateDraft(createEmptyDraft(appConfig));
      setEditorOpen(false);
      await loadItems(statusFilter);
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '삭제에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  }

  async function handleAction(action: PlanActionType) {
    if (savingRef.current || !draftRef.current.id) {
      return;
    }

    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    setSaving(true);

    try {
      const response = await runPlanAction(draftRef.current.id, action);
      messageApi.success(response.message ?? getActionSuccessMessage(action));
      await loadItems(statusFilter);
      updateDraft(toDraft(response.item));
      await Promise.all([
        loadActionHistories(response.item.id),
        loadIssueHistories(response.item.id),
        loadSourceWorkHistories(response.item.id),
      ]);
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '작업 요청에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  }

  async function handleIssueAction() {
    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    if (savingRef.current || !draftRef.current.id || !issueActionNote.trim()) {
      messageApi.warning('조치 내용을 입력해 주세요.');
      return;
    }

    setSaving(true);

    try {
      const response = await appendPlanIssueAction(
        draftRef.current.id,
        issueActionNote,
        resolveLatestIssue,
        retryLatestIssue,
      );
      messageApi.success(resolvePlanHistorySuccessMessage(
        response.message,
        resolveLatestIssue ? '이슈 조치 및 해결 기록을 남겼습니다.' : '이슈 조치 기록을 남겼습니다.',
      ));
      setIssueActionNote('');
      setResolveLatestIssue(false);
      setRetryLatestIssue(false);
      await Promise.all([
        loadIssueHistories(draftRef.current.id),
        loadActionHistories(draftRef.current.id),
        loadItems(statusFilter),
      ]);
      if (response.planItem) {
        updateDraft(toDraft(response.planItem));
      }
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '이슈 조치 기록 저장에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  }

  async function handleActionNote() {
    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    if (savingRef.current || !draftRef.current.id || !actionNote.trim()) {
      messageApi.warning('추가 조치 내용을 입력해 주세요.');
      return;
    }

    setSaving(true);

    try {
      const response = await appendPlanActionHistory(draftRef.current.id, actionNote);
      messageApi.success(resolvePlanHistorySuccessMessage(response.message, '조치 이력을 추가했습니다.'));
      setActionNote('');
      await Promise.all([
        loadItems(statusFilter),
        loadActionHistories(draftRef.current.id),
        loadIssueHistories(draftRef.current.id),
      ]);
      if (response.planItem) {
        updateDraft(toDraft(response.planItem));
      }
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '조치 이력 저장에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  }

  async function handleCopyText(text: string) {
    try {
      await navigator.clipboard.writeText(text);
      messageApi.success('복사했습니다.');
    } catch {
      messageApi.error('복사에 실패했습니다.');
    }
  }

  async function handleJangsingProcessingChange(id: number, jangsingProcessingRequired: boolean) {
    if (jangsingProcessingSavingId === id) {
      return;
    }

    if (!hasAccess) {
      messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
      return;
    }

    const currentItem = items.find((item) => item.id === id);

    if (!currentItem || currentItem.jangsingProcessingRequired === jangsingProcessingRequired) {
      return;
    }

    if (isPlanItemRequestLocked(currentItem)) {
      messageApi.warning('자동화 접수된 항목은 기능동작확인을 수정할 수 없습니다.');
      return;
    }

    if (!isFunctionCheckEditableStatus(currentItem.status)) {
      messageApi.warning('기능동작확인은 작업완료 건만 수정할 수 있습니다.');
      return;
    }

    setJangsingProcessingSavingId(id);

    try {
      const updatedItem = await updatePlanItemJangsingProcessingRequired(id, jangsingProcessingRequired);
      setItems((previous) =>
        previous.map((item) => (item.id === id ? updatedItem : item)).sort(comparePlanItemsByRecent),
      );

      if (draftRef.current.id === id) {
        updateDraft(toDraft(updatedItem));
      }

      messageApi.success(`기능동작확인을 ${getFunctionCheckLabel(jangsingProcessingRequired)}로 변경했습니다.`);
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '기능동작확인 저장에 실패했습니다.');
    } finally {
      setJangsingProcessingSavingId(null);
    }
  }

  function handleAutoRefreshToggle() {
    setAutoRefreshEnabled((previous) => {
      const next = !previous;
      messageApi.success(next ? '자동조회를 다시 시작합니다.' : '자동조회를 중지했습니다.');
      return next;
    });
    setAutoRefreshRemainingMs(autoRefreshIntervalMs);
  }

  const hasIssueHistory = issueHistories.length > 0;
  const currentDraftItem = draft.id ? items.find((item) => item.id === draft.id) ?? selectedItem : selectedItem;
  const requestReceived = isPlanItemRequestLocked(currentDraftItem);
  const isRequestLocked = !hasAccess || requestReceived;
  const isRestrictedClient = !hasAccess;
  const isCreating = !draft.id;
  const canEditFunctionCheck = Boolean(
    hasAccess && selectedItem && !isRequestLocked && isFunctionCheckEditableStatus(selectedItem.status),
  );
  const canSave = hasAccess && !isRequestLocked;
  const automationTypeOptions = useMemo(
    () => buildAutomationTypeOptions(automationTypes, draft.automationType),
    [automationTypes, draft.automationType],
  );
  const automationContextOptions = useMemo(
    () => buildAutomationContextOptions(automationContexts, draft.automationContextIds),
    [automationContexts, draft.automationContextIds],
  );
  const automationTypeLabel = useMemo(
    () => resolveAutomationTypeLabel(automationTypes, draft.automationType),
    [automationTypes, draft.automationType],
  );
  const latestActionHistory = actionHistories[0] ?? null;
  const latestIssueHistory = issueHistories[0] ?? null;
  const releaseCompletedTimestamps = useMemo(
    () =>
      actionHistories
        .filter((history) => history.actionType === 'release반영완료')
        .slice()
        .sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime())
        .map((history) => history.createdAt),
    [actionHistories],
  );
  const latestReleaseCompletedAt = releaseCompletedTimestamps[0] ?? null;
  const previousReleaseCompletedAt = releaseCompletedTimestamps[1] ?? null;
  const sourceWorkHistoriesAfterLatestRelease = useMemo(
    () => sourceWorkHistories.filter((history) => isHistoryAfterReference(history.createdAt, latestReleaseCompletedAt)),
    [latestReleaseCompletedAt, sourceWorkHistories],
  );
  const sourceWorkHistoriesForLatestReleasedCycle = useMemo(() => {
    if (!latestReleaseCompletedAt) {
      return [];
    }

    return sourceWorkHistories.filter((history) => {
      if (!history.createdAt) {
        return false;
      }

      const createdAt = new Date(history.createdAt).getTime();
      const upperBound = new Date(latestReleaseCompletedAt).getTime();

      if (Number.isNaN(createdAt) || Number.isNaN(upperBound) || createdAt > upperBound) {
        return false;
      }

      if (!previousReleaseCompletedAt) {
        return true;
      }

      return createdAt > new Date(previousReleaseCompletedAt).getTime();
    });
  }, [latestReleaseCompletedAt, previousReleaseCompletedAt, sourceWorkHistories]);
  const shouldShowCompletedCycleUsage =
    isMainCompletedPlanItem(selectedItem) &&
    sourceWorkHistoriesAfterLatestRelease.length === 0 &&
    sourceWorkHistoriesForLatestReleasedCycle.length > 0;
  const currentReleaseSourceWorkHistories = shouldShowCompletedCycleUsage
    ? sourceWorkHistoriesForLatestReleasedCycle
    : sourceWorkHistoriesAfterLatestRelease;
  const currentReleaseUsagePrefix = shouldShowCompletedCycleUsage
    ? '최근 완료 배포 기준'
    : latestReleaseCompletedAt
      ? '이번 반영 배포 이후'
      : '현재 누적 기준';
  const currentReleaseUsageSummary = useMemo(
    () => buildAutomationUsageSummary(currentReleaseSourceWorkHistories, appConfig),
    [appConfig, currentReleaseSourceWorkHistories],
  );
  const currentReleaseUsageSummaryByHistoryId = useMemo(
    () => buildAutomationUsageSummaryByHistoryId(currentReleaseSourceWorkHistories, appConfig),
    [appConfig, currentReleaseSourceWorkHistories],
  );
  const usageSummaryByPlanId = useMemo(
    () =>
      Object.fromEntries(
        items.flatMap((item) => {
          const summary = buildAutomationUsageSummaryFromSnapshot(item.usageSnapshot, appConfig);
          return summary ? [[item.id, summary]] : [];
        }),
      ) as Record<number, AutomationUsageSummary>,
    [appConfig, items],
  );
  const currentReleaseSourceWorkHistoryIdSet = useMemo(
    () => new Set(currentReleaseSourceWorkHistories.map((history) => history.id)),
    [currentReleaseSourceWorkHistories],
  );
  const selectedSourceWorkUsageSummary =
    selectedSourceWork && currentReleaseSourceWorkHistoryIdSet.has(selectedSourceWork.id)
      ? currentReleaseUsageSummaryByHistoryId.get(selectedSourceWork.id) ?? null
      : null;
  const memoRows = screens.md ? 18 : 9;
  const isMobileAutomationLayout = !screens.md;
  const overviewActionContent = (
    <Space wrap>
      <Space size={8} className="plan-board-page__auto-refresh-control">
        <LongPressButton
          onClick={() => void loadItems(statusFilter)}
          onLongPress={() => {
            if (!hasAccess) {
              messageApi.error(TOKEN_ACCESS_REQUIRED_MESSAGE);
              return;
            }

            handleAutoRefreshToggle();
          }}
          longPressMs={AUTO_REFRESH_LONG_PRESS_MS}
          loading={loading}
          title="길게 눌러 자동조회 On/Off"
          className={`plan-board-page__auto-refresh-button${
            isAutoRefreshRunning ? ' plan-board-page__auto-refresh-button--active' : ''
          }`}
        >
          조회
        </LongPressButton>
        {isAutoRefreshRunning ? (
          <Text className="plan-board-page__auto-refresh-countdown">
            자동 조회까지 {autoRefreshCountdownSeconds}초
          </Text>
        ) : null}
      </Space>
      {listRequestMeta ? (
        <Text type="secondary">
          최근 조회 {formatResponseBytes(listRequestMeta.responseBytes)} · {listRequestMeta.durationMs}ms
        </Text>
      ) : null}
      <Button onClick={handleCreateNew} disabled={isRestrictedClient}>
        새 메모
      </Button>
      <Button onClick={() => void handleSetup()} disabled={isRestrictedClient}>
        테이블 생성
      </Button>
    </Space>
  );
  return (
    <div className="plan-board-page">
      {contextHolder}
      {modalContextHolder}

      {isMobileAutomationLayout ? null : (
        <Card className="plan-board-page__overview" bordered={false}>
          <Flex justify="space-between" align="center" gap={12} wrap>
            <div>
              <Title level={4}>자동화</Title>
              <Paragraph className="plan-board-page__intro">
                작업 메모와 이력, 증적을 확인하고 필요한 내용을 수동으로 정리합니다.
              </Paragraph>
            </div>

            {overviewActionContent}
          </Flex>
        </Card>
      )}

      {isRestrictedClient ? (
        <Alert
          showIcon
          type="warning"
          className="plan-board-page__alert"
          message="권한 토큰이 없어 자동화 조회만 사용할 수 있습니다."
          description={TOKEN_ACCESS_REQUIRED_MESSAGE}
        />
      ) : null}

      {loading ? (
        <Alert
          showIcon
          type="info"
          className="plan-board-page__alert"
          message="자동화 현황 데이터를 불러오는 중입니다."
          description={
            <Space size={8}>
              <Spin size="small" />
              <span>서버 설정과 연결 상태를 확인하고 있습니다.</span>
            </Space>
          }
        />
      ) : null}

      {errorMessage ? (
        <Alert
          showIcon
          type="warning"
          className="plan-board-page__alert"
          message="자동화 현황 메뉴를 아직 사용할 수 없습니다."
          description={<ExpandableDetailText text={errorMessage} />}
          action={
            <Button
              type="text"
              size="small"
              aria-label="오류 메시지 복사"
              icon={<CopyOutlined />}
              disabled={!hasAccess}
              onClick={() => void handleCopyText(errorMessage)}
            />
          }
        />
      ) : null}

      <PlanListDetailLayout
        listTitle={`자동화 목록 / ${getPlanQuickFilterLabel(quickFilter) ?? getPlanFilterLabel(statusFilter)}`}
        listExtra={<Text code>{filteredItems.length} items</Text>}
        listContent={
          <div className="plan-board-page__list-panel">
            <div className="plan-board-page__list-controls">
              {isMobileAutomationLayout ? (
                <div className="plan-board-page__mobile-overview">
                  <Flex vertical gap={10}>
                    <div>
                      <Text strong className="plan-board-page__mobile-overview-title">
                        자동화 목록
                      </Text>
                      <Paragraph className="plan-board-page__mobile-overview-description">
                        작업 메모와 이력, 증적을 한 화면 흐름에서 바로 확인합니다.
                      </Paragraph>
                    </div>
                    {overviewActionContent}
                  </Flex>
                </div>
              ) : null}
              {quickFilter ? (
                <Alert
                  showIcon
                  type={quickFilter === 'automation-failed' ? 'error' : 'info'}
                  className="plan-board-page__alert"
                  message={`${getPlanQuickFilterLabel(quickFilter)} 목록을 표시합니다.`}
                  description={
                    quickFilter === 'automation-failed'
                      ? '브랜치 생성, 작업, release/main 반영 단계에서 실패한 항목만 추렸습니다.'
                      : quickFilter === 'working'
                        ? '현재 상태가 작업중인 항목만 추렸습니다.'
                        : 'release 반영은 끝났지만 main 반영이 남아 있거나 실패한 항목만 추렸습니다.'
                  }
                />
              ) : null}
              {selectedItem?.lastError ? (
                <Alert
                  showIcon
                  type="error"
                  className="plan-board-page__alert"
                  message="현재 선택된 자동화 항목에 오류가 있습니다."
                  description={
                    <ExpandableDetailText text={resolveProtectedText(selectedItem.lastError, !hasAccess)} type="danger" />
                  }
                  action={
                    <Button
                      type="text"
                      size="small"
                      aria-label="오류 메시지 복사"
                      icon={<CopyOutlined />}
                      disabled={!hasAccess}
                      onClick={() => void handleCopyText(selectedItem.lastError ?? '')}
                    />
                  }
                />
              ) : null}

              <Input.Search
                allowClear
                value={searchKeyword}
                placeholder="작업 ID, 메모 내용, 브랜치, 이슈 태그 검색"
                disabled={isRestrictedClient}
                onChange={(event) => {
                  setSearchKeyword(event.target.value);
                }}
              />
              <Flex gap={8} wrap className="plan-board-page__list-filter-bar">
                <Select
                  size="small"
                  value={workerStateFilter}
                  options={WORKER_STATE_FILTER_OPTIONS}
                  disabled={isRestrictedClient}
                  onChange={setWorkerStateFilter}
                />
                <Select
                  size="small"
                  value={releaseStateFilter}
                  options={RELEASE_STATE_FILTER_OPTIONS}
                  disabled={isRestrictedClient}
                  onChange={setReleaseStateFilter}
                />
                <Select
                  size="small"
                  value={mainStateFilter}
                  options={MAIN_STATE_FILTER_OPTIONS}
                  disabled={isRestrictedClient}
                  onChange={setMainStateFilter}
                />
                <Select
                  size="small"
                  value={issueStateFilter}
                  options={ISSUE_STATE_FILTER_OPTIONS}
                  disabled={isRestrictedClient}
                  onChange={setIssueStateFilter}
                />
                <Select
                  size="small"
                  value={costStateFilter}
                  options={COST_STATE_FILTER_OPTIONS}
                  disabled={isRestrictedClient}
                  onChange={setCostStateFilter}
                />
              </Flex>
            </div>

            <div className="plan-board-page__list-scroller">
              <PlanItemList
                activeDraftId={draft.id}
                currentPage={currentListPage}
                editorOpen={editorOpen}
                hasAccess={hasAccess}
                items={filteredItems}
                jangsingProcessingSavingId={jangsingProcessingSavingId}
                reviewIndicatorsByPlanId={reviewIndicatorsByPlanId}
                searchKeyword={searchKeyword}
                usageSummaryByPlanId={usageSummaryByPlanId}
                onChangePage={setCurrentListPage}
                onChangeJangsingProcessing={handleJangsingProcessingChange}
                onSelectItem={handleSelectItem}
              />
            </div>
          </div>
        }
        desktopDetailOpen={editorOpen}
        mobileDetailOpen={editorOpen}
        mobileLayoutMode="detail-only"
        detailTitle={draft.id ? `상세 #${draft.id}` : '새 메모'}
        detailActions={
          <>
            {hasAccess && draft.id ? (
              <Button danger onClick={() => void handleDelete()} loading={saving} disabled={!canSave}>
                삭제
              </Button>
            ) : null}
            {hasAccess && !sourceViewerOpen ? (
              <Button
                type="primary"
                onClick={() => void handleSave()}
                loading={saving}
                disabled={Boolean(draft.id && !canSave)}
              >
                저장
              </Button>
            ) : null}
          </>
        }
        onCloseDetail={closeEditor}
        showDesktopClose
        emptyDetailTitle="상세 보기"
        detailContent={
          !sourceViewerOpen ? (
            <div className="plan-board-page__detail-panel">
              {selectedItem ? (
                <Alert
                  type={selectedItem.hasOpenIssues ? 'warning' : 'info'}
                  showIcon
                  message="작업 정보"
                  description={
                    <Space direction="vertical" size={4}>
                      <Text>상태: {selectedItem.status}</Text>
                      <Text>등록일: {formatPlanDateTime(selectedItem.createdAt)}</Text>
                      <Text>접수 시각: {formatPlanDateTime(selectedItem.startedAt)}</Text>
                      <Text>작업완료 시각: {formatPlanDateTime(selectedItem.completedAt)}</Text>
                      <Text>최종 반영 시각: {formatPlanDateTime(selectedItem.mergedAt)}</Text>
                      <Text>최근 수정: {formatPlanDateTime(selectedItem.updatedAt)}</Text>
                      <Text>브랜치: {selectedItem.assignedBranch ?? '-'}</Text>
                      <AutomationStatusBar item={selectedItem} usageSummary={currentReleaseUsageSummary} />
                      {currentReleaseUsageSummary ? (
                        <Space size={[8, 4]} wrap>
                          <Text>
                            {currentReleaseUsagePrefix} 소스 작업: {currentReleaseSourceWorkHistories.length}건
                          </Text>
                          <Text>{currentReleaseUsageSummary.costLabel}</Text>
                          <Text>{currentReleaseUsageSummary.tokenLabel}</Text>
                          {currentReleaseUsageSummary?.tierColor && currentReleaseUsageSummary.tierLabel ? (
                            <Tag color={currentReleaseUsageSummary.tierColor}>{currentReleaseUsageSummary.tierLabel}</Tag>
                          ) : null}
                          {currentReleaseUsageSummary.description ? (
                            <Text type="secondary">{currentReleaseUsageSummary.description}</Text>
                          ) : null}
                        </Space>
                      ) : null}
                      <Text>릴리즈 브랜치: {selectedItem.releaseTarget ?? 'release'}</Text>
                      <Text>메인 자동등록: {selectedItem.autoDeployToMain ? '예' : '아니오'}</Text>
                      <Text>원본 요청 수정 가능: {isPlanItemRequestLocked(selectedItem) ? '아니오' : '예'}</Text>
                      {latestActionHistory ? (
                        <Flex align="start" gap={8} wrap>
                          <Text style={{ flex: 1 }}>
                            최근 조치: [{latestActionHistory.actionType}] {resolveProtectedText(latestActionHistory.note, !hasAccess)}
                          </Text>
                          <Button
                            size="small"
                            icon={<CopyOutlined />}
                            disabled={!hasAccess}
                            onClick={() =>
                              void handleCopyText(`[${latestActionHistory.actionType}] ${latestActionHistory.note}`)
                            }
                          />
                        </Flex>
                      ) : null}
                      {selectedItem.lastError ? (
                        <Flex align="start" gap={8} wrap>
                          <div style={{ flex: 1, minWidth: 0 }}>
                            <Text type="danger">최근 오류:</Text>
                            <ExpandableDetailText text={resolveProtectedText(selectedItem.lastError, !hasAccess)} type="danger" />
                          </div>
                          <Button
                            size="small"
                            icon={<CopyOutlined />}
                            disabled={!hasAccess}
                            onClick={() => void handleCopyText(selectedItem.lastError ?? '')}
                          />
                        </Flex>
                      ) : null}
                      {latestIssueHistory ? (
                        <Flex align="start" gap={8} wrap>
                          <div style={{ flex: 1, minWidth: 0 }}>
                            <Text>최근 이슈: {latestIssueHistory.issueTag}</Text>
                            <ExpandableDetailText text={resolveProtectedText(latestIssueHistory.message, !hasAccess)} />
                          </div>
                          <Button
                            size="small"
                            icon={<CopyOutlined />}
                            disabled={!hasAccess}
                            onClick={() =>
                              void handleCopyText(`${latestIssueHistory.issueTag} ${latestIssueHistory.message}`)
                            }
                          />
                        </Flex>
                      ) : null}
                    </Space>
                  }
                />
              ) : null}

              {selectedItem ? (
                <PlanDetailSection title="작업 체크리스트" defaultOpen>
                  <PlanWorkChecklist
                    item={selectedItem}
                    sourceWorkCount={sourceWorkHistories.length}
                    hasOpenIssues={selectedItem.hasOpenIssues}
                  />
                </PlanDetailSection>
              ) : null}

              {selectedItem ? (
                <PlanDetailSection title="릴리즈 준비 요약" defaultOpen>
                  <ReleaseReadySummary
                    hasAccess={hasAccess}
                    item={selectedItem}
                    sourceWorkHistories={sourceWorkHistories}
                    issueHistories={issueHistories}
                  />
                </PlanDetailSection>
              ) : null}

              <Space wrap className="plan-board-page__action-bar">
                {actionButtons.map((action) => (
                  <Button
                    key={action.key}
                    onClick={() => void handleAction(action.key)}
                    loading={saving}
                    disabled={isRestrictedClient}
                  >
                    {action.label}
                  </Button>
                ))}
              </Space>

              <div className="plan-board-page__form">
                <div>
                  <Text strong>작업 상태</Text>
                  <div>
                    <Tag color={statusTagColorMap[draft.status]}>{draft.status}</Tag>
                  </div>
                </div>

                {!isCreating ? (
                  <div>
                    <Text strong>기능동작확인</Text>
                    <div>
                      {canEditFunctionCheck ? (
                        <Segmented
                          options={FUNCTION_CHECK_OPTIONS}
                          value={getFunctionCheckLabel(draft.jangsingProcessingRequired)}
                          onChange={(value) => {
                            updateDraft((previous) => ({
                              ...previous,
                              jangsingProcessingRequired: value === '완료',
                            }));
                          }}
                        />
                      ) : (
                        <Text type="secondary">작업완료 이후에만 선택할 수 있습니다.</Text>
                      )}
                    </div>
                  </div>
                ) : null}

                <div>
                  <Text strong>작업 ID</Text>
                  <Input
                    value={draft.workId}
                    placeholder="예: 기능확인-기본"
                    readOnly={isRequestLocked}
                    onChange={(event) => {
                      const workId = event.target.value;
                      updateDraft((previous) => ({
                        ...previous,
                        workId,
                      }));
                    }}
                  />
                </div>

                <div>
                  <Text strong>자동화 처리</Text>
                  {requestReceived ? (
                    <div className="plan-board-page__readonly-field" aria-readonly="true">
                      <Text>{automationTypeLabel}</Text>
                      <Tag color="processing">접수 후 읽기전용</Tag>
                    </div>
                  ) : (
                    <Select
                      className="plan-board-page__select plan-board-page__select--automation"
                      value={draft.automationType}
                      options={automationTypeOptions}
                      popupClassName="plan-board-page__select-popup"
                      getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                      disabled={!hasAccess}
                      onChange={(automationType) => {
                        updateDraft((previous) => ({
                          ...previous,
                          automationType,
                        }));
                      }}
                    />
                  )}
                </div>

                <div>
                  <Flex justify="space-between" align="center" gap={8} wrap>
                    <Text strong>Context</Text>
                    <Space size={8} wrap>
                      <Text type="secondary">복수 선택 가능, 모두 해제 가능</Text>
                      <Button size="small" onClick={() => navigate(buildPlansPath('automation-context'))}>
                        Context 관리
                      </Button>
                    </Space>
                  </Flex>
                  {requestReceived ? (
                    <div className="plan-board-page__readonly-field" aria-readonly="true">
                      <Text>{draft.automationContextIds.length ? `${draft.automationContextIds.length}개 선택` : '선택 안함'}</Text>
                      <Tag color="processing">접수 후 읽기전용</Tag>
                    </div>
                  ) : (
                    <Select
                      mode="multiple"
                      allowClear
                      className="plan-board-page__select"
                      value={draft.automationContextIds}
                      options={automationContextOptions}
                      popupClassName="plan-board-page__select-popup"
                      getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                      disabled={!hasAccess}
                      placeholder="선택된 Context 없음"
                      onChange={(automationContextIds) => {
                        updateDraft((previous) => ({
                          ...previous,
                          automationContextIds,
                        }));
                      }}
                    />
                  )}
                </div>

                <div>
                  <Flex justify="space-between" align="center" gap={8} wrap>
                    <Text strong>메모</Text>
                    <Space size={8} wrap>
                      <Button
                        size="small"
                        icon={<PaperClipOutlined />}
                        disabled={!hasAccess || isRequestLocked}
                        loading={noteAttachmentUploading}
                        onClick={() => {
                          noteAttachmentInputRef.current?.click();
                        }}
                      >
                        첨부
                      </Button>
                      <Button
                        size="small"
                        icon={<CopyOutlined />}
                        disabled={!hasAccess}
                        onClick={() => void handleCopyText(noteInputValue)}
                      >
                        복사
                      </Button>
                    </Space>
                  </Flex>
                  <div className="plan-board-page__notepad-frame">
                    <TextArea
                      value={hasAccess ? noteInputValue : maskNotePreviewByWord(noteInputValue)}
                      rows={memoRows}
                      placeholder={hasAccess ? '작업 내용을 자유롭게 입력하세요.' : '권한 토큰 등록 후 편집할 수 있습니다.'}
                      className={`plan-board-page__notepad${isRequestLocked ? ' plan-board-page__notepad--readonly' : ''}`}
                      readOnly={isRequestLocked}
                      onChange={(event) => {
                        handleNoteChange(event.target.value);
                      }}
                    />
                  </div>
                  <input
                    ref={noteAttachmentInputRef}
                    type="file"
                    multiple
                    className="plan-board-page__hidden-file-input"
                    onChange={handleNoteAttachmentInputChange}
                  />
                  {isRequestLocked ? (
                    <Text type="secondary">
                      {hasAccess ? '자동화 접수된 항목은 본문을 수정할 수 없습니다.' : '조회 화면에서는 작업 메모를 40% 마스킹해 표시합니다.'}
                    </Text>
                  ) : null}
                  {noteResources.length ? <PlanNoteResourcePanel resources={noteResources} /> : null}
                </div>

                {selectedReleaseReviewNote.trim() ? (
                  <div>
                    <Flex justify="space-between" align="center" gap={8} wrap>
                      <Text strong>release 검수 메모</Text>
                      <Button
                        size="small"
                        type="text"
                        icon={<CopyOutlined />}
                        aria-label="release 검수 메모 복사"
                        disabled={!hasAccess}
                        onClick={() => void handleCopyText(selectedReleaseReviewNote)}
                      />
                    </Flex>
                    <div className="plan-board-page__notepad-frame">
                      <TextArea
                        value={hasAccess ? selectedReleaseReviewNote : maskNotePreviewByWord(selectedReleaseReviewNote)}
                        rows={4}
                        className="plan-board-page__notepad plan-board-page__notepad--readonly"
                        readOnly
                      />
                    </div>
                  </div>
                ) : null}

                <div>
                  <Checkbox
                    checked={draft.autoDeployToMain ?? true}
                    disabled={isRequestLocked}
                    onChange={(event) => {
                      const autoDeployToMain = event.target.checked;
                      updateDraft((previous) => ({
                        ...previous,
                        autoDeployToMain,
                      }));
                    }}
                  >
                    메인까지 자동등록
                  </Checkbox>
                  {isCreating ? (
                    <div style={{ marginTop: 8 }}>
                      <Text type="secondary">
                        앱 기본값: {appConfig.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'}
                      </Text>
                    </div>
                  ) : null}
                </div>

                <div>
                  <Checkbox
                    checked={draft.suppressWebPush ?? false}
                    disabled={isRequestLocked}
                    onChange={(event) => {
                      const suppressWebPush = event.target.checked;
                      updateDraft((previous) => ({
                        ...previous,
                        suppressWebPush,
                      }));
                    }}
                  >
                    자동화 웹푸쉬 보내지 않기
                  </Checkbox>
                  <div style={{ marginTop: 8 }}>
                    <Text type="secondary">자동화가 직접 요청한 알림은 이 설정과 관계없이 보냅니다.</Text>
                  </div>
                </div>

                <div>
                  <Text strong>접수 / 처리 시각</Text>
                  <Space direction="vertical" size={4} style={{ display: 'flex' }}>
                    <Text>등록일: {formatPlanDateTime(selectedItem?.createdAt ?? null)}</Text>
                    <Text>접수 시각: {formatPlanDateTime(selectedItem?.startedAt ?? null)}</Text>
                    <Text>작업완료 시각: {formatPlanDateTime(selectedItem?.completedAt ?? null)}</Text>
                    <Text>최종 반영 시각: {formatPlanDateTime(selectedItem?.mergedAt ?? null)}</Text>
                    <Text>최근 수정: {formatPlanDateTime(selectedItem?.updatedAt ?? null)}</Text>
                    {currentReleaseUsageSummary ? (
                      <Space size={[8, 4]} wrap>
                        <Text>
                          {currentReleaseUsagePrefix}: 소스 작업 {currentReleaseSourceWorkHistories.length}건
                        </Text>
                        <Text>{currentReleaseUsageSummary.costLabel}</Text>
                        <Text>{currentReleaseUsageSummary.tokenLabel}</Text>
                        {currentReleaseUsageSummary?.tierColor && currentReleaseUsageSummary.tierLabel ? (
                          <Tag color={currentReleaseUsageSummary.tierColor}>{currentReleaseUsageSummary.tierLabel}</Tag>
                        ) : null}
                        {currentReleaseUsageSummary.description ? (
                          <Text type="secondary">{currentReleaseUsageSummary.description}</Text>
                        ) : null}
                      </Space>
                    ) : null}
                  </Space>
                </div>

                <PlanDetailSection title="소스 작업 내역">
                  {sourceWorkHistories.length ? (
                    <List
                      dataSource={sourceWorkHistories}
                      renderItem={(history) => {
                        const isCurrentReleaseHistory = currentReleaseSourceWorkHistoryIdSet.has(history.id);
                        const usageSummary = isCurrentReleaseHistory
                          ? currentReleaseUsageSummaryByHistoryId.get(history.id) ?? null
                          : null;

                        return (
                          <List.Item key={`source-action-${history.id}`}>
                            <Space direction="vertical" size={8} style={{ width: '100%' }}>
                              <Flex justify="space-between" align="start" gap={8}>
                                <Space direction="vertical" size={2}>
                                  <Text strong>{resolveProtectedText(history.summary, !hasAccess)}</Text>
                                  <Text type="secondary">{history.branchName}</Text>
                                  {usageSummary ? (
                                    <Space size={[8, 4]} wrap>
                                      <Text type="secondary">
                                        {currentReleaseUsagePrefix} {usageSummary.costLabel}
                                      </Text>
                                      <Text type="secondary">{usageSummary.tokenLabel}</Text>
                                      {usageSummary.tierColor && usageSummary.tierLabel ? (
                                        <Tag color={usageSummary.tierColor}>{usageSummary.tierLabel}</Tag>
                                      ) : null}
                                      {usageSummary.description ? (
                                        <Text type="secondary">{usageSummary.description}</Text>
                                      ) : null}
                                    </Space>
                                  ) : null}
                                </Space>
                                <Text type="secondary">
                                  {new Date(history.createdAt).toLocaleString('ko-KR')}
                                </Text>
                              </Flex>
                              <Flex justify="end">
                                <Button disabled={!hasAccess} onClick={() => void handleOpenSourceWork(history.id)}>
                                  미리보기
                                </Button>
                              </Flex>
                            </Space>
                          </List.Item>
                        );
                      }}
                    />
                  ) : (
                    <Empty description="기록된 소스 작업 이력이 없습니다." />
                  )}
                </PlanDetailSection>

                <PlanDetailSection title="조치 / 이슈 이력">
                  {combinedHistories.length ? (
                    <List
                      dataSource={combinedHistories}
                      renderItem={(history) => (
                        <List.Item key={history.id}>
                          <Space direction="vertical" size={4} style={{ width: '100%' }}>
                            <Flex justify="space-between" align="start" gap={8}>
                              {history.kind === 'action' ? (
                                <Tag color="blue">{history.action.actionType}</Tag>
                              ) : (
                                <Tag color={history.issue.resolved ? 'default' : 'error'}>
                                  이슈 {history.issue.issueTag}
                                </Tag>
                              )}
                              <Text type="secondary">
                                {new Date(history.createdAt).toLocaleString('ko-KR')}
                              </Text>
                            </Flex>
                            {history.kind === 'action' ? (
                              <Text>{resolveProtectedText(history.action.note, !hasAccess)}</Text>
                            ) : (
                              <>
                                <ExpandableDetailText text={resolveProtectedText(history.issue.message, !hasAccess)} />
                                <Flex justify="end">
                                  <Button
                                    size="small"
                                    icon={<CopyOutlined />}
                                    disabled={!hasAccess}
                                    onClick={() => void handleCopyText(history.issue.message)}
                                  >
                                    에러 복사
                                  </Button>
                                </Flex>
                                {history.issue.actionNote ? (
                                  <Alert
                                    type={history.issue.resolved ? 'success' : 'info'}
                                    showIcon
                                    message={history.issue.resolved ? '조치 완료' : '조치 기록'}
                                    description={
                                      <pre className="plan-board-page__viewer-pre">
                                        {resolveProtectedText(history.issue.actionNote, !hasAccess)}
                                      </pre>
                                    }
                                  />
                                ) : null}
                              </>
                            )}
                          </Space>
                        </List.Item>
                      )}
                    />
                  ) : (
                    <Empty description="조치 및 이력 기록이 없습니다." />
                  )}
                </PlanDetailSection>

                {isRequestLocked ? (
                  <Card size="small" title="추가 조치 기록">
                    <Space direction="vertical" size={12} style={{ width: '100%' }}>
                      <TextArea
                        value={actionNote}
                        rows={4}
                        placeholder="작업시작 이후의 추가 조치사항을 기록하세요."
                        disabled={!hasAccess}
                        onChange={(event) => {
                          setActionNote(event.target.value);
                        }}
                      />
                      <Flex justify="end">
                        <Button onClick={() => void handleActionNote()} loading={saving} disabled={!hasAccess}>
                          조치 이력 추가
                        </Button>
                      </Flex>
                    </Space>
                  </Card>
                ) : null}

                <div>
                  <Text strong>이슈 태그</Text>
                  <div>
                    {selectedItem?.issueTags.length ? (
                      <Space wrap size={[4, 4]}>
                        {selectedItem.issueTags.map((issueTag) => (
                          <Tag key={`detail-${issueTag}`} color={selectedItem.hasOpenIssues ? 'error' : 'default'}>
                            {issueTag}
                          </Tag>
                        ))}
                      </Space>
                    ) : (
                      <Text type="secondary">현재 기록된 이슈가 없습니다.</Text>
                    )}
                  </div>
                </div>

                {hasIssueHistory ? (
                  <Card size="small" title="이슈 조치 기록">
                    <Space direction="vertical" size={12} style={{ width: '100%' }}>
                      <TextArea
                        value={issueActionNote}
                        rows={4}
                        placeholder="오류 원인 분석, 조치 내용, 재시도 결과를 기록하세요."
                        disabled={!hasAccess}
                        onChange={(event) => {
                          setIssueActionNote(event.target.value);
                        }}
                      />
                      <Checkbox
                        checked={resolveLatestIssue}
                        disabled={!hasAccess}
                        onChange={(event) => {
                          setResolveLatestIssue(event.target.checked);
                        }}
                      >
                        최신 이슈를 해결 처리
                      </Checkbox>
                      <Checkbox
                        checked={retryLatestIssue}
                        disabled={!hasAccess}
                        onChange={(event) => {
                          setRetryLatestIssue(event.target.checked);
                        }}
                      >
                        재처리 요청
                      </Checkbox>
                      <Flex justify="end">
                        <Button onClick={() => void handleIssueAction()} loading={saving} disabled={!hasAccess}>
                          조치 기록 저장
                        </Button>
                      </Flex>
                    </Space>
                  </Card>
                ) : null}
              </div>
            </div>
          ) : (
            <div className="plan-board-page__overlay-body">
              <Flex justify="space-between" align="start" gap={12} wrap>
                <div>
                  <Title level={5} className="plan-board-page__viewer-heading">
                    {resolveProtectedText(selectedSourceWork?.summary, !hasAccess)}
                  </Title>
                  <Text type="secondary">{selectedSourceWork?.branchName}</Text>
                  {selectedSourceWorkUsageSummary ? (
                    <Space size={[8, 4]} wrap>
                      <Text type="secondary">
                        {currentReleaseUsagePrefix} {selectedSourceWorkUsageSummary.costLabel}
                      </Text>
                      <Text type="secondary">{selectedSourceWorkUsageSummary.tokenLabel}</Text>
                      {selectedSourceWorkUsageSummary.tierColor && selectedSourceWorkUsageSummary.tierLabel ? (
                        <Tag color={selectedSourceWorkUsageSummary.tierColor}>
                          {selectedSourceWorkUsageSummary.tierLabel}
                        </Tag>
                      ) : null}
                      {selectedSourceWorkUsageSummary.description ? (
                        <Text type="secondary">{selectedSourceWorkUsageSummary.description}</Text>
                      ) : null}
                    </Space>
                  ) : null}
                </div>
                <Space>
                  <Button
                    icon={<ArrowLeftOutlined />}
                    aria-label="상세로 돌아가기"
                    onClick={() => setSourceViewerOpen(false)}
                  />
                  <Button onClick={closeEditor}>닫기</Button>
                </Space>
              </Flex>

              <Tabs
                className="plan-board-page__viewer-tabs"
                items={buildSourceViewerTabs(selectedSourceWork, {
                  hasAccess,
                  note: selectedItem?.note ?? '',
                  noteMasked: Boolean(selectedItem?.noteMasked || !hasAccess),
                })}
              />
            </div>
          )
        }
      />

    </div>
  );
}

const PlanItemList = memo(
  function PlanItemList({
    activeDraftId,
    currentPage,
    editorOpen,
    hasAccess,
    items,
    jangsingProcessingSavingId,
    reviewIndicatorsByPlanId,
    searchKeyword,
    usageSummaryByPlanId,
    onChangePage,
    onChangeJangsingProcessing,
    onSelectItem,
  }: {
    activeDraftId: number | null;
    currentPage: number;
    editorOpen: boolean;
    hasAccess: boolean;
    items: PlanItem[];
    jangsingProcessingSavingId: number | null;
    reviewIndicatorsByPlanId: Record<number, ReviewListIndicator>;
    searchKeyword: string;
    usageSummaryByPlanId: Record<number, AutomationUsageSummary>;
    onChangePage: (page: number) => void;
    onChangeJangsingProcessing: (id: number, jangsingProcessingRequired: boolean) => void;
    onSelectItem: (item: PlanItem) => void;
  }) {
    if (items.length === 0) {
      return <Empty description={searchKeyword.trim() ? '검색 결과가 없습니다.' : '등록된 작업 항목이 없습니다.'} />;
    }

    return (
      <List
        className="plan-board-page__list"
        dataSource={items}
        pagination={{
          align: 'center',
          current: currentPage,
          pageSize: PLAN_LIST_PAGE_SIZE,
          showSizeChanger: false,
          onChange: hasAccess ? onChangePage : undefined,
        }}
        renderItem={(item) => (
          (() => {
            const reviewIndicator = reviewIndicatorsByPlanId[item.id];
            const usageSummary = usageSummaryByPlanId[item.id] ?? null;

            return (
              <List.Item
                key={item.id}
                className={`plan-board-page__list-item${
                  activeDraftId === item.id && editorOpen ? ' plan-board-page__list-item--active' : ''
                }`}
                onClick={() => {
                  onSelectItem(item);
                }}
              >
                <div className="plan-board-page__list-body">
                  <Flex justify="space-between" align="start" gap={8}>
                    <Text strong>{formatPlanListLabel(item)}</Text>
                    <Space size={[6, 6]} wrap className="plan-board-page__list-tags">
                      {reviewIndicator ? (
                        <Tag color={getReleaseReviewListTagColor(reviewIndicator.status)}>
                          {getReleaseReviewListTagLabel(reviewIndicator)}
                        </Tag>
                      ) : null}
                      <Tag color={statusTagColorMap[item.status]}>{item.status}</Tag>
                    </Space>
                  </Flex>
                  <Flex justify="space-between" align="center" gap={8} wrap>
                    <Text type="secondary">기능동작확인</Text>
                    {isFunctionCheckEditableStatus(item.status) ? (
                      <Space size={6} wrap>
                        <Segmented
                          size="small"
                          options={FUNCTION_CHECK_OPTIONS}
                          value={getFunctionCheckLabel(item.jangsingProcessingRequired)}
                          disabled={!hasAccess || jangsingProcessingSavingId === item.id || isPlanItemRequestLocked(item)}
                          onClick={(event) => {
                            event.stopPropagation();
                          }}
                          onChange={(value) => {
                            onChangeJangsingProcessing(item.id, value === '완료');
                          }}
                        />
                      </Space>
                    ) : (
                      <Text type="secondary">작업완료 후 선택</Text>
                    )}
                  </Flex>
                  <AutomationStatusBar item={item} usageSummary={usageSummary} compact />
                  {item.assignedBranch ? <Text type="secondary">브랜치: {item.assignedBranch}</Text> : null}
                  {item.issueTags.length > 0 ? (
                    <Space wrap size={[4, 4]}>
                      {item.issueTags.map((issueTag) => (
                        <Tag key={`${item.id}-${issueTag}`} color={item.hasOpenIssues ? 'error' : 'default'}>
                          {issueTag}
                        </Tag>
                      ))}
                    </Space>
                  ) : null}
                  <Paragraph ellipsis={{ rows: 2 }} className="plan-board-page__list-note">
                    {item.noteMasked || !hasAccess ? maskNotePreviewByWord(item.note) : item.note}
                  </Paragraph>
                  <Flex justify="space-between" align="center" gap={8} wrap>
                    <Space size={8} wrap>
                      {item.noteMasked ? <Tag bordered={false}>요청내용 마스킹</Tag> : null}
                      <Text type="secondary">등록 {formatPlanDateTime(item.createdAt)}</Text>
                    </Space>
                    <Text type="secondary">수정 {formatPlanDateTime(item.updatedAt)}</Text>
                  </Flex>
                </div>
              </List.Item>
            );
          })()
        )}
      />
    );
  },
  (previous, next) =>
    previous.items === next.items &&
    previous.jangsingProcessingSavingId === next.jangsingProcessingSavingId &&
    previous.reviewIndicatorsByPlanId === next.reviewIndicatorsByPlanId &&
    previous.searchKeyword === next.searchKeyword &&
    previous.currentPage === next.currentPage &&
    previous.activeDraftId === next.activeDraftId &&
    previous.editorOpen === next.editorOpen &&
    previous.hasAccess === next.hasAccess,
);

function LongPressButton({
  children,
  className,
  loading,
  longPressMs,
  onClick,
  onLongPress,
  title,
}: {
  children: ReactNode;
  className?: string;
  loading?: boolean;
  longPressMs: number;
  onClick: () => void;
  onLongPress: () => void;
  title?: string;
}) {
  const [longPressTriggered, setLongPressTriggered] = useState(false);
  const timerRef = useRef<number | null>(null);

  const clearPressTimer = () => {
    if (timerRef.current !== null) {
      window.clearTimeout(timerRef.current);
      timerRef.current = null;
    }
  };

  useEffect(() => clearPressTimer, []);

  return (
    <Button
      className={className}
      loading={loading}
      title={title}
      onClick={() => {
        if (longPressTriggered) {
          setLongPressTriggered(false);
          return;
        }

        onClick();
      }}
      onPointerDown={(event) => {
        if (event.pointerType === 'mouse' && event.button !== 0) {
          return;
        }

        timerRef.current = window.setTimeout(() => {
          setLongPressTriggered(true);
          onLongPress();
        }, longPressMs);
      }}
      onPointerUp={() => {
        clearPressTimer();
        window.setTimeout(() => {
          setLongPressTriggered(false);
        }, 0);
      }}
      onPointerLeave={() => {
        clearPressTimer();
      }}
      onPointerCancel={() => {
        clearPressTimer();
        setLongPressTriggered(false);
      }}
    >
      {children}
    </Button>
  );
}

function getActionSuccessMessage(action: PlanActionType) {
  if (action === 'start-work') {
    return '작업을 시작했습니다.';
  }

  if (action === 'complete-development') {
    return '작업완료로 전환했습니다. release 반영 자동화를 대기합니다.';
  }

  if (action === 'retry-branch') {
    return '브랜치 재시도를 요청했습니다.';
  }

  if (action === 'retry-work') {
    return '자동 작업 재처리를 요청했습니다.';
  }

  if (action === 'retry-merge') {
    return 'release 반영 재시도를 요청했습니다.';
  }

  if (action === 'cancel-release') {
    return '취소완료 처리했습니다.';
  }

  return '이슈 브랜치 기준으로 main 반영을 요청했습니다.';
}

const FUNCTION_CHECK_OPTIONS = ['완료', '오동작'];

function buildSourceViewerTabs(
  sourceWork: PlanSourceWorkHistory | null,
  planContext?: {
    hasAccess?: boolean;
    note: string;
    noteMasked: boolean;
  },
) {
  if (!sourceWork) {
    return [];
  }

  const normalizedPreviewUrl = normalizeReleasePreviewUrl(sourceWork.previewUrl);
  const hasAccess = Boolean(planContext?.hasAccess);

  const summaryItems = [
    `요청 요약: ${resolveProtectedText(sourceWork.summary, !hasAccess)}`,
    `브랜치: ${sourceWork.branchName}`,
    `Preview: ${normalizedPreviewUrl ?? '없음'}`,
    `기록 시각: ${new Date(sourceWork.createdAt).toLocaleString('ko-KR')}`,
  ];

  return [
    {
      key: 'summary',
      label: '작업란',
      children: (
        <div className="plan-board-page__viewer-stack">
          <ol className="plan-board-page__summary-list">
            {summaryItems.map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ol>
          <div>
            <Text strong>요청 메모</Text>
            <Paragraph className="plan-board-page__memo-pre">
              {resolvePlanNoteDetail(planContext?.note ?? '', Boolean(planContext?.noteMasked))}
            </Paragraph>
          </div>
          {sourceWork.changedFiles.length ? (
            <div className="plan-board-page__file-tags">
              {sourceWork.changedFiles.map((file) => (
                <Tag key={`viewer-summary-${file}`}>{file}</Tag>
              ))}
            </div>
          ) : null}
          {normalizedPreviewUrl ? (
            <Button
              type="link"
              href={hasAccess ? normalizedPreviewUrl : undefined}
              target="_blank"
              rel="noreferrer"
              disabled={!hasAccess}
              style={{ paddingInline: 0 }}
            >
              Preview 링크 열기
            </Button>
          ) : null}
        </div>
      ),
    },
    {
      key: 'source',
      label: '전체소스',
      children: (
        <CodexDiffPreviewer
          files={sourceWork.sourceFiles}
          diffText={sourceWork.diffText}
          height="auto"
          mode="source"
          showModeSwitch={false}
        />
      ),
    },
    {
      key: 'diff',
      label: 'diff',
      children: (
        <CodexDiffPreviewer
          files={sourceWork.sourceFiles}
          diffText={sourceWork.diffText}
          height="auto"
          mode="diff"
          showModeSwitch={false}
        />
      ),
    },
    {
      key: 'commands',
      label: '커맨드',
      children: (
        <PreviewerUI
          type="code"
          title="Command Log"
          description="자동화 실행 커맨드"
          value={sourceWork.commandLog || '기록된 실행 커맨드가 없습니다.'}
          language="bash"
          height="auto"
        />
      ),
    },
    {
      key: 'evidence',
      label: '증적',
      children: <WorklogEvidenceTab sourceWork={sourceWork} />,
    },
    {
      key: 'files',
      label: '파일',
      children: sourceWork.changedFiles.length ? (
        <List
          dataSource={sourceWork.changedFiles}
          renderItem={(file) => (
            <List.Item key={`viewer-file-row-${file}`}>
              <Text code>{file}</Text>
            </List.Item>
          )}
        />
      ) : (
        <Empty description="기록된 파일이 없습니다." />
      ),
    },
  ];
}

function resolvePlanNoteDetail(note: string, masked: boolean) {
  if (!masked) {
    return note || '메모가 없습니다.';
  }

  return maskNotePreviewByWord(note);
}

export function WorklogEvidenceTab({
  sourceWork,
}: {
  sourceWork: PlanSourceWorkHistory;
}) {
  const normalizedPreviewUrl = normalizeReleasePreviewUrl(sourceWork.previewUrl);
  const worklogPaths = useMemo(() => {
    const explicitWorklogPaths = sourceWork.changedFiles.filter((file) => /^docs\/worklogs\/.+\.md$/i.test(file));
    const inferredWorklogPaths = sourceWork.changedFiles
      .map((file) => file.match(/^docs\/assets\/worklogs\/(\d{4}-\d{2}-\d{2})\//i)?.[1] ?? null)
      .filter((value): value is string => Boolean(value))
      .map((captureDate) => `docs/worklogs/${captureDate}.md`);

    return Array.from(new Set([...explicitWorklogPaths, ...inferredWorklogPaths]));
  }, [sourceWork.changedFiles]);
  const screenshotPaths = useMemo(
    () =>
      Array.from(
        new Set(
          sourceWork.changedFiles.filter((file) =>
            /^docs\/assets\/worklogs\/.+\.(png|jpe?g|gif|webp|svg)$/i.test(file),
          ),
        ),
      ),
    [sourceWork.changedFiles],
  );
  const artifactPaths = useMemo(
    () =>
      Array.from(
        new Set(
          sourceWork.changedFiles.filter(
            (file) =>
              /^docs\/assets\/worklogs\/.+/i.test(file) &&
              !/^docs\/assets\/worklogs\/.+\.(png|jpe?g|gif|webp|svg)$/i.test(file),
          ),
        ),
      ),
    [sourceWork.changedFiles],
  );
  const fetchableTextPaths = useMemo(
    () =>
      Array.from(
        new Set(
          [...worklogPaths, ...artifactPaths].filter((path) => isFetchableTextArtifactPath(path)),
        ),
      ),
    [artifactPaths, worklogPaths],
  );
  const fetchableTextPathsKey = fetchableTextPaths.join('\n');
  const [worklogContents, setWorklogContents] = useState<Record<string, string>>({});
  const [loading, setLoading] = useState(false);
  const [previewModalItem, setPreviewModalItem] = useState<EvidenceAttachmentItem | null>(null);
  const [isPreviewModalExpanded, setIsPreviewModalExpanded] = useState(false);

  async function handleCopyText(text: string) {
    try {
      await navigator.clipboard.writeText(text);
      message.success('복사했습니다.');
    } catch {
      message.error('복사에 실패했습니다.');
    }
  }

  useEffect(() => {
    let cancelled = false;

    if (!fetchableTextPaths.length) {
      setWorklogContents({});
      return;
    }

    setLoading(true);

    void Promise.all(
      fetchableTextPaths.map(async (path) => {
        try {
          const response = await fetch(toPublicAssetPath(path), { cache: 'no-store' });

          if (!response.ok) {
            throw new Error(`작업일지 로드 실패: ${path}`);
          }

          return [path, await response.text()] as const;
        } catch {
          return [path, '작업일지 내용을 불러오지 못했습니다.'] as const;
        }
      }),
    )
      .then((entries) => {
        if (cancelled) {
          return;
        }

        setWorklogContents(Object.fromEntries(entries));
      })
      .finally(() => {
        if (!cancelled) {
          setLoading(false);
        }
      });

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

  const previewItems: EvidenceAttachmentItem[] = [
    ...worklogPaths.map((path) => ({
      key: `worklog:${path}`,
      kind: 'markdown' as const,
      title: path.split('/').pop() ?? path,
      description: path,
      linkUrl: toPublicAssetPath(path),
      value: worklogContents[path] ?? (loading ? '작업일지 내용을 불러오는 중입니다.' : '작업일지 내용을 불러오지 못했습니다.'),
    })),
    ...screenshotPaths.map((path) => ({
      key: `screenshot:${path}`,
      kind: 'image' as const,
      title: path.split('/').pop() ?? path,
      description: path,
      linkUrl: toPublicAssetPath(path),
      value: toPublicAssetPath(path),
    })),
    ...artifactPaths.map((path) =>
      buildArtifactPreviewItem(path, worklogContents[path], loading),
    ),
    ...(normalizedPreviewUrl
      ? [
          {
            key: `preview:${sourceWork.id}`,
            kind: 'preview' as const,
            title: 'Preview 링크',
            description: normalizedPreviewUrl,
            linkUrl: normalizedPreviewUrl,
            value: normalizedPreviewUrl,
          },
        ]
      : []),
    ...(sourceWork.commandLog
      ? [
          {
            key: `commands:${sourceWork.id}`,
            kind: 'code' as const,
            title: '실행 커맨드',
            description: '자동화 실행 로그',
            value: sourceWork.commandLog,
            language: 'bash',
          },
        ]
      : []),
  ];

  if (previewItems.length === 0 && !sourceWork.sourceFiles.length && !sourceWork.diffText) {
    return <Empty description="연결된 작업일지 산출물이 없습니다." />;
  }

  return (
    <>
      <Space direction="vertical" size={16} style={{ width: '100%' }}>
        {sourceWork.sourceFiles.length || sourceWork.diffText ? (
          <CodexDiffPreviewer
            title="작업 소스"
            description="변경 파일 기준 전체 소스와 raw diff를 Codex preview 스타일로 전환해 표시합니다."
            files={sourceWork.sourceFiles}
            diffText={sourceWork.diffText}
            height="auto"
          />
        ) : null}
        {previewItems.length ? (
          <EvidenceAttachmentStrip
            title="산출물 Preview"
            attachments={previewItems}
            compact
            onPreview={(attachment) => {
              setPreviewModalItem(attachment);
            }}
            onCopy={(attachment) => handleCopyText(attachment.copyValue ?? attachment.value)}
          />
        ) : null}
      </Space>

      <Modal
        open={Boolean(previewModalItem)}
        title={previewModalItem?.title ?? 'Preview'}
        footer={null}
        width={isPreviewModalExpanded ? 'calc(100vw - 32px)' : 1120}
        rootClassName={`plan-board-page__evidence-modal${
          isPreviewModalExpanded ? ' plan-board-page__evidence-modal--expanded' : ''
        }`}
        onCancel={() => {
          setPreviewModalItem(null);
          setIsPreviewModalExpanded(false);
        }}
        closeIcon={<CloseOutlined />}
      >
        {previewModalItem ? (
          <div className="plan-board-page__evidence-modal-shell">
            <Flex justify="space-between" align="center" gap={12} wrap className="plan-board-page__evidence-modal-toolbar">
              <Text type="secondary">{previewModalItem.description}</Text>
              <Space size={8}>
                <Button
                  aria-label="복사"
                  icon={<CopyOutlined />}
                  onClick={() => void handleCopyText(previewModalItem.value)}
                />
                {previewModalItem.linkUrl ? (
                  <Button
                    aria-label="다운로드"
                    icon={<DownloadOutlined />}
                    href={previewModalItem.linkUrl}
                    target={previewModalItem.kind === 'preview' ? '_blank' : undefined}
                    rel={previewModalItem.kind === 'preview' ? 'noreferrer' : undefined}
                    download={previewModalItem.kind === 'preview' ? undefined : true}
                  />
                ) : null}
                <Button
                  aria-label={isPreviewModalExpanded ? '최대화 해제' : '최대화'}
                  icon={isPreviewModalExpanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
                  onClick={() => {
                    setIsPreviewModalExpanded((previous) => !previous);
                  }}
                />
                <Button
                  aria-label="닫기"
                  icon={<CloseOutlined />}
                  onClick={() => {
                    setPreviewModalItem(null);
                    setIsPreviewModalExpanded(false);
                  }}
                />
              </Space>
            </Flex>
            <div className="plan-board-page__evidence-modal-body">
              <EvidenceAttachmentPreviewBody attachment={previewModalItem} />
            </div>
          </div>
        ) : null}
      </Modal>
    </>
  );
}

function toPublicAssetPath(path: string) {
  const normalizedPath = path.trim().replace(/^\.?\//, '');

  return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
}

function normalizeReleasePreviewUrl(url: string | null) {
  return url;
}

const IMAGE_ARTIFACT_PATTERN = /\.(png|jpe?g|gif|webp|svg)$/i;
const VIDEO_ARTIFACT_PATTERN = /\.(mp4|webm|mov|m4v|ogv)$/i;
const AUDIO_ARTIFACT_PATTERN = /\.(mp3|wav|m4a|aac|flac|oga|ogg)$/i;
const PDF_ARTIFACT_PATTERN = /\.pdf$/i;
const HTML_ARTIFACT_PATTERN = /\.(html?)$/i;
const MARKDOWN_ARTIFACT_PATTERN = /\.(md|mdx)$/i;
const JSON_ARTIFACT_PATTERN = /\.(json|jsonl)$/i;
const TEXT_ARTIFACT_PATTERN =
  /\.(txt|log|csv|yaml|yml|xml|diff|patch|sh|bash|zsh|ini|cfg|conf|sql|js|jsx|ts|tsx|css|scss|less|java|kt|py|rb|go|rs)$/i;

function buildArtifactPreviewItem(path: string, content: string | undefined, loading: boolean): EvidenceAttachmentItem {
  const publicPath = toPublicAssetPath(path);
  const fileName = path.split('/').pop() ?? path;

  if (IMAGE_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'image',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: publicPath,
    };
  }

  if (VIDEO_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'video',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: publicPath,
    };
  }

  if (AUDIO_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'audio',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: publicPath,
    };
  }

  if (PDF_ARTIFACT_PATTERN.test(path) || HTML_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'pdf',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: publicPath,
    };
  }

  if (MARKDOWN_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'markdown',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: content ?? (loading ? '파일 내용을 불러오는 중입니다.' : '파일 내용을 불러오지 못했습니다.'),
    };
  }

  if (JSON_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'json',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: content ?? (loading ? '파일 내용을 불러오는 중입니다.' : '파일 내용을 불러오지 못했습니다.'),
    };
  }

  if (TEXT_ARTIFACT_PATTERN.test(path)) {
    return {
      key: `artifact:${path}`,
      kind: 'code',
      title: fileName,
      description: path,
      linkUrl: publicPath,
      value: content ?? (loading ? '파일 내용을 불러오는 중입니다.' : '파일 내용을 불러오지 못했습니다.'),
      language: inferArtifactLanguage(path),
    };
  }

  return {
    key: `artifact:${path}`,
    kind: 'empty',
    title: fileName,
    description: path,
    linkUrl: publicPath,
    value: '미리보기를 지원하지 않는 형식입니다. 링크로 원본 파일을 열어 확인하세요.',
  };
}

function isFetchableTextArtifactPath(path: string) {
  return MARKDOWN_ARTIFACT_PATTERN.test(path) || JSON_ARTIFACT_PATTERN.test(path) || TEXT_ARTIFACT_PATTERN.test(path);
}

function inferArtifactLanguage(path: string) {
  const normalizedPath = path.toLowerCase();

  if (normalizedPath.endsWith('.tsx')) {
    return 'tsx';
  }

  if (normalizedPath.endsWith('.ts')) {
    return 'typescript';
  }

  if (normalizedPath.endsWith('.jsx')) {
    return 'jsx';
  }

  if (normalizedPath.endsWith('.js')) {
    return 'javascript';
  }

  if (normalizedPath.endsWith('.json') || normalizedPath.endsWith('.jsonl')) {
    return 'json';
  }

  if (normalizedPath.endsWith('.yml') || normalizedPath.endsWith('.yaml')) {
    return 'yaml';
  }

  if (normalizedPath.endsWith('.html') || normalizedPath.endsWith('.htm')) {
    return 'html';
  }

  if (normalizedPath.endsWith('.css') || normalizedPath.endsWith('.scss') || normalizedPath.endsWith('.less')) {
    return 'css';
  }

  if (normalizedPath.endsWith('.diff') || normalizedPath.endsWith('.patch')) {
    return 'diff';
  }

  if (normalizedPath.endsWith('.sh') || normalizedPath.endsWith('.bash') || normalizedPath.endsWith('.zsh')) {
    return 'bash';
  }

  if (normalizedPath.endsWith('.py')) {
    return 'python';
  }

  if (normalizedPath.endsWith('.md') || normalizedPath.endsWith('.mdx')) {
    return 'markdown';
  }

  return 'text';
}

function toDraft(item: PlanItem): PlanDraft {
  return {
    id: item.id,
    workId: item.workId,
    note: item.note,
    automationType: item.automationType,
    automationContextIds: item.automationContextIds ?? [],
    status: item.status,
    jangsingProcessingRequired: item.jangsingProcessingRequired,
    autoDeployToMain: item.autoDeployToMain,
    suppressWebPush: item.suppressWebPush,
    repeatRequestEnabled: item.repeatRequestEnabled,
    repeatIntervalMinutes: item.repeatIntervalMinutes,
  };
}

function isFunctionCheckEditableStatus(status: PlanItem['status'] | PlanDraft['status']) {
  return isCompletedPlanStatus(status);
}

function isCompletedPlanStatus(status: PlanItem['status'] | PlanDraft['status']) {
  return status === '작업완료' || status === '릴리즈완료' || status === '완료';
}

function isReleaseCompletedPlanItem(item: Pick<PlanItem, 'status' | 'mergedAt'>) {
  return item.status === '릴리즈완료' || item.status === '완료' || Boolean(item.mergedAt);
}

function isMainCompletedPlanItem(item: Pick<PlanItem, 'status' | 'workerStatus' | 'autoDeployToMain'> | null | undefined) {
  if (!item || !item.autoDeployToMain) {
    return false;
  }

  return item.status === '완료' || item.workerStatus?.replace(/\s+/g, '') === 'main반영완료';
}

function PlanDetailSection({
  children,
  defaultOpen = false,
  title,
}: {
  children: ReactNode;
  defaultOpen?: boolean;
  title: string;
}) {
  return (
    <details className="plan-board-page__detail-section" open={defaultOpen}>
      <summary className="plan-board-page__detail-section-summary">
        <Text strong>{title}</Text>
        <Text type="secondary">접기/펼치기</Text>
      </summary>
      <div className="plan-board-page__detail-section-body">{children}</div>
    </details>
  );
}

function PlanWorkChecklist({
  hasOpenIssues,
  item,
  sourceWorkCount,
}: {
  hasOpenIssues: boolean;
  item: PlanItem;
  sourceWorkCount: number;
}) {
  const workerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';
  const checklist = [
    {
      key: 'implementation',
      label: '구현 변경 기록',
      checked: sourceWorkCount > 0 || workerStatus === '자동완료' || item.status !== '등록',
      note: sourceWorkCount > 0 ? `${sourceWorkCount}개 소스 작업 증적` : '소스 작업 증적 대기',
    },
    {
      key: 'verification',
      label: '기능동작확인',
      checked: item.status === '작업완료' || item.status === '릴리즈완료' || item.status === '완료',
      note: getFunctionCheckLabel(item.jangsingProcessingRequired),
    },
    {
      key: 'release',
      label: 'release 반영',
      checked: item.status === '릴리즈완료' || item.status === '완료' || Boolean(item.mergedAt),
      note: workerStatus === 'release반영실패' ? 'release 반영 실패' : item.releaseTarget ?? 'release',
    },
    {
      key: 'main',
      label: 'main 동기화',
      checked: item.status === '완료' || workerStatus === 'main반영완료' || !item.autoDeployToMain,
      note: getMainChecklistNote(item),
    },
    {
      key: 'issues',
      label: '열린 이슈 없음',
      checked: !hasOpenIssues,
      note: hasOpenIssues ? '조치 필요' : '정상',
    },
  ];

  return (
    <Space direction="vertical" size={8} className="plan-board-page__checklist">
      {checklist.map((entry) => (
        <Flex key={entry.key} justify="space-between" align="center" gap={8} wrap>
          <Checkbox checked={entry.checked} disabled>
            {entry.label}
          </Checkbox>
          <Text type={entry.checked ? 'secondary' : 'warning'}>{entry.note}</Text>
        </Flex>
      ))}
    </Space>
  );
}

function ReleaseReadySummary({
  hasAccess,
  issueHistories,
  item,
  sourceWorkHistories,
}: {
  hasAccess: boolean;
  issueHistories: PlanIssueHistory[];
  item: PlanItem;
  sourceWorkHistories: PlanSourceWorkHistory[];
}) {
  const changedFiles = Array.from(new Set(sourceWorkHistories.flatMap((history) => history.changedFiles)));
  const unresolvedIssueCount = issueHistories.filter((history) => !history.resolved).length;
  const workerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';
  const releaseBlocked =
    Boolean(item.lastError) ||
    item.hasOpenIssues ||
    workerStatus === 'release반영실패' ||
    workerStatus === 'main반영실패' ||
    (item.status === '작업중' && sourceWorkHistories.length === 0);

  return (
    <Space direction="vertical" size={12} className="plan-board-page__release-summary">
      <Flex gap={8} wrap>
        <Tag color={releaseBlocked ? 'error' : 'success'}>
          {releaseBlocked ? '추가 확인 필요' : 'release 반영 가능'}
        </Tag>
        <Tag>{`변경 파일 ${changedFiles.length}개`}</Tag>
        <Tag color={unresolvedIssueCount ? 'warning' : 'default'}>{`미해결 이슈 ${unresolvedIssueCount}개`}</Tag>
      </Flex>
      <Space direction="vertical" size={4}>
        <Text>테스트/검증: {getFunctionCheckLabel(item.jangsingProcessingRequired)}</Text>
        <Text>release 상태: {getReleaseSummaryNote(item)}</Text>
        <Text>main 상태: {getMainChecklistNote(item)}</Text>
      </Space>
      {changedFiles.length ? (
        <div className="plan-board-page__file-tags">
          {changedFiles.slice(0, 20).map((file) => (
            <Tag key={`release-summary-${file}`}>{file}</Tag>
          ))}
          {changedFiles.length > 20 ? <Tag>{`+${changedFiles.length - 20}`}</Tag> : null}
        </div>
      ) : (
        <Text type="secondary">기록된 변경 파일이 없습니다.</Text>
      )}
      {item.lastError ? <ExpandableDetailText text={resolveProtectedText(item.lastError, !hasAccess)} type="danger" /> : null}
    </Space>
  );
}

function getReleaseSummaryNote(item: PlanItem) {
  const workerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';

  if (workerStatus === 'release반영실패') {
    return 'release 반영 실패';
  }

  if (item.status === '릴리즈완료' || item.status === '완료' || item.mergedAt) {
    return `반영 완료 (${formatPlanDateTime(item.mergedAt)})`;
  }

  if (workerStatus === 'release반영중') {
    return 'release 반영 중';
  }

  if (workerStatus === 'release반영대기' || item.status === '작업완료') {
    return 'release 반영 대기';
  }

  return 'release 반영 전';
}

function getMainChecklistNote(item: PlanItem) {
  const workerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';

  if (!item.autoDeployToMain) {
    return 'main 자동등록 미대상';
  }

  if (workerStatus === 'main반영실패') {
    return 'main 반영 실패';
  }

  if (item.status === '완료' || workerStatus === 'main반영완료') {
    return `main 반영 완료 (${formatPlanDateTime(item.mergedAt)})`;
  }

  if (workerStatus === 'main반영중') {
    return 'main 반영 중';
  }

  if (workerStatus === 'main반영대기' || item.status === '릴리즈완료') {
    return 'main 반영 대기';
  }

  return 'main 반영 전';
}

function matchesWorkerStateFilter(item: PlanItem, filter: WorkerStateFilter) {
  if (filter === 'all') {
    return true;
  }

  const normalizedWorkerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';

  if (filter === 'active') {
    return isAutomationInProgress(item.workerStatus);
  }

  if (filter === 'waiting') {
    return normalizedWorkerStatus.endsWith('대기') || normalizedWorkerStatus === '브랜치준비';
  }

  if (filter === 'failed') {
    return isAutomationFailedItem(item) || Boolean(item.lastError);
  }

  if (filter === 'done') {
    return normalizedWorkerStatus === '자동완료' || normalizedWorkerStatus.endsWith('완료');
  }

  return !normalizedWorkerStatus;
}

function matchesReleaseStateFilter(item: PlanItem, filter: ReleaseStateFilter) {
  if (filter === 'all') {
    return true;
  }

  const normalizedWorkerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';

  if (filter === 'pending') {
    return item.status === '작업완료' || normalizedWorkerStatus === 'release반영대기' || normalizedWorkerStatus === 'release반영중';
  }

  if (filter === 'merged') {
    return item.status === '릴리즈완료' || item.status === '완료' || Boolean(item.mergedAt);
  }

  return normalizedWorkerStatus === 'release반영실패';
}

function matchesMainStateFilter(item: PlanItem, filter: MainStateFilter) {
  if (filter === 'all') {
    return true;
  }

  const normalizedWorkerStatus = item.workerStatus?.replace(/\s+/g, '') ?? '';

  if (filter === 'pending') {
    return item.status === '릴리즈완료' || normalizedWorkerStatus === 'main반영대기' || normalizedWorkerStatus === 'main반영중';
  }

  if (filter === 'merged') {
    return item.status === '완료' || normalizedWorkerStatus === 'main반영완료';
  }

  if (filter === 'failed') {
    return normalizedWorkerStatus === 'main반영실패';
  }

  return !item.autoDeployToMain && item.status !== '완료';
}

function matchesIssueStateFilter(item: PlanItem, filter: IssueStateFilter) {
  if (filter === 'all') {
    return true;
  }

  if (filter === 'open') {
    return item.hasOpenIssues;
  }

  return !item.hasOpenIssues;
}

function matchesCostStateFilter(item: PlanItem, filter: CostStateFilter, usageSummary: AutomationUsageSummary | null) {
  void item;

  if (filter === 'all') {
    return true;
  }

  if (filter === 'recorded') {
    return Boolean(usageSummary);
  }

  if (filter === 'none') {
    return !usageSummary;
  }

  if (!usageSummary?.tierLabel) {
    return false;
  }

  if (filter === 'stable') {
    return usageSummary.tierLabel === '안정';
  }

  if (filter === 'attention') {
    return usageSummary.tierLabel === '관심';
  }

  if (filter === 'warning') {
    return usageSummary.tierLabel === '주의';
  }

  return usageSummary.tierLabel === '높음';
}

function getFunctionCheckLabel(jangsingProcessingRequired: boolean) {
  return jangsingProcessingRequired ? '완료' : '오동작';
}

function comparePlanItemsByRecent(left: PlanItem, right: PlanItem) {
  return right.id - left.id;
}

function comparePlanItemsByAutomationPriority(left: PlanItem, right: PlanItem) {
  const leftPriority = isAutomationInProgress(left.workerStatus) ? 0 : 1;
  const rightPriority = isAutomationInProgress(right.workerStatus) ? 0 : 1;

  return leftPriority - rightPriority;
}

function getPlanFilterLabel(statusFilter: PlanFilterStatus) {
  if (statusFilter === 'all') {
    return '전체';
  }

  if (statusFilter === 'in-progress') {
    return '작업중';
  }

  if (statusFilter === 'done') {
    return '완료';
  }

  return '오류';
}

function isAutomationInProgress(workerStatus: string | null) {
  return workerStatus ? ACTIVE_WORKER_STATUSES.has(workerStatus) : false;
}

function shouldAutoRefreshAutomation(workerStatus: string | null) {
  return workerStatus ? AUTO_REFRESH_CANDIDATE_WORKER_STATUSES.has(workerStatus) : false;
}

function formatElapsedDuration(timestamp: string | null) {
  if (!timestamp) {
    return null;
  }

  const elapsedMs = Date.now() - new Date(timestamp).getTime();

  if (!Number.isFinite(elapsedMs) || elapsedMs < 0) {
    return null;
  }

  const totalMinutes = Math.floor(elapsedMs / 60000);

  if (totalMinutes < 1) {
    return '1분 미만 경과';
  }

  if (totalMinutes < 60) {
    return `${totalMinutes}분 경과`;
  }

  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;

  if (minutes === 0) {
    return `${hours}시간 경과`;
  }

  return `${hours}시간 ${minutes}분 경과`;
}

function describeAutomationWork(workerStatus: string | null) {
  if (!workerStatus) {
    return null;
  }

  const normalizedStatus = workerStatus.replace(/\s+/g, '');

  if (normalizedStatus === '브랜치생성중') {
    return '작업 브랜치를 만들고 자동 작업 준비를 진행하고 있습니다.';
  }

  if (normalizedStatus === '자동작업중') {
    return '요청 내용을 반영해 코드와 관련 파일을 수정하고 결과를 확인하고 있습니다.';
  }

  if (normalizedStatus === 'release반영중') {
    return '작업 결과를 release 브랜치에 병합하고 반영 상태를 확인하고 있습니다.';
  }

  if (normalizedStatus === 'main반영중') {
    return 'release 브랜치에 쌓인 반영 결과를 main 브랜치에 일괄 적용하고 있습니다.';
  }

  if (normalizedStatus === '브랜치준비') {
    return '브랜치 생성 작업이 시작되기 전 대기 중입니다.';
  }

  if (normalizedStatus === 'release반영대기') {
    return '개발 작업이 끝나서 release 반영 순서를 기다리고 있습니다.';
  }

  if (normalizedStatus === 'main반영대기') {
    return 'release 브랜치 기준 main 일괄 반영 순서를 기다리고 있습니다.';
  }

  if (normalizedStatus === '브랜치실패') {
    return '브랜치 생성 단계에서 문제가 발생해 재시도가 필요합니다.';
  }

  if (normalizedStatus === '자동작업실패') {
    return '코드 수정 또는 검증 단계에서 문제가 발생해 확인이 필요합니다.';
  }

  if (normalizedStatus === 'release반영실패') {
    return 'release 브랜치 반영 단계에서 문제가 발생했습니다.';
  }

  if (normalizedStatus === 'main반영실패') {
    return 'main 브랜치 일괄 반영 단계에서 문제가 발생했습니다.';
  }

  if (normalizedStatus === '자동완료' || normalizedStatus.endsWith('완료')) {
    return '자동화 단계 처리가 완료되었습니다.';
  }

  if (workerStatus.includes('검증')) {
    return `${workerStatus} 단계로 결과를 점검하고 필요한 후속 수정을 확인하고 있습니다.`;
  }

  if (workerStatus.includes('수정')) {
    return `${workerStatus} 단계로 요청 내용을 반영하는 작업을 진행하고 있습니다.`;
  }

  return `${workerStatus} 단계 작업을 진행하고 있습니다.`;
}

type AutomationTone = 'idle' | 'active' | 'success' | 'error';
type AutomationPresentation = {
  label: string;
  description: string | null;
  tone: AutomationTone;
  active: boolean;
};

function getAutomationStatusPresentation(item: Pick<PlanItem, 'status' | 'workerStatus' | 'lastError'>) {
  const workerStatus = item.workerStatus;
  const description = describeAutomationWork(workerStatus);

  if (!workerStatus) {
    return {
      label: '자동화 대기',
      description: item.status === '완료' ? '모든 자동화 단계가 끝난 상태입니다.' : '자동화 작업이 아직 시작되지 않았습니다.',
      tone: item.status === '완료' ? 'success' : 'idle',
      active: false,
    } satisfies AutomationPresentation;
  }

  if (workerStatus.includes('실패') || item.lastError) {
    return {
      label: `자동화 상태: ${workerStatus}`,
      description,
      tone: 'error',
      active: false,
    } satisfies AutomationPresentation;
  }

  if (isAutomationInProgress(workerStatus)) {
    return {
      label: `자동화 진행중: ${workerStatus}`,
      description,
      tone: 'active',
      active: true,
    } satisfies AutomationPresentation;
  }

  if (workerStatus === '자동완료' || workerStatus.endsWith('완료') || item.status === '완료') {
    return {
      label: `자동화 상태: ${workerStatus}`,
      description,
      tone: 'success',
      active: false,
    } satisfies AutomationPresentation;
  }

  return {
    label: `자동화 상태: ${workerStatus}`,
    description,
    tone: 'idle',
    active: false,
  } satisfies AutomationPresentation;
}

function extractAutomationTokenUsageText(sourceWork: Pick<PlanSourceWorkHistory, 'summary' | 'commandLog'> | null | undefined) {
  const candidates = [sourceWork?.commandLog, sourceWork?.summary];

  for (const candidate of candidates) {
    if (!candidate) {
      continue;
    }

    const line = candidate
      .split('\n')
      .map((entry) => entry.trim())
      .find((entry) => /^토큰 사용량:\s*/.test(entry));

    if (line) {
      return line.replace(/^토큰 사용량:\s*/u, '').trim();
    }
  }

  return null;
}

function isHistoryAfterReference(createdAt: string | null | undefined, referenceAt: string | null | undefined) {
  if (!createdAt) {
    return false;
  }

  if (!referenceAt) {
    return true;
  }

  return new Date(createdAt).getTime() > new Date(referenceAt).getTime();
}

function parseTokenMetricValue(valueText: string, unitText: string | undefined) {
  const normalized = Number(valueText.replace(/,/g, ''));

  if (!Number.isFinite(normalized)) {
    return null;
  }

  const unit = unitText?.toLowerCase() ?? '';

  if (unit === 'k') {
    return Math.round(normalized * 1_000);
  }

  if (unit === 'm') {
    return Math.round(normalized * 1_000_000);
  }

  return Math.round(normalized);
}

function parseAutomationTokenUsageMetrics(tokenUsageText: string) {
  const normalizedText = tokenUsageText
    .replace(/^tokens?\s+used\s*:?\s*/iu, '')
    .replace(/\(([^)]+)\)/g, ', $1')
    .trim();
  const metrics = new Map<string, number>();

  for (const match of normalizedText.matchAll(/(\d[\d,]*(?:\.\d+)?)\s*([km]?)\s*(input|output|total|cached|reasoning)\b/giu)) {
    const label = match[3]?.toLowerCase() ?? '';
    const value = parseTokenMetricValue(match[1] ?? '', match[2]);

    if (!label || value === null) {
      continue;
    }

    metrics.set(label, value);
  }

  for (const match of normalizedText.matchAll(/\b(input|output|total|cached|reasoning)\s*[:=]?\s*(\d[\d,]*(?:\.\d+)?)\s*([km]?)/giu)) {
    const label = match[1]?.toLowerCase() ?? '';
    const value = parseTokenMetricValue(match[2] ?? '', match[3]);

    if (!label || value === null) {
      continue;
    }

    metrics.set(label, value);
  }

  if (metrics.size > 0) {
    return metrics;
  }

  const fallbackMatch = normalizedText.match(/(\d[\d,]*(?:\.\d+)?)\s*([km]?)/i);

  if (!fallbackMatch) {
    return null;
  }

  const fallbackValue = parseTokenMetricValue(fallbackMatch[1], fallbackMatch[2]);

  if (fallbackValue === null) {
    return null;
  }

  return new Map([['total', fallbackValue]]);
}

function formatTokenMetricValue(value: number) {
  return new Intl.NumberFormat('ko-KR').format(value);
}

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

function summarizeAutomationTokenUsage(
  sourceWorks: Array<Pick<PlanSourceWorkHistory, 'summary' | 'commandLog'>>,
) {
  const totals = new Map<string, number>();

  sourceWorks.forEach((sourceWork) => {
    const tokenUsageText = extractAutomationTokenUsageText(sourceWork);

    if (!tokenUsageText) {
      return;
    }

    const metrics = parseAutomationTokenUsageMetrics(tokenUsageText);

    if (!metrics) {
      return;
    }

    metrics.forEach((value, key) => {
      totals.set(key, (totals.get(key) ?? 0) + value);
    });
  });

  if (totals.size === 0) {
    return null;
  }

  const entries: Array<[string, string]> = [
    ['total', '총'],
    ['input', '입력'],
    ['output', '출력'],
    ['cached', '캐시'],
    ['reasoning', '추론'],
  ];

  return entries
    .filter(([key]) => totals.has(key))
    .map(([key, label]) => `${label} ${formatTokenMetricValue(totals.get(key) ?? 0)}`)
    .join(' · ');
}

type AutomationUsageSummary = {
  costLabel: string;
  tokenLabel: string;
  description: string | null;
  tierColor: 'success' | 'processing' | 'warning' | 'error' | null;
  tierLabel: string | null;
};

function formatProcessingDurationLabel(durationSeconds: number | null | undefined) {
  const normalized = Number(durationSeconds ?? 0);

  if (!Number.isFinite(normalized) || normalized <= 0) {
    return null;
  }

  if (normalized < 60) {
    return `처리 ${Math.round(normalized)}초`;
  }

  if (normalized < 3600) {
    return `처리 ${Math.round(normalized / 60)}분`;
  }

  return `처리 ${(normalized / 3600).toFixed(1)}시간`;
}

function summarizeAutomationUsageSnapshotTokens(snapshot: PlanAutomationUsageSnapshot) {
  const tokenTotals = snapshot.tokenTotals ?? {
    total: snapshot.totalTokens,
    input: 0,
    output: 0,
    cached: 0,
    reasoning: 0,
  };
  const entries = [
    [Number(tokenTotals.total ?? snapshot.totalTokens ?? 0), '총'],
    [Number(tokenTotals.input ?? 0), '입력'],
    [Number(tokenTotals.output ?? 0), '출력'],
    [Number(tokenTotals.cached ?? 0), '캐시'],
    [Number(tokenTotals.reasoning ?? 0), '추론'],
  ] as Array<[number, string]>;
  const validEntries = entries.filter(([value]) => Number.isFinite(value) && value > 0);

  if (validEntries.length === 0) {
    if (Number(snapshot.sourceWorkCount ?? 0) > 0) {
      return '총 0';
    }
    return null;
  }

  return validEntries
    .map(([value, label]) => `${label} ${formatTokenMetricValue(value)}`)
    .join(' · ');
}

function buildAutomationUsageSummaryFromSnapshot(
  snapshot: PlanAutomationUsageSnapshot | null | undefined,
  appConfig: Pick<AppConfig, 'planCost'>,
): AutomationUsageSummary | null {
  if (!snapshot) {
    return null;
  }

  const tokenSummaryText = summarizeAutomationUsageSnapshotTokens(snapshot);
  const estimatedCost = estimateAutomationCost(
    Math.max(0, Number(snapshot.totalTokens ?? 0)),
    Math.max(0, Number(snapshot.retryCount ?? 0)),
    snapshot.processingDurationSeconds,
    appConfig.planCost.baseCostPerMillionTokens,
    appConfig.planCost.retryCostMultiplierPercent,
    appConfig.planCost.hourlyCostMultiplierPercent,
    appConfig.planCost.timeCostUnit,
  );
  const tier = getAutomationCostTier(
    estimatedCost,
    appConfig.planCost.baseCostPerMillionTokens,
    appConfig.planCost.attentionCostThresholdMultiplier,
    appConfig.planCost.warningCostThresholdMultiplier,
    appConfig.planCost.highCostThresholdMultiplier,
  );
  const descriptionParts = [
    snapshot.retryCount > 0 ? `재처리 ${snapshot.retryCount}회 포함` : null,
    snapshot.sourceWorkCount > 0 ? `증적 ${snapshot.sourceWorkCount}건` : null,
    formatProcessingDurationLabel(snapshot.processingDurationSeconds),
  ].filter(Boolean);

  if (!tokenSummaryText && snapshot.sourceWorkCount === 0 && !snapshot.processingDurationSeconds) {
    return null;
  }

  return {
    costLabel: `예상 비용 ${formatCostMetricValue(estimatedCost)}`,
    tokenLabel: tokenSummaryText ? `누적 토큰 사용량 ${tokenSummaryText}` : '누적 토큰 사용량 기록 없음',
    description: descriptionParts.length > 0 ? descriptionParts.join(' · ') : null,
    tierColor: tier.color,
    tierLabel: tier.label,
  };
}

function getAutomationTotalTokenCount(metrics: Map<string, number>) {
  const total = metrics.get('total');

  if (typeof total === 'number' && Number.isFinite(total)) {
    return total;
  }

  return ['input', 'output', 'cached', 'reasoning'].reduce((sum, key) => sum + (metrics.get(key) ?? 0), 0);
}

function estimateAutomationCost(
  totalTokens: number,
  retryCount: number,
  processingDurationSeconds: number | null | undefined,
  baseCostPerMillionTokens: number,
  retryCostMultiplierPercent: number,
  hourlyCostMultiplierPercent: number,
  timeCostUnit: AppConfig['planCost']['timeCostUnit'],
) {
  if (totalTokens <= 0) {
    return 0;
  }

  const durationSeconds = Math.max(0, Number(processingDurationSeconds ?? 0));
  const durationUnits =
    timeCostUnit === 'second' ? durationSeconds : timeCostUnit === 'minute' ? durationSeconds / 60 : durationSeconds / 3600;
  const additiveMultiplier =
    1 + retryCount * (retryCostMultiplierPercent / 100) + durationUnits * (hourlyCostMultiplierPercent / 100);

  return (totalTokens / 1_000_000) * baseCostPerMillionTokens * additiveMultiplier;
}

function getAutomationCostTier(
  cost: number,
  baseCostPerMillionTokens: number,
  attentionCostThresholdMultiplier: number,
  warningCostThresholdMultiplier: number,
  highCostThresholdMultiplier: number,
) {
  const attentionThreshold = baseCostPerMillionTokens * Math.max(0.1, attentionCostThresholdMultiplier);
  const warningThreshold = baseCostPerMillionTokens * Math.max(attentionCostThresholdMultiplier, warningCostThresholdMultiplier);
  const highThreshold = baseCostPerMillionTokens * Math.max(warningCostThresholdMultiplier, highCostThresholdMultiplier);

  if (cost <= attentionThreshold) {
    return { color: 'success', label: '안정' } as const;
  }

  if (cost <= warningThreshold) {
    return { color: 'processing', label: '관심' } as const;
  }

  if (cost <= highThreshold) {
    return { color: 'warning', label: '주의' } as const;
  }

  return { color: 'error', label: '높음' } as const;
}

function buildAutomationUsageSummary(
  sourceWorks: Array<Pick<PlanSourceWorkHistory, 'summary' | 'commandLog' | 'createdAt'>>,
  appConfig: Pick<AppConfig, 'planCost'>,
): AutomationUsageSummary | null {
  const tokenSummaryText = summarizeAutomationTokenUsage(sourceWorks);

  if (!tokenSummaryText) {
    return null;
  }

  const totals = new Map<string, number>();

  sourceWorks.forEach((sourceWork) => {
    const tokenUsageText = extractAutomationTokenUsageText(sourceWork);

    if (!tokenUsageText) {
      return;
    }

    const metrics = parseAutomationTokenUsageMetrics(tokenUsageText);

    if (!metrics) {
      return;
    }

    metrics.forEach((value, key) => {
      totals.set(key, (totals.get(key) ?? 0) + value);
    });
  });

  const totalTokens = getAutomationTotalTokenCount(totals);
  const retryCount = Math.max(0, sourceWorks.length - 1);
  const processingDurationSeconds =
    sourceWorks.length > 1
      ? Math.max(
          0,
          Math.round(
            (new Date(sourceWorks[sourceWorks.length - 1].createdAt).getTime() - new Date(sourceWorks[0].createdAt).getTime()) /
              1_000,
          ),
        )
      : null;
  const estimatedCost = estimateAutomationCost(
    totalTokens,
    retryCount,
    processingDurationSeconds,
    appConfig.planCost.baseCostPerMillionTokens,
    appConfig.planCost.retryCostMultiplierPercent,
    appConfig.planCost.hourlyCostMultiplierPercent,
    appConfig.planCost.timeCostUnit,
  );
  const tier = getAutomationCostTier(
    estimatedCost,
    appConfig.planCost.baseCostPerMillionTokens,
    appConfig.planCost.attentionCostThresholdMultiplier,
    appConfig.planCost.warningCostThresholdMultiplier,
    appConfig.planCost.highCostThresholdMultiplier,
  );

  return {
    costLabel: `예상 비용 ${formatCostMetricValue(estimatedCost)}`,
    tokenLabel: `누적 토큰 사용량 ${tokenSummaryText}`,
    description: [retryCount > 0 ? `재처리 ${retryCount}회 포함` : null, formatProcessingDurationLabel(processingDurationSeconds)]
      .filter(Boolean)
      .join(' · ') || null,
    tierColor: tier.color,
    tierLabel: tier.label,
  };
}

function buildAutomationUsageSummaryByHistoryId(
  sourceWorks: PlanSourceWorkHistory[],
  appConfig: Pick<AppConfig, 'planCost'>,
) {
  const summaries = new Map<number, AutomationUsageSummary>();
  const orderedSourceWorks = sourceWorks
    .slice()
    .sort((left, right) => new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime());
  const cumulativeSourceWorks: Array<Pick<PlanSourceWorkHistory, 'summary' | 'commandLog' | 'createdAt'>> = [];

  orderedSourceWorks.forEach((sourceWork) => {
    cumulativeSourceWorks.push(sourceWork);
    const summary = buildAutomationUsageSummary(cumulativeSourceWorks, appConfig);

    if (summary) {
      summaries.set(sourceWork.id, summary);
    }
  });

  return summaries;
}

function AutomationStatusBar({
  item,
  usageSummary,
  compact = false,
}: {
  item: Pick<PlanItem, 'status' | 'workerStatus' | 'lastError' | 'startedAt'>;
  usageSummary?: AutomationUsageSummary | null;
  compact?: boolean;
}) {
  const [elapsedLabel, setElapsedLabel] = useState(() => formatElapsedDuration(item.startedAt));

  useEffect(() => {
    setElapsedLabel(formatElapsedDuration(item.startedAt));

    if (!item.startedAt || !isAutomationInProgress(item.workerStatus)) {
      return;
    }

    const intervalId = window.setInterval(() => {
      setElapsedLabel(formatElapsedDuration(item.startedAt));
    }, 30000);

    return () => {
      window.clearInterval(intervalId);
    };
  }, [item.startedAt, item.workerStatus]);

  const presentation = getAutomationStatusPresentation(item);
  const classes = [
    'plan-board-page__automation-status',
    `plan-board-page__automation-status--${presentation.tone}`,
    compact ? 'plan-board-page__automation-status--compact' : '',
    presentation.active ? 'plan-board-page__automation-status--active' : '',
  ]
    .filter(Boolean)
    .join(' ');

  return (
    <div className={classes}>
      <span className="plan-board-page__automation-status-fill" />
      <div className="plan-board-page__automation-status-copy">
        <Text className="plan-board-page__automation-status-text">{presentation.label}</Text>
        {presentation.description || elapsedLabel ? (
          <Text className="plan-board-page__automation-status-description" type="secondary">
            {[presentation.description, presentation.active ? elapsedLabel : null].filter(Boolean).join(' · ')}
          </Text>
        ) : null}
        {usageSummary ? (
          <Space size={[8, 4]} wrap>
            <Text className="plan-board-page__automation-status-description" type="secondary">
              {usageSummary.costLabel}
            </Text>
            <Text className="plan-board-page__automation-status-description" type="secondary">
              {usageSummary.tokenLabel}
            </Text>
            {usageSummary.tierColor && usageSummary.tierLabel ? (
              <Tag color={usageSummary.tierColor}>{usageSummary.tierLabel}</Tag>
            ) : null}
            {usageSummary.description ? (
              <Text className="plan-board-page__automation-status-description" type="secondary">
                {usageSummary.description}
              </Text>
            ) : null}
          </Space>
        ) : null}
      </div>
    </div>
  );
}
