/**
 * MemoryLearningForm.tsx — "Memory & Learning" settings section
 *
 * Two stacked panels:
 *   1. MemoryEntriesPanel  — list / inline-edit / delete user memory entries
 *   2. ReflectionTimelinePanel — paged snapshot history + revert + 30-day metrics
 */

import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { HelpText } from './HelpText';

// ── API types ─────────────────────────────────────────────────────────────────

type MemoryType = 'user' | 'feedback' | 'project' | 'reference';

// Mirrors the server's flat shape from `listMemoryEntries` in
// src/user-folder/memory.ts and `GET /api/local/memory/entries` in
// src/bridge/memory-api.ts. If you change this shape, update both.
interface MemoryEntry {
  name: string;
  description: string;
  type: MemoryType;
  body: string;
}

interface MemoryListResponse {
  entries: MemoryEntry[];
  index: string;
}

interface SnapshotIndexEntry {
  ts: string;
  snapshotId: string;
  jobId: string;
  pieceName: string;
  memoryChanges: number;
  pieceEdited: boolean;
  reverted: boolean;
  // outcome appears in detail, not in index — fetched lazily
}

interface SnapshotDetail {
  snapshotId: string;
  ts: string;
  originalJobId: string;
  userId: string;
  pieceName: string;
  outcome: string;
  reasoning: string;
  modelUsed?: string;
  tokensIn?: number;
  tokensOut?: number;
  ratingAtTime?: 'good' | 'bad' | null;
  memoryChanges: number;
  pieceEdited: boolean;
  rejections?: Array<{ code: string; name?: string }>;
  beforeFiles: Record<string, string>;
  afterFiles: Record<string, string>;
  pieceBeforeYaml?: string;
  pieceAfterYaml?: string;
  diff?: string;
}

interface HistoryPage {
  items: SnapshotIndexEntry[];
  nextCursor: string | null;
}

interface ReflectionMetrics {
  applied: number;
  partial: number;
  abstained: number;
  rejected: number;
  failed: number;
  tokensIn: number;
  tokensOut: number;
  pieceEdits: number;
}

// ── API helpers ───────────────────────────────────────────────────────────────

async function fetchMemoryEntries(): Promise<MemoryListResponse> {
  const res = await fetch('/api/local/memory/entries');
  if (!res.ok) throw new Error(`メモリエントリの読み込みに失敗しました (${res.status})`);
  return res.json();
}

async function upsertMemoryEntry(
  name: string,
  payload: { description: string; type: MemoryType; body: string },
): Promise<void> {
  const res = await fetch(`/api/local/memory/entries/${encodeURIComponent(name)}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
  const data = await res.json().catch(() => ({ error: res.statusText }));
  if (!res.ok) throw new Error(data.error ?? res.statusText);
}

async function deleteMemoryEntry(name: string): Promise<void> {
  const res = await fetch(`/api/local/memory/entries/${encodeURIComponent(name)}`, {
    method: 'DELETE',
  });
  if (!res.ok) {
    const data = await res.json().catch(() => ({ error: res.statusText }));
    throw new Error(data.error ?? res.statusText);
  }
}

async function fetchHistoryPage(cursor?: string): Promise<HistoryPage> {
  const params = new URLSearchParams({ limit: '20' });
  if (cursor) params.set('before', cursor);
  const res = await fetch(`/api/local/reflection/history?${params}`);
  if (!res.ok) throw new Error(`履歴の読み込みに失敗しました (${res.status})`);
  return res.json();
}

async function fetchSnapshotDetail(snapshotId: string): Promise<SnapshotDetail> {
  const res = await fetch(`/api/local/reflection/history/${encodeURIComponent(snapshotId)}`);
  if (!res.ok) throw new Error(`スナップショットの読み込みに失敗しました (${res.status})`);
  return res.json();
}

async function revertSnapshot(snapshotId: string): Promise<{ reverted: boolean }> {
  const res = await fetch(
    `/api/local/reflection/history/${encodeURIComponent(snapshotId)}/revert`,
    { method: 'POST' },
  );
  if (!res.ok) {
    const data = await res.json().catch(() => ({ error: res.statusText }));
    throw new Error(data.error ?? res.statusText);
  }
  return res.json();
}

async function fetchMetrics(days: number = 30): Promise<ReflectionMetrics> {
  const res = await fetch(`/api/local/reflection/metrics?days=${days}`);
  if (!res.ok) throw new Error(`メトリクスの読み込みに失敗しました (${res.status})`);
  return res.json();
}

// ── Shared UI primitives ──────────────────────────────────────────────────────

const OUTCOME_LABELS: Record<string, { label: string; cls: string }> = {
  applied:   { label: '適用済み',   cls: 'bg-emerald-100 text-emerald-800' },
  partial:   { label: '一部適用',   cls: 'bg-yellow-100 text-yellow-800' },
  abstained: { label: '学習なし',   cls: 'bg-slate-100 text-slate-600' },
  rejected:  { label: '却下',       cls: 'bg-red-100 text-red-700' },
  failed:    { label: '失敗',       cls: 'bg-red-200 text-red-900' },
};

function OutcomeBadge({ outcome }: { outcome: string }) {
  const { label, cls } = OUTCOME_LABELS[outcome] ?? { label: outcome, cls: 'bg-slate-100 text-slate-600' };
  return (
    <span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-semibold ${cls}`}>
      {label}
    </span>
  );
}

function formatTs(ts: string): string {
  try {
    return new Date(ts).toLocaleString(undefined, {
      year: 'numeric', month: 'short', day: 'numeric',
      hour: '2-digit', minute: '2-digit',
    });
  } catch {
    return ts;
  }
}

// ── Validator rejection code messages ─────────────────────────────────────────

const REJECTION_MESSAGES: Record<string, string> = {
  rejected_bad_name:          '名前が無効です（英数字・ハイフン・アンダースコア、1〜64文字）',
  rejected_bad_description:   '概要は必須で、1行以内で入力してください',
  rejected_unknown_type:      'タイプは user / feedback / project / reference のいずれかを指定してください',
  rejected_bad_body:          '本文は文字列で指定してください',
  rejected_body_too_large:    '本文が許容サイズを超えています',
  rejected_bad_request:       'リクエストの形式が正しくありません',
};

function rejectionMessage(code: string): string {
  return REJECTION_MESSAGES[code] ?? code;
}

// ── MemoryEntryModal ──────────────────────────────────────────────────────────

interface EntryFormState {
  name: string;
  description: string;
  type: MemoryType;
  body: string;
}

const MEMORY_TYPES: MemoryType[] = ['user', 'feedback', 'project', 'reference'];

function MemoryEntryModal({
  initial,
  isNew,
  onClose,
  onSaved,
}: {
  initial: EntryFormState;
  isNew: boolean;
  onClose: () => void;
  onSaved: () => void;
}) {
  const [form, setForm] = useState<EntryFormState>(initial);
  const [error, setError] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);

  const set = <K extends keyof EntryFormState>(k: K, v: EntryFormState[K]) =>
    setForm(prev => ({ ...prev, [k]: v }));

  const handleSave = async () => {
    setSaving(true);
    setError(null);
    try {
      await upsertMemoryEntry(form.name, {
        description: form.description,
        type: form.type,
        body: form.body,
      });
      onSaved();
      onClose();
    } catch (e: any) {
      setError(rejectionMessage(e.message));
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
      <div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 flex flex-col max-h-[90vh]">
        <div className="flex items-center justify-between px-4 py-3 border-b border-hairline">
          <h3 className="text-sm font-semibold text-slate-800">
            {isNew ? '新しいメモリエントリ' : `編集 — ${initial.name}`}
          </h3>
          <button
            onClick={onClose}
            className="text-slate-400 hover:text-slate-700 text-lg leading-none"
            aria-label="閉じる"
          >
            ×
          </button>
        </div>

        <div className="overflow-y-auto p-4 space-y-3 flex-1">
          {/* Name — only editable when creating */}
          <div>
            <label className="block text-2xs font-medium text-slate-600 mb-1">
              名前
              {isNew && <span className="text-slate-400 ml-1">（英数字・ハイフン・アンダースコア）</span>}
            </label>
            <input
              type="text"
              value={form.name}
              onChange={e => set('name', e.target.value)}
              disabled={!isNew}
              placeholder="my-fact"
              className={`w-full h-8 px-2.5 text-[13px] font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${
                !isNew ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-white'
              }`}
            />
          </div>

          <div>
            <label className="block text-2xs font-medium text-slate-600 mb-1">概要</label>
            <input
              type="text"
              value={form.description}
              onChange={e => set('description', e.target.value)}
              placeholder="メモリ一覧に表示される1行の説明"
              className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
            />
          </div>

          <div>
            <label className="block text-2xs font-medium text-slate-600 mb-1">タイプ</label>
            <select
              value={form.type}
              onChange={e => set('type', e.target.value as MemoryType)}
              className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring outline-none bg-white"
            >
              {MEMORY_TYPES.map(t => (
                <option key={t} value={t}>{t}</option>
              ))}
            </select>
            <HelpText>
              user: あなた固有の好み・役割 ／ feedback: 過去のフィードバック・教訓 ／ project: プロジェクト別の文脈 ／ reference: 参照資料・外部情報
            </HelpText>
          </div>

          <div>
            <label className="block text-2xs font-medium text-slate-600 mb-1">本文</label>
            <textarea
              value={form.body}
              onChange={e => set('body', e.target.value)}
              rows={8}
              className="w-full px-2.5 py-2 text-xs font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none resize-y"
              placeholder="Markdown またはプレーンテキスト…"
            />
          </div>

          {error && (
            <div className="text-xs text-red-600 bg-red-50 border border-red-200 px-3 py-2 rounded-md">
              {error}
            </div>
          )}
        </div>

        <div className="flex justify-end gap-2 px-4 py-3 border-t border-hairline">
          <button
            onClick={onClose}
            className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface transition-colors"
          >
            キャンセル
          </button>
          <button
            onClick={() => void handleSave()}
            disabled={saving || !form.name.trim() || !form.description.trim()}
            className="px-3 h-8 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50 transition-colors"
          >
            {saving ? '保存中…' : '保存'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── MemoryEntriesPanel ────────────────────────────────────────────────────────

function MemoryEntriesPanel() {
  const qc = useQueryClient();
  const { data, isLoading, error } = useQuery<MemoryListResponse>({
    queryKey: ['memory-entries'],
    queryFn: fetchMemoryEntries,
  });

  const [modal, setModal] = useState<{ entry: EntryFormState; isNew: boolean } | null>(null);
  const [deleting, setDeleting] = useState<string | null>(null);
  const [deleteError, setDeleteError] = useState<string | null>(null);

  const handleNew = () => {
    setModal({
      isNew: true,
      entry: { name: '', description: '', type: 'user', body: '' },
    });
  };

  const handleEdit = (e: MemoryEntry) => {
    setModal({
      isNew: false,
      entry: {
        name: e.name,
        description: e.description,
        type: e.type,
        body: e.body,
      },
    });
  };

  const handleDelete = async (name: string) => {
    if (!confirm(`メモリエントリ「${name}」を削除しますか？`)) return;
    setDeleting(name);
    setDeleteError(null);
    try {
      await deleteMemoryEntry(name);
      await qc.invalidateQueries({ queryKey: ['memory-entries'] });
    } catch (e: any) {
      setDeleteError(`「${name}」の削除に失敗しました: ${e.message}`);
    } finally {
      setDeleting(null);
    }
  };

  const handleSaved = () => {
    void qc.invalidateQueries({ queryKey: ['memory-entries'] });
  };

  return (
    <div className="rounded-lg border border-hairline">
      <div className="flex items-center justify-between px-4 py-3 border-b border-hairline bg-surface rounded-t-lg">
        <div>
          <h3 className="text-sm font-semibold text-slate-800">メモリエントリ</h3>
          <p className="text-2xs text-slate-500 mt-0.5">
            エージェントの毎セッションに注入される永続的な情報。
          </p>
        </div>
        <button
          onClick={handleNew}
          className="px-2.5 h-7 text-2xs font-medium bg-accent text-accent-fg rounded-md hover:bg-accent-deep transition-colors"
        >
          + 新しいエントリ
        </button>
      </div>

      {isLoading && (
        <div className="px-4 py-6 text-xs text-slate-400 text-center">読み込み中…</div>
      )}

      {error && (
        <div className="px-4 py-3 text-xs text-red-600">
          メモリエントリの読み込みに失敗しました: {String(error)}
        </div>
      )}

      {deleteError && (
        <div className="px-4 py-2 text-xs text-red-600 bg-red-50">
          {deleteError}
        </div>
      )}

      {data && data.entries.length === 0 && (
        <div className="px-4 py-8 text-center">
          <p className="text-xs text-slate-400">メモリエントリはまだありません。</p>
          <p className="text-2xs text-slate-400 mt-1">
            タスク完了後に reflection エンジンが自動で追加します。
            手動で追加することもできます。
          </p>
        </div>
      )}

      {data && data.entries.length > 0 && (
        <ul className="divide-y divide-hairline">
          {data.entries.map(entry => (
            <li key={entry.name} className="flex items-start gap-3 px-4 py-3 hover:bg-surface/60 transition-colors">
              <div className="flex-1 min-w-0">
                <div className="flex items-center gap-2 flex-wrap">
                  <span className="text-xs font-mono font-medium text-slate-800 truncate">
                    {entry.name}
                  </span>
                  <span className="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-500 flex-shrink-0">
                    {entry.type}
                  </span>
                </div>
                <p className="text-2xs text-slate-500 mt-0.5 truncate">{entry.description}</p>
                {entry.body && (
                  <p className="text-2xs text-slate-400 mt-0.5 line-clamp-2 font-mono whitespace-pre-wrap break-words">
                    {entry.body.slice(0, 200)}{entry.body.length > 200 ? '…' : ''}
                  </p>
                )}
              </div>
              <div className="flex gap-1.5 flex-shrink-0 mt-0.5">
                <button
                  onClick={() => handleEdit(entry)}
                  className="px-2 h-6 text-2xs text-slate-600 border border-hairline bg-white hover:bg-surface rounded transition-colors"
                >
                  編集
                </button>
                <button
                  onClick={() => void handleDelete(entry.name)}
                  disabled={deleting === entry.name}
                  className="px-2 h-6 text-2xs text-red-700 border border-red-200 bg-white hover:bg-red-50 rounded transition-colors disabled:opacity-50"
                >
                  {deleting === entry.name ? '…' : '削除'}
                </button>
              </div>
            </li>
          ))}
        </ul>
      )}

      {modal && (
        <MemoryEntryModal
          initial={modal.entry}
          isNew={modal.isNew}
          onClose={() => setModal(null)}
          onSaved={handleSaved}
        />
      )}
    </div>
  );
}

// ── SnapshotCard ──────────────────────────────────────────────────────────────

function SnapshotCard({ item, onReverted }: { item: SnapshotIndexEntry; onReverted: () => void }) {
  const [expanded, setExpanded] = useState(false);
  const [confirmRevert, setConfirmRevert] = useState(false);
  const [revertDone, setRevertDone] = useState<boolean | null>(null);

  const detailQuery = useQuery<SnapshotDetail>({
    queryKey: ['snapshot-detail', item.snapshotId],
    queryFn: () => fetchSnapshotDetail(item.snapshotId),
    enabled: expanded,
    staleTime: 5 * 60 * 1000,
  });

  const revertMutation = useMutation({
    mutationFn: () => revertSnapshot(item.snapshotId),
    onSuccess: (result) => {
      setRevertDone(result.reverted);
      setConfirmRevert(false);
      if (result.reverted) onReverted();
    },
  });

  return (
    <div className={`border border-hairline rounded-md overflow-hidden ${item.reverted ? 'opacity-60' : ''}`}>
      {/* Header row — always visible */}
      <button
        type="button"
        onClick={() => setExpanded(p => !p)}
        className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-surface/60 transition-colors"
      >
        <span className="text-2xs text-slate-400 flex-shrink-0 w-32 truncate" title={item.ts}>
          {formatTs(item.ts)}
        </span>
        <span className="text-2xs font-mono text-slate-700 truncate flex-1" title={item.pieceName}>
          {item.pieceName}
        </span>
        <span className="flex-shrink-0 flex items-center gap-1.5">
          {item.memoryChanges > 0 && (
            <span className="text-[10px] px-1.5 py-0.5 bg-blue-50 text-blue-700 rounded">
              {item.memoryChanges} mem
            </span>
          )}
          {item.pieceEdited && (
            <span className="text-[10px] px-1.5 py-0.5 bg-purple-50 text-purple-700 rounded">
              piece
            </span>
          )}
          {item.reverted && (
            <span className="text-[10px] px-1.5 py-0.5 bg-slate-100 text-slate-500 rounded">
              revert済み
            </span>
          )}
          {detailQuery.data && <OutcomeBadge outcome={detailQuery.data.outcome} />}
        </span>
        <span className="text-slate-400 text-xs flex-shrink-0">{expanded ? '▲' : '▼'}</span>
      </button>

      {/* Expanded detail */}
      {expanded && (
        <div className="border-t border-hairline bg-slate-50 px-3 py-3 space-y-3">
          {detailQuery.isLoading && (
            <div className="text-xs text-slate-400">詳細を読み込み中…</div>
          )}
          {detailQuery.error && (
            <div className="text-xs text-red-600">
              読み込みに失敗しました: {String(detailQuery.error)}
            </div>
          )}
          {detailQuery.data && (() => {
            const d = detailQuery.data;
            return (
              <>
                <div className="flex items-center gap-3 flex-wrap">
                  <OutcomeBadge outcome={d.outcome} />
                  {d.modelUsed && (
                    <span className="text-[10px] text-slate-400">{d.modelUsed}</span>
                  )}
                  {(d.tokensIn || d.tokensOut) && (
                    <span className="text-[10px] text-slate-400">
                      {(d.tokensIn ?? 0).toLocaleString()} in / {(d.tokensOut ?? 0).toLocaleString()} out tokens
                    </span>
                  )}
                </div>

                {d.reasoning && (
                  <div>
                    <div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1">
                      推論
                    </div>
                    <p className="text-xs text-slate-700 whitespace-pre-wrap">{d.reasoning}</p>
                  </div>
                )}

                {d.rejections && d.rejections.length > 0 && (
                  <div>
                    <div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1">
                      却下理由
                    </div>
                    <ul className="space-y-0.5">
                      {d.rejections.map((r, i) => (
                        <li key={i} className="text-2xs text-red-700">
                          <span className="font-mono">{r.code}</span>
                          {r.name && <span className="text-slate-500 ml-1">({r.name})</span>}
                        </li>
                      ))}
                    </ul>
                  </div>
                )}

                {d.diff && (
                  <div>
                    <div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1">
                      変更内容
                    </div>
                    <pre className="text-2xs text-slate-700 bg-white border border-hairline rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap">
                      {d.diff}
                    </pre>
                  </div>
                )}

                {/* Before / After file diff */}
                {(Object.keys(d.beforeFiles).length > 0 || Object.keys(d.afterFiles).length > 0) && (
                  <BeforeAfterDiff beforeFiles={d.beforeFiles} afterFiles={d.afterFiles} />
                )}

                {/* Piece diff */}
                {d.pieceEdited && d.pieceBeforeYaml && d.pieceAfterYaml && (
                  <div>
                    <div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1">
                      Piece の差分
                    </div>
                    <div className="grid grid-cols-2 gap-2">
                      <div>
                        <div className="text-[10px] text-slate-400 mb-0.5">変更前</div>
                        <pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
                          {d.pieceBeforeYaml}
                        </pre>
                      </div>
                      <div>
                        <div className="text-[10px] text-slate-400 mb-0.5">変更後</div>
                        <pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
                          {d.pieceAfterYaml}
                        </pre>
                      </div>
                    </div>
                  </div>
                )}

                {/* Revert controls */}
                {!item.reverted && (
                  <div className="pt-1">
                    {revertDone === true && (
                      <span className="text-xs text-emerald-700">正常に revert しました。</span>
                    )}
                    {revertDone === false && (
                      <span className="text-xs text-slate-500">すでに revert 済みです。</span>
                    )}
                    {revertDone === null && !confirmRevert && (
                      <button
                        type="button"
                        onClick={() => setConfirmRevert(true)}
                        className="px-2.5 h-7 text-2xs text-amber-800 border border-amber-300 bg-amber-50 hover:bg-amber-100 rounded transition-colors"
                      >
                        このスナップショットを revert…
                      </button>
                    )}
                    {revertDone === null && confirmRevert && (
                      <div className="flex items-center gap-2">
                        <span className="text-xs text-amber-800">
                          このスナップショットの変更前の状態に戻しますか？
                        </span>
                        <button
                          type="button"
                          onClick={() => revertMutation.mutate()}
                          disabled={revertMutation.isPending}
                          className="px-2.5 h-7 text-2xs font-semibold bg-red-600 text-white hover:bg-red-700 rounded disabled:opacity-50 transition-colors"
                        >
                          {revertMutation.isPending ? 'revert 中…' : 'revert を確定'}
                        </button>
                        <button
                          type="button"
                          onClick={() => setConfirmRevert(false)}
                          className="px-2.5 h-7 text-2xs text-slate-600 border border-hairline bg-white hover:bg-surface rounded transition-colors"
                        >
                          キャンセル
                        </button>
                      </div>
                    )}
                    {revertMutation.isError && (
                      <div className="text-2xs text-red-600 mt-1">
                        {String(revertMutation.error)}
                      </div>
                    )}
                  </div>
                )}
              </>
            );
          })()}
        </div>
      )}
    </div>
  );
}

// ── BeforeAfterDiff ───────────────────────────────────────────────────────────

function BeforeAfterDiff({
  beforeFiles,
  afterFiles,
}: {
  beforeFiles: Record<string, string>;
  afterFiles: Record<string, string>;
}) {
  const allNames = Array.from(
    new Set([...Object.keys(beforeFiles), ...Object.keys(afterFiles)]),
  ).sort();

  if (allNames.length === 0) return null;

  const [selected, setSelected] = useState(allNames[0]);

  const before = beforeFiles[selected];
  const after = afterFiles[selected];
  const isAdded = !before && !!after;
  const isRemoved = !!before && !after;

  return (
    <div>
      <div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1">
        メモリファイルの差分
      </div>
      {allNames.length > 1 && (
        <div className="flex gap-1 mb-2 flex-wrap">
          {allNames.map(n => (
            <button
              key={n}
              type="button"
              onClick={() => setSelected(n)}
              className={`px-1.5 py-0.5 text-[10px] rounded border ${
                selected === n
                  ? 'border-accent bg-accent-soft text-accent font-semibold'
                  : 'border-hairline text-slate-500 hover:bg-surface'
              }`}
            >
              {n}
            </button>
          ))}
        </div>
      )}
      {isAdded && (
        <div className="text-2xs text-emerald-700 bg-emerald-50 border border-emerald-200 rounded px-2 py-1 mb-1">
          追加
        </div>
      )}
      {isRemoved && (
        <div className="text-2xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 mb-1">
          削除
        </div>
      )}
      <div className="grid grid-cols-2 gap-2">
        {!isAdded && (
          <div>
            <div className="text-[10px] text-slate-400 mb-0.5">変更前</div>
            <pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
              {before ?? '（空）'}
            </pre>
          </div>
        )}
        {!isRemoved && (
          <div className={isAdded ? 'col-span-2' : ''}>
            <div className="text-[10px] text-slate-400 mb-0.5">変更後</div>
            <pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
              {after ?? '（空）'}
            </pre>
          </div>
        )}
      </div>
    </div>
  );
}

// ── MetricsSummary ────────────────────────────────────────────────────────────

function MetricsSummary() {
  const { data, isLoading, error } = useQuery<ReflectionMetrics>({
    queryKey: ['reflection-metrics', 30],
    queryFn: () => fetchMetrics(30),
    staleTime: 5 * 60 * 1000,
  });

  if (isLoading) {
    return <div className="text-xs text-slate-400 px-4 py-3">メトリクスを読み込み中…</div>;
  }

  if (error) {
    return (
      <div className="text-xs text-red-600 px-4 py-3">
        メトリクスの読み込みに失敗しました: {String(error)}
      </div>
    );
  }

  if (!data) return null;

  const totalRuns = data.applied + data.partial + data.abstained + data.rejected + data.failed;
  const appliedPct = totalRuns > 0 ? Math.round(((data.applied + data.partial) / totalRuns) * 100) : 0;
  const abstainPct = totalRuns > 0 ? Math.round((data.abstained / totalRuns) * 100) : 0;
  const totalTokens = data.tokensIn + data.tokensOut;

  return (
    <div className="px-4 py-3 bg-slate-50 border-t border-hairline rounded-b-lg">
      <div className="text-[10px] font-semibold text-slate-500 uppercase tracking-wide mb-2">
        30日間のサマリ
      </div>
      <div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
        {[
          { label: '合計実行回数', value: String(totalRuns) },
          { label: '適用率', value: `${appliedPct}%` },
          { label: '学習なし率', value: `${abstainPct}%` },
          { label: 'Tokens', value: totalTokens > 1000 ? `${Math.round(totalTokens / 1000)}k` : String(totalTokens) },
          { label: 'Piece 編集', value: String(data.pieceEdits) },
        ].map(({ label, value }) => (
          <div
            key={label}
            className="bg-white border border-hairline rounded px-2 py-1.5 text-center"
          >
            <div className="text-2xs font-semibold text-slate-800">{value}</div>
            <div className="text-[10px] text-slate-400 mt-0.5">{label}</div>
          </div>
        ))}
      </div>
      {totalRuns === 0 && (
        <p className="text-2xs text-slate-400 mt-2">
          まだ reflection の実行履歴がありません。最初の reflection が完了するとメトリクスが表示されます。
        </p>
      )}
    </div>
  );
}

// ── ReflectionTimelinePanel ───────────────────────────────────────────────────

const OUTCOME_FILTER_OPTIONS = [
  { value: 'applied',   label: '適用済み' },
  { value: 'partial',   label: '一部適用' },
  { value: 'abstained', label: '学習なし' },
  { value: 'rejected',  label: '却下' },
  { value: 'failed',    label: '失敗' },
];

function ReflectionTimelinePanel() {
  const qc = useQueryClient();

  // Filters (client-side — the backend doesn't support filtering natively)
  const [outcomeFilter, setOutcomeFilter] = useState<string[]>([]);
  const [includeReverted, setIncludeReverted] = useState(true);

  const {
    data,
    isLoading,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery<HistoryPage>({
    queryKey: ['reflection-history'],
    queryFn: ({ pageParam }) =>
      fetchHistoryPage(typeof pageParam === 'string' ? pageParam : undefined),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  });

  const allItems: SnapshotIndexEntry[] = (data?.pages ?? []).flatMap(p => p.items);

  // Client-side filtering — outcome is on the detail, but index has `reverted`
  const filteredItems = allItems.filter(item => {
    if (!includeReverted && item.reverted) return false;
    // Outcome filtering is only possible after detail is loaded; skip if no filter set
    return true;
  });

  const handleReverted = () => {
    void qc.invalidateQueries({ queryKey: ['reflection-history'] });
    void qc.invalidateQueries({ queryKey: ['reflection-metrics'] });
  };

  return (
    <div className="rounded-lg border border-hairline">
      <div className="px-4 py-3 border-b border-hairline bg-surface rounded-t-lg">
        <h3 className="text-sm font-semibold text-slate-800">Reflection タイムライン</h3>
        <p className="text-2xs text-slate-500 mt-0.5">
          reflection 実行の履歴。各行を展開すると推論・変更前後の差分・revert コントロールを確認できます。
        </p>

        {/* Filters */}
        <div className="flex flex-wrap items-center gap-3 mt-2">
          <label className="flex items-center gap-1.5 text-2xs text-slate-600 cursor-pointer">
            <input
              type="checkbox"
              checked={includeReverted}
              onChange={e => setIncludeReverted(e.target.checked)}
              className="rounded"
            />
            revert 済みを表示
          </label>

          <div className="flex items-center gap-1.5 flex-wrap">
            <span className="text-2xs text-slate-500">結果:</span>
            {OUTCOME_FILTER_OPTIONS.map(opt => (
              <label key={opt.value} className="flex items-center gap-1 text-2xs text-slate-600 cursor-pointer">
                <input
                  type="checkbox"
                  checked={outcomeFilter.length === 0 || outcomeFilter.includes(opt.value)}
                  onChange={e => {
                    if (e.target.checked) {
                      setOutcomeFilter(prev =>
                        prev.length === 0 ? [] : prev.filter(v => v !== opt.value).concat(opt.value),
                      );
                    } else {
                      setOutcomeFilter(prev => {
                        const next = prev.length === 0
                          ? OUTCOME_FILTER_OPTIONS.map(o => o.value).filter(v => v !== opt.value)
                          : prev.filter(v => v !== opt.value);
                        return next;
                      });
                    }
                  }}
                />
                {opt.label}
              </label>
            ))}
            {outcomeFilter.length > 0 && (
              <button
                type="button"
                onClick={() => setOutcomeFilter([])}
                className="text-[10px] text-accent underline"
              >
                リセット
              </button>
            )}
          </div>
        </div>
      </div>

      <div className="p-3 space-y-2">
        {isLoading && (
          <div className="text-xs text-slate-400 text-center py-4">読み込み中…</div>
        )}

        {error && (
          <div className="text-xs text-red-600 px-2">
            読み込みに失敗しました: {String(error)}
          </div>
        )}

        {!isLoading && filteredItems.length === 0 && (
          <div className="text-center py-6">
            <p className="text-xs text-slate-400">まだ reflection の実行履歴がありません。</p>
            <p className="text-2xs text-slate-400 mt-1">
              タスク完了後に自動で reflection が実行されます。
            </p>
          </div>
        )}

        {filteredItems.map(item => (
          <SnapshotCard key={item.snapshotId} item={item} onReverted={handleReverted} />
        ))}

        {hasNextPage && (
          <div className="flex justify-center pt-1">
            <button
              type="button"
              onClick={() => void fetchNextPage()}
              disabled={isFetchingNextPage}
              className="px-3 h-8 text-xs text-slate-600 border border-hairline bg-white hover:bg-surface rounded-md disabled:opacity-50 transition-colors"
            >
              {isFetchingNextPage ? '読み込み中…' : 'さらに表示'}
            </button>
          </div>
        )}
      </div>

      <MetricsSummary />
    </div>
  );
}

// ── MemoryLearningForm (root export) ──────────────────────────────────────────

export function MemoryLearningForm() {
  return (
    <div className="space-y-6">
      <h2 className="text-base font-semibold text-slate-800">Memory &amp; Learning</h2>
      <p className="text-xs text-slate-500 -mt-4">
        エージェントが毎セッション参照する永続的なメモリエントリを管理し、自動学習（reflection）の実行履歴を確認できます。
      </p>

      <MemoryEntriesPanel />
      <ReflectionTimelinePanel />
    </div>
  );
}
