import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { fetchLocalFileContent } from '../../../api';

// Mirror of `src/progress/event-log.ts` EventBase. Kept as a duplicate
// here because the Vite UI build is a separate project from the engine.
interface TraceEvent {
  v: 1;
  ts: string;
  seq: number;
  eventId: string;
  runId: string;
  parentEventId?: string;
  correlationId?: string;
  llmToolCallId?: string;
  movement?: string;
  iteration?: number;
  kind: string;
  payload: unknown;
}

interface ParseSummary {
  events: TraceEvent[];
  skipped: number;
  unknownVersion: number;
}

function parseEventsJsonl(raw: string): ParseSummary {
  const out: TraceEvent[] = [];
  let skipped = 0;
  let unknownVersion = 0;
  for (const line of raw.split('\n')) {
    if (!line.trim()) continue;
    let parsed: unknown;
    try {
      parsed = JSON.parse(line);
    } catch {
      skipped++;
      continue;
    }
    if (!parsed || typeof parsed !== 'object') {
      skipped++;
      continue;
    }
    const obj = parsed as Record<string, unknown>;
    if (obj.v !== 1) {
      unknownVersion++;
      continue;
    }
    if (typeof obj.kind !== 'string' || typeof obj.seq !== 'number' || typeof obj.eventId !== 'string') {
      skipped++;
      continue;
    }
    out.push(obj as unknown as TraceEvent);
  }
  return { events: out, skipped, unknownVersion };
}

// Categorize for filter chips and color coding.
const CATEGORIES: Array<{ id: string; label: string; kinds: string[]; tone: string }> = [
  { id: 'run', label: 'Run', kinds: ['run_start', 'run_complete'], tone: 'bg-surface-2 text-slate-700 border-hairline' },
  { id: 'movement', label: 'Movement', kinds: ['movement_start', 'movement_complete', 'transition', 'complete'], tone: 'bg-blue-50 text-blue-800 border-blue-100' },
  { id: 'tool', label: 'Tool', kinds: ['tool_call', 'tool_result'], tone: 'bg-white text-slate-700 border-hairline' },
  { id: 'llm', label: 'LLM', kinds: ['llm_call_start', 'llm_call_end'], tone: 'bg-indigo-50 text-indigo-800 border-indigo-100' },
  { id: 'cache', label: 'Cache', kinds: ['cache_set', 'cache_hit', 'cache_invalidate'], tone: 'bg-amber-50 text-amber-800 border-amber-100' },
  { id: 'memory', label: 'Memory', kinds: ['memory_invalidate', 'memory_update_call', 'memory_handoff_write', 'memory_handoff_read', 'memory_delta_write', 'memory_delta_absorb', 'memory_snapshot_written', 'memory_snapshot_failed'], tone: 'bg-emerald-50 text-emerald-800 border-emerald-100' },
  { id: 'watchdog', label: 'Watchdog', kinds: ['watchdog_fire', 'followup_detected'], tone: 'bg-red-50 text-red-800 border-red-100' },
  { id: 'context', label: 'Context', kinds: ['context_action'], tone: 'bg-violet-50 text-violet-800 border-violet-100' },
];

/**
 * Color a duration bar by magnitude. Bars are inline next to tool_result /
 * llm_call_end rows so users can scan a timeline and spot the long tail at
 * a glance — XPostDetail taking 3 min stands out as red, a 200ms Read fades
 * to almost nothing.
 */
function durationBarStyle(ms: number): { widthPct: number; tone: string } {
  if (!Number.isFinite(ms) || ms <= 0) return { widthPct: 0, tone: 'bg-slate-200' };
  // Log scale: 100ms = 10%, 1s = 30%, 10s = 60%, 100s = 90%, >180s = 100%.
  const widthPct = Math.min(100, Math.max(4, Math.log10(ms) * 22 - 22));
  const tone = ms >= 60_000 ? 'bg-red-400'
    : ms >= 10_000 ? 'bg-orange-400'
    : ms >= 2_000 ? 'bg-amber-400'
    : ms >= 500 ? 'bg-emerald-400'
    : 'bg-slate-300';
  return { widthPct, tone };
}

function formatDurationLabel(ms: number): string {
  if (!Number.isFinite(ms) || ms < 0) return '?';
  if (ms < 1000) return `${Math.round(ms)}ms`;
  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
  const sec = Math.round(ms / 1000);
  return `${Math.floor(sec / 60)}m${(sec % 60).toString().padStart(2, '0')}s`;
}

function toneFor(kind: string): string {
  for (const c of CATEGORIES) if (c.kinds.includes(kind)) return c.tone;
  return 'bg-white text-slate-600 border-slate-200';
}

function categoryFor(kind: string): string {
  for (const c of CATEGORIES) if (c.kinds.includes(kind)) return c.id;
  return 'other';
}

function summarizePayload(event: TraceEvent): string {
  const p = event.payload as Record<string, unknown> | null;
  if (!p) return '';
  switch (event.kind) {
    case 'tool_call': {
      const args = p.args as Record<string, unknown> | undefined;
      const filePath = args?.['file_path'] ?? args?.['path'] ?? args?.['url'] ?? args?.['pattern'];
      return `${String(p.tool ?? '?')}${filePath ? ` ${filePath}` : ''}`;
    }
    case 'tool_result':
      return `${String(p.tool ?? '?')} ${p.isError ? '⚠ error' : 'ok'}${p.cacheHit ? ' (cached)' : ''} ${formatDurationLabel(Number(p.durationMs ?? 0))}`;
    case 'llm_call_start':
      return `iter=${p.iteration ?? '?'} msgs=${p.messageCount ?? '?'}`;
    case 'llm_call_end': {
      const tokens = (typeof p.promptTokens === 'number' && typeof p.completionTokens === 'number')
        ? ` in=${p.promptTokens} out=${p.completionTokens}`
        : '';
      const shape = (p.toolCalls as number) > 0 ? ` tools=${p.toolCalls}`
        : (p.textChars as number) > 0 ? ` text=${p.textChars}c`
        : '';
      return `${formatDurationLabel(Number(p.durationMs ?? 0))}${tokens}${shape}${p.hadError ? ' ⚠' : ''}`;
    }
    case 'cache_set':
      return `${String(p.tool ?? '?')} (${String(p.volatility ?? '?')})`;
    case 'cache_hit':
      return `${String(p.tool ?? '?')} from ${String(p.sourceMovement ?? '?')} (${p.ageMs ?? '?'}ms ago)`;
    case 'cache_invalidate':
    case 'memory_invalidate':
      return `${String(p.trigger ?? '')} → ${p.entriesEvicted ?? 0} entries`;
    case 'memory_update_call': {
      const counts = p.counts as Record<string, number> | null;
      if (!counts) return p.empty ? 'empty payload' : '';
      const parts: string[] = [];
      if (counts.factsAdded) parts.push(`facts +${counts.factsAdded}`);
      if (counts.factsMerged) parts.push(`facts merged ${counts.factsMerged}`);
      if (counts.decisionsAdded) parts.push(`decisions +${counts.decisionsAdded}`);
      if (counts.openQuestionsAdded) parts.push(`open_questions +${counts.openQuestionsAdded}`);
      if (counts.doNotRepeatAdded) parts.push(`do_not_repeat +${counts.doNotRepeatAdded}`);
      return parts.join(', ') || 'no changes';
    }
    case 'memory_handoff_write':
      return p.skipped ? `skipped: ${p.reason}` : `→ child #${p.subtaskIndex ?? '?'} (${p.factsCount ?? 0}f / ${p.decisionsCount ?? 0}d)`;
    case 'memory_handoff_read':
      return `from parent ${String(p.parentJobId ?? '?')}`;
    case 'memory_delta_write':
      return p.skipped ? `skipped: ${p.reason}` : `${p.childStatus} ${p.partial ? '(partial) ' : ''}(${p.factsCount ?? 0}f / ${p.decisionsCount ?? 0}d)`;
    case 'memory_delta_absorb':
      return `${String(p.outcome ?? '?')}${p.childJobId ? ` ← ${p.childJobId}` : ''}`;
    case 'memory_snapshot_written': {
      const parts: string[] = [];
      if (typeof p.facts === 'number') parts.push(`${p.facts}f`);
      if (typeof p.decisions === 'number') parts.push(`${p.decisions}d`);
      if (typeof p.openQuestions === 'number') parts.push(`${p.openQuestions}q`);
      const counts = parts.length ? ` (${parts.join('/')})` : '';
      const sizeKb = typeof p.bytes === 'number' ? ` ${(p.bytes / 1024).toFixed(1)}KB` : '';
      return `${String(p.status ?? '?')} → ${String(p.path ?? '?')}${counts}${sizeKb}`;
    }
    case 'memory_snapshot_failed':
      return `${String(p.status ?? '?')} write failed: ${String(p.error ?? '?')}`;
    case 'watchdog_fire':
      return `${String(p.kind2 ?? '')} at iter=${p.iteration ?? '?'}`;
    case 'followup_detected':
      return `movement=${String(p.movementName ?? '?')}`;
    case 'context_action':
      return `${String(p.type ?? '?')} ratio=${typeof p.ratio === 'number' ? (p.ratio * 100).toFixed(0) + '%' : '?'}`;
    case 'transition':
      return `→ ${String(p.nextStep ?? '?')}`;
    case 'complete':
      return `${String(p.status ?? '?')}`;
    case 'movement_start':
      return `visit ${p.visitCount ?? '?'}/${p.maxVisits ?? '?'}`;
    case 'movement_complete':
      return `→ ${String(p.next ?? '?')}`;
    case 'run_start':
      return `piece=${String(p.pieceName ?? '?')}`;
    case 'run_complete': {
      const cancel = p.cancel as { phase?: string; movement?: string } | undefined;
      const cancelInfo = cancel?.phase ? ` cancel:${cancel.phase}@${cancel.movement ?? '?'}` : '';
      const snapshot = p.memorySnapshotPath ? ` snapshot:${String(p.memorySnapshotPath).replace(/^logs\//, '')}` : '';
      return `${String(p.status ?? '?')}${p.abortReason ? ` (${p.abortReason})` : ''}${cancelInfo}${snapshot}`;
    }
    default:
      return '';
  }
}

interface TraceTabProps {
  taskId: number;
}

export function TraceTab({ taskId }: TraceTabProps) {
  const [refreshKey, setRefreshKey] = useState(0);
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
  const [enabledCategories, setEnabledCategories] = useState<Set<string>>(
    new Set(CATEGORIES.map((c) => c.id).concat(['other'])),
  );
  const [movementFilter, setMovementFilter] = useState<string>('all');
  const [search, setSearch] = useState<string>('');

  const { data, isLoading, error } = useQuery({
    queryKey: ['trace-events', taskId, refreshKey],
    queryFn: async () => {
      try {
        return await fetchLocalFileContent(taskId, 'logs', 'events.jsonl');
      } catch (err) {
        // events.jsonl が存在しない初回タスクなどは空扱い
        if (err instanceof Error && /not.*found|404/i.test(err.message)) return '';
        throw err;
      }
    },
    refetchInterval: 5000, // 自動 5 秒ポーリング
    staleTime: 0,
  });

  const summary = useMemo(() => {
    if (!data) return { events: [], skipped: 0, unknownVersion: 0 };
    return parseEventsJsonl(data);
  }, [data]);

  const movements = useMemo(() => {
    const set = new Set<string>();
    for (const e of summary.events) if (e.movement) set.add(e.movement);
    return ['all', ...Array.from(set).sort()];
  }, [summary.events]);

  /**
   * Per-tool aggregation: total wall-clock time spent + call count, sorted
   * by total time descending. Surfaces "XPostDetail consumed 12 min over
   * 4 calls" at a glance — the kind of thing buried in 500-line event
   * lists otherwise. Only counts non-cache hits so cached results don't
   * dilute the signal.
   */
  const toolTimings = useMemo(() => {
    const stats = new Map<string, { totalMs: number; count: number; errors: number; maxMs: number }>();
    let llmTotalMs = 0;
    let llmCount = 0;
    for (const e of summary.events) {
      const p = e.payload as Record<string, unknown> | null;
      if (!p) continue;
      if (e.kind === 'tool_result' && !p.cacheHit) {
        const tool = String(p.tool ?? '?');
        const ms = Number(p.durationMs ?? 0);
        const cur = stats.get(tool) ?? { totalMs: 0, count: 0, errors: 0, maxMs: 0 };
        cur.totalMs += ms;
        cur.count += 1;
        if (p.isError) cur.errors += 1;
        if (ms > cur.maxMs) cur.maxMs = ms;
        stats.set(tool, cur);
      }
      if (e.kind === 'llm_call_end') {
        llmTotalMs += Number(p.durationMs ?? 0);
        llmCount += 1;
      }
    }
    const tools = Array.from(stats.entries())
      .map(([tool, s]) => ({ tool, ...s }))
      .sort((a, b) => b.totalMs - a.totalMs);
    return { tools, llm: { totalMs: llmTotalMs, count: llmCount } };
  }, [summary.events]);

  const filtered = useMemo(() => {
    const term = search.trim().toLowerCase();
    return summary.events.filter((e) => {
      const cat = categoryFor(e.kind);
      if (!enabledCategories.has(cat)) return false;
      if (movementFilter !== 'all' && e.movement !== movementFilter) return false;
      if (term) {
        const haystack = `${e.kind} ${e.movement ?? ''} ${summarizePayload(e)} ${JSON.stringify(e.payload)}`.toLowerCase();
        if (!haystack.includes(term)) return false;
      }
      return true;
    });
  }, [summary.events, enabledCategories, movementFilter, search]);

  // Group consecutive events by correlationId so tool_call ↔ tool_result are
  // visually paired without manual interaction.
  const grouped = useMemo(() => {
    const groups: Array<{ correlationId?: string; events: TraceEvent[] }> = [];
    for (const e of filtered) {
      const last = groups[groups.length - 1];
      if (e.correlationId && last && last.correlationId === e.correlationId) {
        last.events.push(e);
      } else {
        groups.push({ correlationId: e.correlationId, events: [e] });
      }
    }
    return groups;
  }, [filtered]);

  // Auto-collapse expansion state when raw data is reloaded from polling.
  useEffect(() => {
    setExpanded((prev) => {
      const next = new Set<string>();
      for (const id of prev) {
        if (summary.events.some((e) => e.eventId === id)) next.add(id);
      }
      return next;
    });
  }, [summary.events]);

  function toggleCategory(id: string): void {
    setEnabledCategories((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  function toggleExpanded(eventId: string): void {
    setExpanded((prev) => {
      const next = new Set(prev);
      if (next.has(eventId)) next.delete(eventId);
      else next.add(eventId);
      return next;
    });
  }

  if (isLoading) {
    return <div className="text-[13px] text-slate-500 p-4">読み込み中...</div>;
  }

  if (error) {
    return <div className="text-[13px] text-red-600 p-4">エラー: {String(error)}</div>;
  }

  if (summary.events.length === 0) {
    return (
      <div className="text-xs text-slate-500 p-4 leading-relaxed">
        <div className="section-label mb-1.5">no trace yet</div>
        events.jsonl がまだ存在しません。タスクが少なくとも一度実行されると、engine 内部動作のトレースがここに表示されます。
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-3">
      {/* Filter bar — sticky so it stays visible while scrolling. */}
      <div className="sticky top-0 z-10 -mx-3 px-3 -mt-3 pt-3 pb-2 bg-surface/95 backdrop-blur border-b border-hairline">
        <div className="flex flex-wrap gap-1 mb-2">
          {CATEGORIES.map((c) => {
            const on = enabledCategories.has(c.id);
            return (
              <button
                key={c.id}
                onClick={() => toggleCategory(c.id)}
                className={`h-6 px-2 text-[10px] font-medium border rounded transition-colors ${
                  on ? c.tone : 'bg-white text-slate-400 border-hairline hover:text-slate-600'
                }`}
              >
                {c.label}
              </button>
            );
          })}
          <button
            onClick={() => toggleCategory('other')}
            className={`h-6 px-2 text-[10px] font-medium border rounded transition-colors ${
              enabledCategories.has('other')
                ? 'bg-white text-slate-700 border-hairline'
                : 'bg-white text-slate-400 border-hairline-soft hover:text-slate-600'
            }`}
          >
            Other
          </button>
        </div>
        <div className="flex gap-1.5 items-center">
          <select
            value={movementFilter}
            onChange={(e) => setMovementFilter(e.target.value)}
            className="h-7 text-2xs border border-hairline rounded-md px-2 bg-white text-slate-700 focus:outline-none focus:ring-2 focus:ring-accent-ring"
          >
            {movements.map((m) => (
              <option key={m} value={m}>{m === 'all' ? 'all movements' : m}</option>
            ))}
          </select>
          <div className="flex-1 min-w-0 flex items-center gap-1.5 bg-white border border-hairline rounded-md h-7 px-2 focus-within:ring-2 focus-within:ring-accent-ring">
            <svg aria-hidden="true" className="w-3 h-3 text-slate-400 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
              type="text"
              placeholder="search kind, movement, payload..."
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              className="text-2xs flex-1 min-w-0 bg-transparent outline-none text-slate-900 placeholder:text-slate-400"
            />
          </div>
          <button
            onClick={() => setRefreshKey((k) => k + 1)}
            className="h-7 w-7 flex items-center justify-center text-xs border border-hairline rounded-md text-slate-500 bg-white hover:bg-surface transition-colors"
            title="手動更新（自動 5 秒ごとにも更新されます）"
          >
            ↻
          </button>
        </div>
        <div className="text-[10px] text-slate-500 font-mono tabular-nums mt-1.5">
          {filtered.length} / {summary.events.length} events
          {summary.skipped > 0 && <span className="ml-2 text-amber-600">⚠ {summary.skipped} skipped</span>}
          {summary.unknownVersion > 0 && <span className="ml-2 text-amber-600">⚠ {summary.unknownVersion} unknown version</span>}
        </div>
      </div>

      {/* Tool / LLM time aggregation — surfaces "what ate the wall-clock". */}
      {(toolTimings.tools.length > 0 || toolTimings.llm.count > 0) && (
        <div className="border border-hairline rounded-md p-2 bg-white">
          <div className="section-label mb-1.5">time by source</div>
          <div className="flex flex-col gap-0.5">
            {toolTimings.llm.count > 0 && (() => {
              const bar = durationBarStyle(toolTimings.llm.totalMs);
              return (
                <div className="flex items-center gap-2 text-2xs font-mono">
                  <span className="min-w-[14ch] text-indigo-700 shrink-0">llm × {toolTimings.llm.count}</span>
                  <div className="flex-1 min-w-0 h-2 bg-slate-100 rounded relative overflow-hidden">
                    <div className={`${bar.tone} h-full`} style={{ width: `${bar.widthPct}%` }} />
                  </div>
                  <span className="min-w-[6ch] text-right tabular-nums text-slate-700 shrink-0">
                    {formatDurationLabel(toolTimings.llm.totalMs)}
                  </span>
                </div>
              );
            })()}
            {toolTimings.tools.map(({ tool, totalMs, count, errors, maxMs }) => {
              const bar = durationBarStyle(totalMs);
              return (
                <div key={tool} className="flex items-center gap-2 text-2xs font-mono">
                  <span className={`min-w-[14ch] shrink-0 truncate ${errors > 0 ? 'text-red-700' : 'text-slate-700'}`} title={tool}>
                    {tool} × {count}
                    {errors > 0 ? <span className="text-red-600"> ⚠{errors}</span> : null}
                  </span>
                  <div className="flex-1 min-w-0 h-2 bg-slate-100 rounded relative overflow-hidden">
                    <div className={`${bar.tone} h-full`} style={{ width: `${bar.widthPct}%` }} />
                  </div>
                  <span className="min-w-[6ch] text-right tabular-nums text-slate-700 shrink-0" title={`max ${formatDurationLabel(maxMs)}`}>
                    {formatDurationLabel(totalMs)}
                  </span>
                </div>
              );
            })}
          </div>
          <div className="text-[10px] text-slate-500 mt-1.5">
            総時間 (cache hit 除外、max は個別呼び出しの最大値)
          </div>
        </div>
      )}

      {/* Event list */}
      <div className="flex flex-col">
        {grouped.map((group, gi) => {
          const grouped_ = group.correlationId && group.events.length > 1;
          return (
            <div key={gi} className={grouped_ ? 'border-l-2 border-hairline pl-2 my-0.5' : ''}>
              {group.events.map((e) => {
                const open = expanded.has(e.eventId);
                const tone = toneFor(e.kind);
                return (
                  <div key={e.eventId} className={`border rounded-md text-xs mb-1 ${tone}`}>
                    <button
                      onClick={() => toggleExpanded(e.eventId)}
                      className="w-full text-left px-2 py-1.5 flex items-center gap-2 cursor-pointer"
                    >
                      <span className="font-mono text-[10px] text-slate-500 shrink-0 w-[12ch] tabular-nums">
                        {new Date(e.ts).toLocaleTimeString(undefined, { hour12: false })}
                      </span>
                      <span className="font-mono font-semibold text-2xs shrink-0 min-w-[14ch]">{e.kind}</span>
                      {e.movement && (
                        <span className="font-mono text-[10px] text-slate-500 shrink-0">
                          {e.movement}{typeof e.iteration === 'number' ? `:${e.iteration}` : ''}
                        </span>
                      )}
                      <span className="flex-1 truncate font-mono text-2xs">{summarizePayload(e)}</span>
                      {(() => {
                        // Inline magnitude bar for any event carrying a durationMs.
                        // Visual sort: a 3-minute fetch jumps off the screen even
                        // when scrolled past, so you don't need to read every row.
                        const p = e.payload as Record<string, unknown> | null;
                        const ms = (e.kind === 'tool_result' || e.kind === 'llm_call_end') && p
                          ? Number(p.durationMs ?? 0)
                          : 0;
                        if (ms <= 0) return null;
                        const bar = durationBarStyle(ms);
                        return (
                          <div className="w-[60px] h-1.5 bg-slate-100 rounded shrink-0 overflow-hidden" title={`${ms}ms`}>
                            <div className={`${bar.tone} h-full`} style={{ width: `${bar.widthPct}%` }} />
                          </div>
                        );
                      })()}
                      <span className="text-slate-400 shrink-0 text-[10px]">{open ? '▾' : '▸'}</span>
                    </button>
                    {open && (
                      <div className="border-t border-current/15 px-2 py-1.5 bg-white/60 font-mono text-2xs whitespace-pre overflow-x-auto leading-relaxed">
                        {JSON.stringify(e, null, 2)}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}
