import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { POLLING } from '../lib/constants.js';
import { EmptyState } from '../components/shared/EmptyState';
import { StatChip } from '../components/shared/StatChip';
import { usePieceList } from '../hooks/usePieces';
import { resolvePieceOptions } from '../lib/splitPieces';
import { fetchMyOrgs, listBrowserSessionProfiles, type Visibility } from '../api';
import { useAuthState } from '../App';

type TaskKind = 'agent' | 'script';

interface ScheduledTask {
  id: number;
  title: string | null;
  body: string;
  pieceName: string;
  profile: string;
  outputFormat: string;
  cronExpression: string;
  nextRunAt: string;
  lastRunAt: string | null;
  lastJobId: string | null;
  isActive: boolean;
  ownerId: string | null;
  ownerName: string | null;
  visibility: Visibility;
  visibilityScopeOrgId: string | null;
  visibilityScopeOrgName: string | null;
  browserSessionProfileId: number | null;
  taskKind: TaskKind;
  scriptName: string | null;
  scriptParams: string | null;
  createdAt: string;
}

interface ScheduleFormState {
  title: string;
  body: string;
  piece: string;
  scheduleType: string;
  hour: number;
  minute: number;
  dayOfWeek: number;
  dayOfMonth: number;
  cronExpression: string;
  scheduledAt: string;
  outputFormat: string;
  visibility: Visibility;
  visibilityScopeOrgId: string | null;
  browserSessionProfileId: number | null;
  taskKind: TaskKind;
  scriptName: string;
  scriptParams: string;
}

type ScheduleFilter = 'all' | 'active' | 'paused';
type DetailMode = 'view' | 'edit' | 'new';

const DAYS = ['日', '月', '火', '水', '木', '金', '土'];
const DAY_OPTIONS = ['日曜', '月曜', '火曜', '水曜', '木曜', '金曜', '土曜'];

const SCHEDULE_TYPE_OPTIONS: Array<{ value: string; label: string; hint: string }> = [
  { value: 'daily', label: '毎日', hint: '指定した時刻に毎日' },
  { value: 'weekly', label: '毎週', hint: '曜日と時刻を指定' },
  { value: 'monthly', label: '毎月', hint: '日付と時刻を指定' },
  { value: 'cron', label: 'Cron', hint: 'Cron 式で柔軟に指定' },
  { value: 'once', label: '一回', hint: '指定した日時に1回のみ' },
];

const OUTPUT_FORMAT_OPTIONS = [
  { value: 'markdown', label: 'markdown' },
  { value: 'plain', label: 'plain' },
  { value: 'json', label: 'json' },
];

function parseCronToDisplay(cron: string): string {
  if (cron === 'once') return '一回のみ';
  const parts = cron.split(' ');
  if (parts.length !== 5) return cron;
  const [min, hour, dom, , dow] = parts;
  const hhmm = `${hour}:${min.padStart(2, '0')}`;

  if (dom !== '*' && dow === '*') return `毎月${dom}日 ${hhmm} (UTC)`;
  if (dow !== '*' && dom === '*') return `毎週${DAYS[Number(dow)] ?? dow}曜 ${hhmm} (UTC)`;
  if (dom === '*' && dow === '*') return `毎日 ${hhmm} (UTC)`;
  return cron;
}

function cronToFormState(cron: string): Pick<ScheduleFormState, 'scheduleType' | 'hour' | 'minute' | 'dayOfWeek' | 'dayOfMonth' | 'cronExpression'> {
  if (cron === 'once') return { scheduleType: 'once', hour: 9, minute: 0, dayOfWeek: 1, dayOfMonth: 1, cronExpression: '' };
  const parts = cron.split(' ');
  if (parts.length !== 5) return { scheduleType: 'cron', hour: 9, minute: 0, dayOfWeek: 1, dayOfMonth: 1, cronExpression: cron };
  const [min, hour, dom, , dow] = parts;
  const h = Number(hour), m = Number(min);

  if (dom !== '*' && dow === '*') return { scheduleType: 'monthly', hour: h, minute: m, dayOfWeek: 1, dayOfMonth: Number(dom), cronExpression: '' };
  if (dow !== '*' && dom === '*') return { scheduleType: 'weekly', hour: h, minute: m, dayOfWeek: Number(dow), dayOfMonth: 1, cronExpression: '' };
  if (dom === '*' && dow === '*') return { scheduleType: 'daily', hour: h, minute: m, dayOfWeek: 1, dayOfMonth: 1, cronExpression: '' };
  return { scheduleType: 'cron', hour: h, minute: m, dayOfWeek: 1, dayOfMonth: 1, cronExpression: cron };
}

function taskToFormState(task: ScheduledTask): ScheduleFormState {
  const cronState = cronToFormState(task.cronExpression);
  return {
    title: task.title ?? '',
    body: task.body,
    piece: task.pieceName || 'auto',
    outputFormat: task.outputFormat || 'markdown',
    scheduledAt: '',
    visibility: task.visibility,
    visibilityScopeOrgId: task.visibilityScopeOrgId,
    browserSessionProfileId: task.browserSessionProfileId ?? null,
    taskKind: task.taskKind,
    scriptName: task.scriptName ?? '',
    scriptParams: task.scriptParams ?? '',
    ...cronState,
  };
}

function blankFormState(visibility: Visibility = 'private', orgId: string | null = null): ScheduleFormState {
  return {
    title: '',
    body: '',
    piece: 'auto',
    scheduleType: 'daily',
    hour: 9,
    minute: 0,
    dayOfWeek: 1,
    dayOfMonth: 1,
    cronExpression: '',
    scheduledAt: '',
    outputFormat: 'markdown',
    visibility,
    visibilityScopeOrgId: orgId,
    browserSessionProfileId: null,
    taskKind: 'agent',
    scriptName: '',
    scriptParams: '',
  };
}

function formatDate(iso: string | null): string {
  if (!iso) return '—';
  return new Date(iso).toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}

function relativeFromNow(iso: string | null): string {
  if (!iso) return '—';
  const diff = new Date(iso).getTime() - Date.now();
  const abs = Math.abs(diff);
  const mins = Math.round(abs / 60000);
  const hrs = Math.round(mins / 60);
  const days = Math.round(hrs / 24);
  const unit = mins < 60 ? `${mins}分` : hrs < 24 ? `${hrs}時間` : `${days}日`;
  return diff >= 0 ? `${unit}後` : `${unit}前`;
}

function buildCronPreview(form: ScheduleFormState): string {
  const { scheduleType, hour, minute, dayOfWeek, dayOfMonth, cronExpression } = form;
  const hh = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
  if (scheduleType === 'daily') return `毎日 ${hh} (UTC)`;
  if (scheduleType === 'weekly') return `毎週${DAY_OPTIONS[dayOfWeek] ?? ''} ${hh} (UTC)`;
  if (scheduleType === 'monthly') return `毎月${dayOfMonth}日 ${hh} (UTC)`;
  if (scheduleType === 'cron') return parseCronToDisplay(cronExpression || '');
  if (scheduleType === 'once') return '一回のみ';
  return '';
}

async function fetchScheduledTasks(): Promise<ScheduledTask[]> {
  const res = await fetch('/api/scheduled-tasks');
  if (res.status === 401 || res.status === 403 || res.status === 404) return [];
  if (!res.ok) throw new Error('Failed to fetch scheduled tasks');
  const data = await res.json();
  return data.tasks ?? [];
}

interface SchedulesPageProps {
  showToast?: (message: string, variant?: 'success' | 'error') => void;
}

export function SchedulesPage({ showToast }: SchedulesPageProps = {}) {
  const qc = useQueryClient();
  const { data: tasks = [], isLoading } = useQuery({
    queryKey: ['scheduledTasks'],
    queryFn: fetchScheduledTasks,
    refetchInterval: POLLING.VERY_SLOW,
  });

  const notifyError = (label: string, err: unknown) => {
    const msg = `${label}: ${err instanceof Error ? err.message : String(err)}`;
    if (showToast) showToast(msg, 'error');
    else console.error(msg);
  };

  const [filter, setFilter] = useState<ScheduleFilter>('all');
  const [search, setSearch] = useState('');
  const [activeId, setActiveId] = useState<number | null>(null);
  const [mode, setMode] = useState<DetailMode>('view');
  // Mobile single-column flow: list ↔ detail toggle. On sm+ both panes
  // are visible side-by-side and this flag is ignored. Defaults to list
  // (false) so the user lands on the overview.
  const [mobileShowDetail, setMobileShowDetail] = useState(false);

  const counts = useMemo(() => ({
    all: tasks.length,
    active: tasks.filter(t => t.isActive).length,
    paused: tasks.filter(t => !t.isActive).length,
  }), [tasks]);

  const filtered = useMemo(() => {
    return tasks.filter(t => {
      if (filter === 'active' && !t.isActive) return false;
      if (filter === 'paused' && t.isActive) return false;
      if (search) {
        const hay = `${t.title ?? ''} ${t.body}`.toLowerCase();
        if (!hay.includes(search.toLowerCase())) return false;
      }
      return true;
    });
  }, [tasks, filter, search]);

  const active = useMemo(() => {
    if (mode === 'new') return null;
    if (activeId != null) {
      const found = tasks.find(t => t.id === activeId);
      if (found) return found;
    }
    return filtered[0] ?? null;
  }, [tasks, filtered, activeId, mode]);

  useEffect(() => {
    if (mode === 'new') return;
    if (active && activeId !== active.id) setActiveId(active.id);
  }, [active, activeId, mode]);

  const toggleMutation = useMutation({
    mutationFn: async ({ id, isActive }: { id: number; isActive: boolean }) => {
      const res = await fetch(`/api/scheduled-tasks/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ isActive }),
      });
      if (!res.ok) throw new Error(`HTTP ${res.status} ${await res.text().catch(() => '')}`.trim());
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['scheduledTasks'] }),
    onError: (err) => notifyError('スケジュール更新に失敗', err),
  });

  const deleteMutation = useMutation({
    mutationFn: async (id: number) => {
      const res = await fetch(`/api/scheduled-tasks/${id}`, { method: 'DELETE' });
      if (!res.ok) throw new Error(`HTTP ${res.status} ${await res.text().catch(() => '')}`.trim());
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['scheduledTasks'] }),
    onError: (err) => notifyError('スケジュール削除に失敗', err),
  });

  const triggerMutation = useMutation({
    mutationFn: async (id: number) => {
      const res = await fetch(`/api/scheduled-tasks/${id}/trigger`, { method: 'POST' });
      if (!res.ok) throw new Error(`HTTP ${res.status} ${await res.text().catch(() => '')}`.trim());
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['scheduledTasks'] }),
    onError: (err) => notifyError('スケジュール手動実行に失敗', err),
  });

  const clearFilters = () => {
    setSearch('');
    setFilter('all');
  };

  const handleNew = () => {
    setMode('new');
    setMobileShowDetail(true);
  };

  const handleEdit = () => {
    if (active) setMode('edit');
  };

  const handleCancelEdit = () => {
    // On mobile, cancelling from 'new' means user backs out of creation
    // entirely; they didn't want to see some random task's detail.
    if (mode === 'new') setMobileShowDetail(false);
    setMode('view');
  };

  const handleSaved = async (newId?: number) => {
    await qc.invalidateQueries({ queryKey: ['scheduledTasks'] });
    if (newId != null) setActiveId(newId);
    setMode('view');
  };

  const handleSelect = (id: number) => {
    setActiveId(id);
    setMode('view');
    setMobileShowDetail(true);
  };

  const handleMobileBack = () => {
    setMode('view');
    setMobileShowDetail(false);
  };

  return (
    <div className="flex h-full min-h-0">
      <div className={`${mobileShowDetail ? 'hidden sm:flex' : 'flex'} w-full sm:w-[320px] flex-shrink-0 sm:border-r sm:border-hairline bg-white p-3 flex-col min-h-0`}>
        <ScheduleListPane
          tasks={filtered}
          activeId={mode === 'new' ? null : active?.id ?? null}
          highlightNew={mode === 'new'}
          counts={counts}
          search={search}
          setSearch={setSearch}
          filter={filter}
          setFilter={setFilter}
          onSelect={handleSelect}
          onOpenCreate={handleNew}
          onClearFilters={clearFilters}
          isLoading={isLoading}
        />
      </div>
      <div className={`${mobileShowDetail ? 'flex' : 'hidden sm:flex'} flex-1 min-w-0 bg-white flex-col`}>
        <ScheduleDetailPane
          task={active}
          mode={mode}
          onNew={handleNew}
          onEdit={handleEdit}
          onCancelEdit={handleCancelEdit}
          onSaved={handleSaved}
          onMobileBack={handleMobileBack}
          onTrigger={(id) => triggerMutation.mutate(id)}
          onToggle={(id, isActive) => toggleMutation.mutate({ id, isActive })}
          triggeringId={triggerMutation.isPending ? (triggerMutation.variables ?? null) : null}
          togglingId={toggleMutation.isPending ? (toggleMutation.variables?.id ?? null) : null}
          onDelete={(id) => {
            if (confirm('このスケジュールを削除しますか？')) {
              deleteMutation.mutate(id);
              if (activeId === id) setActiveId(null);
              setMobileShowDetail(false);
            }
          }}
        />
      </div>
    </div>
  );
}

interface ScheduleListPaneProps {
  tasks: ScheduledTask[];
  activeId: number | null;
  highlightNew: boolean;
  counts: { all: number; active: number; paused: number };
  search: string;
  setSearch: (s: string) => void;
  filter: ScheduleFilter;
  setFilter: (f: ScheduleFilter) => void;
  onSelect: (id: number) => void;
  onOpenCreate: () => void;
  onClearFilters: () => void;
  isLoading: boolean;
}

function ScheduleListPane({
  tasks, activeId, highlightNew, counts, search, setSearch, filter, setFilter,
  onSelect, onOpenCreate, onClearFilters, isLoading,
}: ScheduleListPaneProps) {
  const hasFilters = !!search || filter !== 'all';

  return (
    <div className="flex flex-col h-full overflow-hidden">
      <button
        type="button"
        onClick={onOpenCreate}
        aria-pressed={highlightNew}
        className={`w-full mb-3 px-3 py-2 rounded-md text-xs font-semibold inline-flex items-center justify-center gap-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
          highlightNew
            ? 'bg-accent-deep text-accent-fg ring-2 ring-accent-ring'
            : 'bg-accent hover:bg-accent-deep text-accent-fg'
        }`}
      >
        <svg
          width="13"
          height="13"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth={2.25}
          strokeLinecap="round"
          strokeLinejoin="round"
          aria-hidden="true"
        >
          <path d="M12 5v14M5 12h14" />
        </svg>
        新しいスケジュール
      </button>

      <div className="flex items-center gap-3 text-[10px] text-slate-500 px-0.5 pb-2.5 font-mono tabular-nums">
        <span><span className="font-semibold text-slate-700">{counts.all}</span> 件</span>
        {(counts.active > 0 || counts.paused > 0) && (
          <>
            <span aria-hidden="true" className="text-slate-300">·</span>
            <span><span className="font-semibold text-emerald-600">{counts.active}</span> 有効</span>
            {counts.paused > 0 && (
              <span><span className="font-semibold text-slate-600">{counts.paused}</span> 停止中</span>
            )}
          </>
        )}
      </div>

      <div className="flex flex-col gap-2 pb-3 border-b border-hairline">
        <div className="flex items-center gap-1.5 bg-white border border-hairline rounded-md pl-2.5 pr-1 h-8">
          <svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
          </svg>
          <input
            aria-label="検索"
            value={search}
            onChange={e => setSearch(e.target.value)}
            placeholder="検索..."
            className="flex-1 bg-transparent border-0 outline-none text-[13px] text-slate-900 placeholder:text-slate-400 min-w-0"
          />
        </div>
        <div role="tablist" aria-label="スケジュールフィルター" className="flex gap-1 overflow-x-auto pb-1 scrollbar-none">
          {([
            ['all', 'すべて', counts.all],
            ['active', '有効', counts.active],
            ['paused', '停止中', counts.paused],
          ] as const).map(([key, label, n]) => (
            <button
              key={key}
              type="button"
              role="tab"
              aria-selected={filter === key}
              onClick={() => setFilter(key as ScheduleFilter)}
              className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
                filter === key
                  ? 'border-accent/60 bg-accent-soft text-accent font-semibold'
                  : 'border-hairline bg-white text-slate-600 hover:bg-surface'
              }`}
            >
              {label} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{n}</span>
            </button>
          ))}
        </div>
      </div>

      <div className="flex flex-col gap-1.5 mt-2 overflow-y-auto flex-1 min-h-0 pr-0.5">
        {isLoading && <div className="text-[13px] text-slate-500 px-2 py-3">読み込み中...</div>}
        {!isLoading && tasks.length === 0 && (
          hasFilters ? (
            <EmptyState
              compact
              title="該当するスケジュールはありません"
              hint="検索やフィルタを変えてみてください。"
              action={
                <button
                  type="button"
                  onClick={onClearFilters}
                  className="px-3 py-1.5 rounded-md text-xs font-semibold bg-white border border-hairline text-slate-700 hover:border-hairline transition-colors"
                >
                  フィルタをクリア
                </button>
              }
            />
          ) : (
            <EmptyState
              compact
              title="スケジュールがありません"
              hint="「新しいスケジュール」から登録できます。"
            />
          )
        )}
        {!isLoading && tasks.map(task => (
          <ScheduleListItem
            key={task.id}
            task={task}
            active={activeId === task.id}
            onClick={() => onSelect(task.id)}
          />
        ))}
      </div>
    </div>
  );
}

function ScheduleListItem({ task, active, onClick }: { task: ScheduledTask; active: boolean; onClick: () => void }) {
  const authState = useAuthState();
  const showOwnership = authState.mode === 'authenticated';
  return (
    <button
      type="button"
      onClick={onClick}
      aria-current={active ? 'true' : undefined}
      className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
        active ? 'border-accent/60 bg-accent-soft' : 'border-hairline bg-white hover:bg-surface'
      }`}
    >
      <div className="flex items-center gap-2 min-w-0">
        <span aria-hidden="true" className={`inline-block w-1.5 h-1.5 rounded-full flex-shrink-0 ${task.isActive ? 'bg-emerald-500' : 'bg-slate-300'}`} />
        <span className={`flex-1 min-w-0 truncate text-[13px] font-semibold ${task.isActive ? 'text-slate-900' : 'text-slate-500'}`}>
          {task.title || 'タイトルなし'}
        </span>
      </div>
      <div className="mt-1 text-2xs text-slate-600 truncate leading-snug">
        {parseCronToDisplay(task.cronExpression)}
      </div>
      <div className="mt-1 text-[10px] text-slate-400 font-mono tabular-nums">
        {task.isActive
          ? task.nextRunAt
            ? `次回: ${formatDate(task.nextRunAt)} (${relativeFromNow(task.nextRunAt)})`
            : '次回: 未定'
          : '停止中'}
      </div>
      {showOwnership && (
        <div className="mt-1.5 flex items-center gap-1.5 text-[10px] flex-wrap">
          {task.ownerId ? (
            <span className="text-slate-600">{task.ownerName ?? 'user'}</span>
          ) : (
            <span className="text-slate-400">system</span>
          )}
          {task.visibility === 'private' && (
            <span className="px-1 rounded text-[10px] font-medium bg-amber-50 text-amber-700 border border-amber-100" title="非公開">🔒 非公開</span>
          )}
          {task.visibility === 'org' && (
            <span className="px-1 rounded text-[10px] font-medium bg-blue-50 text-blue-700 border border-blue-100" title={`${task.visibilityScopeOrgName ?? 'org'} と共有`}>
              🏢 {task.visibilityScopeOrgName ?? 'org'}
            </span>
          )}
          {task.visibility === 'public' && (
            <span className="px-1 rounded text-[10px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100" title="公開">🌐 公開</span>
          )}
        </div>
      )}
    </button>
  );
}

interface ScheduleDetailPaneProps {
  task: ScheduledTask | null;
  mode: DetailMode;
  onNew: () => void;
  onEdit: () => void;
  onCancelEdit: () => void;
  onSaved: (newId?: number) => void | Promise<void>;
  /** Mobile-only callback to return to the list pane. Hidden on sm+. */
  onMobileBack?: () => void;
  onTrigger: (id: number) => void;
  onToggle: (id: number, isActive: boolean) => void;
  onDelete: (id: number) => void;
  /** Task id currently being triggered (for optimistic "実行中..." state). */
  triggeringId?: number | null;
  togglingId?: number | null;
}

function ScheduleDetailPane({
  task, mode, onNew, onEdit, onCancelEdit, onSaved, onMobileBack, onTrigger, onToggle, onDelete,
  triggeringId, togglingId,
}: ScheduleDetailPaneProps) {
  const authState = useAuthState();
  const showOwnership = authState.mode === 'authenticated';
  if (mode === 'new') {
    return (
      <ScheduleEditor
        mode="new"
        initialTask={null}
        onCancel={onCancelEdit}
        onSaved={onSaved}
      />
    );
  }

  if (mode === 'edit' && task) {
    return (
      <ScheduleEditor
        mode="edit"
        initialTask={task}
        onCancel={onCancelEdit}
        onSaved={onSaved}
      />
    );
  }

  if (!task) {
    return (
      <div className="h-full flex items-center justify-center p-10">
        <EmptyState
          title="スケジュールを選択してください"
          hint="左のリストから表示・編集したいスケジュールを開きます。"
          action={
            <button
              type="button"
              onClick={onNew}
              className="px-3 py-1.5 rounded-md text-xs font-semibold bg-accent hover:bg-accent-deep text-accent-fg transition-colors"
            >
              ＋ 新しいスケジュール
            </button>
          }
        />
      </div>
    );
  }

  return (
    <div className="flex flex-col h-full overflow-hidden">
      <div className="flex-shrink-0 px-3 sm:px-5 py-3 sm:py-3.5 border-b border-hairline bg-white flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3">
        <div className="flex items-center gap-2 sm:gap-2.5 min-w-0">
          {onMobileBack && (
            <button
              type="button"
              onClick={onMobileBack}
              aria-label="一覧に戻る"
              className="sm:hidden inline-flex items-center justify-center w-8 h-8 -ml-1 rounded-md text-slate-500 hover:text-slate-900 hover:bg-surface-2 transition-colors"
            >
              <svg className="w-4 h-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
                <path d="M10 4l-4 4 4 4" />
              </svg>
            </button>
          )}
          <span aria-hidden="true" className={`inline-block w-2 h-2 rounded-full flex-shrink-0 ${task.isActive ? 'bg-emerald-500' : 'bg-slate-300'}`} />
          <div className="section-label font-mono flex-shrink-0 hidden sm:block">
            SCHEDULE #{task.id}
          </div>
          <div className="text-sm sm:text-sm font-semibold text-slate-900 truncate">
            {task.title || 'タイトルなし'}
          </div>
        </div>
        <div className="flex items-center gap-1.5 flex-shrink-0 overflow-x-auto scrollbar-none -mx-1 px-1 sm:mx-0 sm:px-0">
          {(() => {
            const isTriggering = triggeringId === task.id;
            const isToggling = togglingId === task.id;
            return (
              <>
                <button
                  type="button"
                  onClick={() => onTrigger(task.id)}
                  disabled={isTriggering}
                  className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-semibold bg-white border border-accent text-accent hover:bg-accent-soft active:scale-[0.97] active:bg-accent-soft transition-[transform,background-color,color] duration-100 inline-flex items-center gap-1 whitespace-nowrap disabled:opacity-70 disabled:cursor-wait"
                >
                  {isTriggering ? (
                    <span className="inline-block w-3 h-3 border-2 border-accent border-t-transparent rounded-full animate-spin" aria-hidden="true" />
                  ) : (
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                      <path d="M8 5v14l11-7z" />
                    </svg>
                  )}
                  {isTriggering ? '実行中...' : '今すぐ実行'}
                </button>
                <button
                  type="button"
                  onClick={() => onToggle(task.id, !task.isActive)}
                  disabled={isToggling}
                  className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-white border border-hairline text-slate-700 hover:bg-surface active:scale-[0.97] active:bg-surface transition-[transform,background-color,color] duration-100 whitespace-nowrap disabled:opacity-70 disabled:cursor-wait"
                >
                  {isToggling ? '...' : (task.isActive ? '停止' : '再開')}
                </button>
                <button
                  type="button"
                  onClick={onEdit}
                  className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-white border border-hairline text-slate-700 hover:bg-surface active:scale-[0.97] active:bg-surface transition-[transform,background-color,color] duration-100 whitespace-nowrap"
                >
                  編集
                </button>
                <button
                  type="button"
                  onClick={() => onDelete(task.id)}
                  className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-white border border-red-200 text-red-700 hover:bg-red-50 active:scale-[0.97] active:bg-red-50 transition-[transform,background-color,color] duration-100 whitespace-nowrap"
                >
                  削除
                </button>
              </>
            );
          })()}
        </div>
      </div>

      <div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 sm:py-5 bg-surface">
        <div className="max-w-[640px] mx-auto">
          <div className="flex gap-2.5 mb-5 flex-wrap">
            <StatChip label="種別" value={task.taskKind === 'script' ? `Script (${task.scriptName ?? '?'})` : 'Agent'} />
            <StatChip label="スケジュール" value={parseCronToDisplay(task.cronExpression)} />
            {task.taskKind === 'agent' && <StatChip label="ピース" value={task.pieceName} />}
            <StatChip
              label={task.isActive ? '次回実行' : 'ステータス'}
              value={task.isActive ? (task.nextRunAt ? relativeFromNow(task.nextRunAt) : '—') : '停止中'}
            />
          </div>

          <div className="bg-white border border-hairline rounded-md p-5">
            <div className="section-label mb-3.5">
              基本情報
            </div>
            <dl className="grid grid-cols-1 gap-3 text-[13px]">
              <div>
                <dt className="text-2xs font-medium text-slate-500 mb-1">タイトル</dt>
                <dd className="text-slate-900">{task.title || <span className="text-slate-400">タイトルなし</span>}</dd>
              </div>
              <div>
                <dt className="text-2xs font-medium text-slate-500 mb-1">プロンプト</dt>
                <dd className="text-slate-700 whitespace-pre-wrap break-words">{task.body}</dd>
              </div>
              <div className="grid grid-cols-2 gap-3">
                <div>
                  <dt className="text-2xs font-medium text-slate-500 mb-1">ピース</dt>
                  <dd className="text-slate-900 font-mono">{task.pieceName}</dd>
                </div>
                <div>
                  <dt className="text-2xs font-medium text-slate-500 mb-1">出力フォーマット</dt>
                  <dd className="text-slate-900 font-mono">{task.outputFormat || 'markdown'}</dd>
                </div>
              </div>
              {showOwnership && (
                <div className="grid grid-cols-2 gap-3">
                  <div>
                    <dt className="text-2xs font-medium text-slate-500 mb-1">所有者</dt>
                    <dd className="text-slate-900">
                      {task.ownerId ? (task.ownerName ?? 'user') : <span className="text-slate-400">system</span>}
                    </dd>
                  </div>
                  <div>
                    <dt className="text-2xs font-medium text-slate-500 mb-1">公開範囲</dt>
                    <dd className="text-slate-900">
                      {task.visibility === 'private' && '🔒 非公開'}
                      {task.visibility === 'org' && `🏢 ${task.visibilityScopeOrgName ?? 'org'}`}
                      {task.visibility === 'public' && '🌐 公開'}
                    </dd>
                  </div>
                </div>
              )}
            </dl>
          </div>

          <div className="bg-white border border-hairline rounded-md p-5 mt-4">
            <div className="section-label mb-3.5">
              スケジュール
            </div>
            <dl className="grid grid-cols-1 gap-3 text-[13px]">
              <div>
                <dt className="text-2xs font-medium text-slate-500 mb-1">Cron 式</dt>
                <dd className="text-slate-900 font-mono">{task.cronExpression || '—'}</dd>
              </div>
              <div className="px-3 py-2.5 bg-surface border border-hairline rounded-md text-xs text-slate-600">
                <b className="font-semibold text-slate-900">{parseCronToDisplay(task.cronExpression)}</b>
                {task.nextRunAt && (
                  <>
                    <span aria-hidden="true" className="mx-1.5 text-slate-300">·</span>
                    次回 {formatDate(task.nextRunAt)} ({relativeFromNow(task.nextRunAt)})
                  </>
                )}
              </div>
              <div className="grid grid-cols-2 gap-3">
                <div>
                  <dt className="text-2xs font-medium text-slate-500 mb-1">前回実行</dt>
                  <dd className="text-slate-700">{formatDate(task.lastRunAt)}</dd>
                </div>
                <div>
                  <dt className="text-2xs font-medium text-slate-500 mb-1">作成日時</dt>
                  <dd className="text-slate-700">{formatDate(task.createdAt)}</dd>
                </div>
              </div>
            </dl>
          </div>

          <div className="h-10" />
        </div>
      </div>
    </div>
  );
}

function EditorFormRow({
  label, help, children,
}: {
  label: string;
  help?: string;
  children: React.ReactNode;
}) {
  return (
    <label className="block">
      <div className="text-2xs font-semibold text-slate-600 mb-1">{label}</div>
      {children}
      {help && <div className="text-[10px] text-slate-400 mt-1">{help}</div>}
    </label>
  );
}

const EDITOR_INPUT_CLASS =
  'w-full px-3 py-2 border border-hairline rounded-md text-[13px] outline-none focus:border-accent focus:ring-2 focus:ring-accent-ring transition-colors';

interface ScheduleEditorProps {
  mode: 'new' | 'edit';
  initialTask: ScheduledTask | null;
  onCancel: () => void;
  onSaved: (newId?: number) => void | Promise<void>;
}

function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditorProps) {
  const isEdit = mode === 'edit';
  const authState = useAuthState();
  const defaultVis: Visibility =
    (authState.mode === 'authenticated' ? authState.user?.defaultVisibility : undefined) ?? 'private';
  const savedOrgId: string | null =
    (authState.mode === 'authenticated' ? authState.user?.defaultVisibilityOrgId : undefined) ?? null;
  const { data: orgs = [] } = useQuery({
    queryKey: ['my-orgs'],
    queryFn: fetchMyOrgs,
    staleTime: 5 * 60 * 1000,
  });
  const { data: sessionProfiles = [] } = useQuery({
    queryKey: ['browser-session-profiles'],
    queryFn: listBrowserSessionProfiles,
    staleTime: 60 * 1000,
  });
  const activeSessionProfiles = sessionProfiles.filter(p => p.status === 'active');
  const [form, setForm] = useState<ScheduleFormState>(
    initialTask ? taskToFormState(initialTask) : blankFormState(defaultVis, savedOrgId)
  );
  const [error, setError] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const titleRef = useRef<HTMLInputElement | null>(null);
  const { data: pieces = [] } = usePieceList();

  useEffect(() => {
    setForm(initialTask ? taskToFormState(initialTask) : blankFormState(defaultVis, savedOrgId));
    setError('');
  }, [initialTask, defaultVis, savedOrgId]);

  // Backfill scope when orgs load asynchronously (mirrors CreateTaskDialog)
  useEffect(() => {
    if (form.visibilityScopeOrgId !== null) return;
    if (orgs.length === 0) return;
    setForm(p => ({ ...p, visibilityScopeOrgId: orgs[0].orgId }));
  }, [orgs, form.visibilityScopeOrgId]);

  useEffect(() => {
    titleRef.current?.focus();
  }, []);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onCancel();
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [onCancel]);

  const pieceOptions = useMemo(() => {
    const opts = [{ value: 'auto', label: 'auto', description: 'LLM が自動選択' }];
    for (const p of resolvePieceOptions(pieces)) {
      if (p.name === 'auto') continue;
      opts.push({ value: p.name, label: p.name, description: p.description ?? '' });
    }
    return opts;
  }, [pieces]);

  const preview = buildCronPreview(form);

  const handleSubmit = async () => {
    if (form.taskKind === 'agent' && !form.body.trim()) {
      setError('プロンプトは必須です');
      return;
    }
    if (form.taskKind === 'script' && !form.scriptName.trim()) {
      setError('スクリプト名は必須です');
      return;
    }
    if (form.taskKind === 'script' && form.scriptParams.trim()) {
      try {
        const parsed = JSON.parse(form.scriptParams);
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
          throw new Error('params must be a JSON object');
        }
      } catch (e) {
        setError(`scriptParams が JSON object として読めません: ${e instanceof Error ? e.message : String(e)}`);
        return;
      }
    }
    try {
      setSubmitting(true);
      setError('');
      const payload: Record<string, unknown> = {
        ...form,
        visibilityScopeOrgId: form.visibility === 'org' ? form.visibilityScopeOrgId : null,
        scriptParams: form.taskKind === 'script' && form.scriptParams.trim() ? form.scriptParams : null,
      };
      if (form.scheduleType === 'once' && form.scheduledAt) {
        payload.scheduledAt = new Date(form.scheduledAt).toISOString();
      }

      if (isEdit && initialTask) {
        const res = await fetch(`/api/scheduled-tasks/${initialTask.id}`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });
        if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error ?? '保存に失敗しました'); }
        await onSaved(initialTask.id);
      } else {
        const res = await fetch('/api/scheduled-tasks', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });
        if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error ?? '作成に失敗しました'); }
        const body = await res.json().catch(() => ({}));
        const newId = body?.task?.id ?? body?.id ?? undefined;
        await onSaved(typeof newId === 'number' ? newId : undefined);
      }
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="flex flex-col h-full overflow-hidden">
      <div className="flex-shrink-0 px-5 py-3.5 border-b border-hairline bg-white flex items-center justify-between gap-3">
        <div className="min-w-0">
          <div className="flex items-center gap-2.5 min-w-0">
            <div className="section-label font-mono flex-shrink-0">
              {isEdit && initialTask ? `SCHEDULE #${initialTask.id}` : 'NEW'}
            </div>
            <div className="text-sm font-semibold text-slate-900 truncate">
              {isEdit ? 'スケジュール編集' : '新しいスケジュール'}
            </div>
          </div>
          <div className="text-2xs text-slate-500 mt-0.5">
            定期実行するタスクの内容とタイミングを指定します。
          </div>
        </div>
        <button
          type="button"
          onClick={onCancel}
          aria-label="閉じる"
          className="w-8 h-8 inline-flex items-center justify-center rounded-md text-slate-400 hover:text-slate-600 hover:bg-surface-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring flex-shrink-0"
        >
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M18 6L6 18M6 6l12 12" />
          </svg>
        </button>
      </div>

      <div className="flex-1 overflow-y-auto px-6 py-5 bg-surface">
        <div className="max-w-[640px] mx-auto space-y-4">
          <div className="bg-white border border-hairline rounded-md p-5">
            <div className="section-label mb-3.5">
              基本情報
            </div>
            <div className="space-y-3">
              <EditorFormRow label="種別" help="Agent: LLM が piece を実行 / Script: 登録済 user script を直接呼び出し (LLM 不要)">
                <div className="flex gap-1.5">
                  {(['agent', 'script'] as const).map(k => {
                    const selected = form.taskKind === k;
                    return (
                      <button
                        key={k}
                        type="button"
                        onClick={() => setForm(p => ({ ...p, taskKind: k }))}
                        aria-pressed={selected}
                        className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors ${
                          selected ? 'border-accent bg-accent-soft text-accent' : 'border-hairline bg-white text-slate-600'
                        }`}
                      >
                        {k === 'agent' ? 'Agent (LLM)' : 'Script (直接)'}
                      </button>
                    );
                  })}
                </div>
              </EditorFormRow>

              <EditorFormRow label="タイトル">
                <input
                  ref={titleRef}
                  value={form.title}
                  onChange={e => setForm(p => ({ ...p, title: e.target.value }))}
                  className={EDITOR_INPUT_CLASS}
                  placeholder={form.taskKind === 'script' ? '日次レポート生成' : '週次ニュースまとめ'}
                />
              </EditorFormRow>

              {form.taskKind === 'agent' ? (
                <>
                  <EditorFormRow label="プロンプト" help="エージェントに送るメッセージ">
                    <textarea
                      value={form.body}
                      onChange={e => setForm(p => ({ ...p, body: e.target.value }))}
                      rows={5}
                      className={`${EDITOR_INPUT_CLASS} resize-y leading-relaxed`}
                      placeholder="実行するプロンプトを入力"
                    />
                  </EditorFormRow>

                  <div className="grid grid-cols-2 gap-3">
                    <EditorFormRow
                      label="Piece"
                      help={pieceOptions.find(o => o.value === form.piece)?.description || undefined}
                    >
                      <select
                        value={form.piece}
                        onChange={e => setForm(p => ({ ...p, piece: e.target.value }))}
                        className={`${EDITOR_INPUT_CLASS} font-mono`}
                      >
                        {pieceOptions.map(o => (
                          <option key={o.value} value={o.value}>{o.label}</option>
                        ))}
                      </select>
                    </EditorFormRow>
                    <EditorFormRow label="出力フォーマット">
                      <select
                        value={form.outputFormat}
                        onChange={e => setForm(p => ({ ...p, outputFormat: e.target.value }))}
                        className={`${EDITOR_INPUT_CLASS} font-mono`}
                      >
                        {OUTPUT_FORMAT_OPTIONS.map(o => (
                          <option key={o.value} value={o.value}>{o.label}</option>
                        ))}
                      </select>
                    </EditorFormRow>
                  </div>
                </>
              ) : (
                <>
                  <EditorFormRow
                    label="スクリプト名"
                    help="user-folder の scripts/ または browser-macros/ にあるファイル名 (.js 拡張子は省略可)"
                  >
                    <input
                      value={form.scriptName}
                      onChange={e => setForm(p => ({ ...p, scriptName: e.target.value }))}
                      className={`${EDITOR_INPUT_CLASS} font-mono`}
                      placeholder="weekly-report"
                    />
                  </EditorFormRow>
                  <EditorFormRow
                    label="params (JSON)"
                    help="スクリプトの frontmatter params に合わせた JSON object。空でも可"
                  >
                    <textarea
                      value={form.scriptParams}
                      onChange={e => setForm(p => ({ ...p, scriptParams: e.target.value }))}
                      rows={4}
                      className={`${EDITOR_INPUT_CLASS} font-mono resize-y leading-relaxed`}
                      placeholder='{"date":"2026-05-11"}'
                    />
                  </EditorFormRow>
                </>
              )}
            </div>
          </div>

          <div className="bg-white border border-hairline rounded-md p-5">
            <div className="section-label mb-3.5">
              スケジュール
            </div>

            <EditorFormRow label="タイプ">
              <div className="flex flex-wrap gap-1.5">
                {SCHEDULE_TYPE_OPTIONS.map(opt => {
                  const selected = form.scheduleType === opt.value;
                  return (
                    <button
                      key={opt.value}
                      type="button"
                      onClick={() => setForm(p => ({ ...p, scheduleType: opt.value }))}
                      aria-pressed={selected}
                      title={opt.hint}
                      className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
                        selected
                          ? 'border-accent bg-accent-soft text-accent'
                          : 'border-hairline bg-white text-slate-600 hover:border-hairline'
                      }`}
                    >
                      {opt.label}
                    </button>
                  );
                })}
              </div>
            </EditorFormRow>

            {form.scheduleType !== 'cron' && form.scheduleType !== 'once' && (
              <div className="grid grid-cols-2 gap-3 mt-3">
                <EditorFormRow label="時刻 (UTC)">
                  <div className="flex items-center gap-1">
                    <input
                      type="number"
                      min={0}
                      max={23}
                      value={form.hour}
                      onChange={e => setForm(p => ({ ...p, hour: Number(e.target.value) }))}
                      className={`${EDITOR_INPUT_CLASS} w-16 text-center font-mono`}
                    />
                    <span className="text-slate-400">:</span>
                    <input
                      type="number"
                      min={0}
                      max={59}
                      value={form.minute}
                      onChange={e => setForm(p => ({ ...p, minute: Number(e.target.value) }))}
                      className={`${EDITOR_INPUT_CLASS} w-16 text-center font-mono`}
                    />
                  </div>
                </EditorFormRow>
                {form.scheduleType === 'weekly' && (
                  <EditorFormRow label="曜日">
                    <select
                      value={form.dayOfWeek}
                      onChange={e => setForm(p => ({ ...p, dayOfWeek: Number(e.target.value) }))}
                      className={EDITOR_INPUT_CLASS}
                    >
                      {DAY_OPTIONS.map((d, i) => <option key={i} value={i}>{d}</option>)}
                    </select>
                  </EditorFormRow>
                )}
                {form.scheduleType === 'monthly' && (
                  <EditorFormRow label="日">
                    <input
                      type="number"
                      min={1}
                      max={31}
                      value={form.dayOfMonth}
                      onChange={e => setForm(p => ({ ...p, dayOfMonth: Number(e.target.value) }))}
                      className={EDITOR_INPUT_CLASS}
                    />
                  </EditorFormRow>
                )}
              </div>
            )}

            {form.scheduleType === 'cron' && (
              <div className="mt-3">
                <EditorFormRow label="Cron 式" help="分 時 日 月 曜日 · 例: 0 7 * * * = 毎日 07:00 (UTC)">
                  <input
                    value={form.cronExpression}
                    onChange={e => setForm(p => ({ ...p, cronExpression: e.target.value }))}
                    className={`${EDITOR_INPUT_CLASS} font-mono`}
                    placeholder="0 9 * * 1"
                  />
                </EditorFormRow>
              </div>
            )}

            {form.scheduleType === 'once' && (
              <div className="mt-3">
                <EditorFormRow label="実行日時（ローカル時刻）">
                  <input
                    type="datetime-local"
                    value={form.scheduledAt}
                    onChange={e => setForm(p => ({ ...p, scheduledAt: e.target.value }))}
                    className={EDITOR_INPUT_CLASS}
                  />
                </EditorFormRow>
              </div>
            )}

            {preview && form.scheduleType !== 'once' && (
              <div className="mt-3 px-3 py-2.5 bg-surface border border-hairline rounded-md text-xs text-slate-600">
                <span className="text-[10px] font-bold text-slate-500 uppercase tracking-wide mr-2">
                  プレビュー
                </span>
                <b className="font-semibold text-slate-900">{preview}</b>
              </div>
            )}
          </div>

          {authState.mode === 'authenticated' && (
            <div className="bg-white border border-hairline rounded-md p-5">
              <div className="section-label mb-3.5">
                公開範囲
              </div>
              <div className="flex flex-col gap-2 text-[13px]">
                <label className="inline-flex items-center gap-2">
                  <input
                    type="radio"
                    checked={form.visibility === 'private'}
                    onChange={() => setForm(p => ({ ...p, visibility: 'private' }))}
                  />
                  <span>🔒 Private（自分のみ）</span>
                </label>
                <label className="inline-flex items-center gap-2">
                  <input
                    type="radio"
                    checked={form.visibility === 'org'}
                    onChange={() => setForm(p => ({ ...p, visibility: 'org' }))}
                    disabled={orgs.length === 0}
                  />
                  <span>🏢 Organization</span>
                </label>
                <label className="inline-flex items-center gap-2">
                  <input
                    type="radio"
                    checked={form.visibility === 'public'}
                    onChange={() => setForm(p => ({ ...p, visibility: 'public' }))}
                  />
                  <span>🌐 Public（全ログインユーザー）</span>
                </label>
                {form.visibility === 'org' && orgs.length > 1 && (
                  <select
                    className={`${EDITOR_INPUT_CLASS} mt-1`}
                    value={form.visibilityScopeOrgId ?? ''}
                    onChange={e => setForm(p => ({ ...p, visibilityScopeOrgId: e.target.value || null }))}
                  >
                    {orgs.map(o => (
                      <option key={o.orgId} value={o.orgId}>{o.orgName}</option>
                    ))}
                  </select>
                )}
                {form.visibility === 'org' && orgs.length === 1 && (
                  <div className="text-2xs text-slate-500 mt-1">→ {orgs[0].orgName}</div>
                )}
                {form.visibility === 'org' && orgs.length === 0 && (
                  <div className="text-2xs text-amber-700 mt-1">
                    Organization に所属していません。Private または Public を選択してください。
                  </div>
                )}
              </div>
            </div>
          )}

          {activeSessionProfiles.length > 0 && (
            <div className="bg-white border border-hairline rounded-md p-5">
              <div className="section-label mb-3.5">
                ブラウザセッション（任意）
              </div>
              <select
                value={form.browserSessionProfileId ?? ''}
                onChange={e =>
                  setForm(p => ({
                    ...p,
                    browserSessionProfileId: e.target.value ? Number(e.target.value) : null,
                  }))
                }
                className={EDITOR_INPUT_CLASS}
              >
                <option value="">None</option>
                {activeSessionProfiles.map(p => (
                  <option key={p.id} value={p.id}>{p.label}</option>
                ))}
              </select>
              <p className="text-2xs text-slate-500 mt-1">
                エージェントは保存済みログインを使ってブラウズします。
              </p>
            </div>
          )}

          {error && (
            <div
              role="alert"
              className="px-3.5 py-2.5 bg-red-50 border border-red-200 rounded-md text-xs text-red-700"
            >
              ⚠ {error}
            </div>
          )}

          <div className="h-4" />
        </div>
      </div>

      <div className="flex-shrink-0 px-6 py-3.5 border-t border-hairline flex justify-end gap-2 bg-white">
        <button
          type="button"
          onClick={onCancel}
          className="px-4 py-2 text-[13px] font-semibold text-slate-600 rounded-md hover:bg-surface-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring"
        >
          キャンセル
        </button>
        <button
          type="button"
          onClick={() => void handleSubmit()}
          disabled={submitting || !form.body.trim()}
          className="px-4 py-2 bg-accent hover:bg-accent-deep text-accent-fg text-[13px] font-semibold rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring"
        >
          {submitting ? (isEdit ? '保存中...' : '作成中...') : (isEdit ? '保存' : '作成')}
        </button>
      </div>
    </div>
  );
}
