import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { LocalTask, LocalTaskComment } from '../../api';
import { ChatMessage } from './ChatMessage';
import { isThinkingComment, hasTrailingThinking } from './thinkingUtils';
import { ChatPetOverlay } from '../pets/ChatPetOverlay';
import { groupCommentsByMovement, MovementGroupExpanded } from './MovementGroup';
import { SubtaskInlineCard } from './SubtaskInlineCard';
import { useJobStream } from '../../hooks/useJobStream';
import { extractStreamingField, CONTENT_FIELD } from '../../lib/streamFieldExtract';


async function toBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = String(reader.result ?? '');
      resolve(result.includes(',') ? result.split(',')[1]! : result);
    };
    reader.onerror = () => reject(reader.error ?? new Error('file read error'));
    reader.readAsDataURL(file);
  });
}

interface ChatPaneProps {
  task: LocalTask;
  comments: LocalTaskComment[];
  onSubmit: (body: string, attachments?: Array<{ name: string; contentBase64: string }>) => Promise<void>;
  onCancel?: () => Promise<void>;
  onOpenDetail?: () => void;
}

export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: ChatPaneProps) {
  const [draft, setDraft] = useState('');
  const [attachments, setAttachments] = useState<Array<{ name: string; contentBase64: string }>>([]);
  const [submitting, setSubmitting] = useState(false);
  const [cancelling, setCancelling] = useState(false);
  const [sendError, setSendError] = useState<string | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  // Snapshot of comments.length at submit start. We hold "submitting" until
  // (a) the new user comment is reflected in the list AND (b) the job is
  // visibly busy (= picked up by a worker). Without this, the gap between the
  // POST resolving and the worker dispatching the job lets the user fire
  // multiple sends in a row.
  const submitBaselineRef = useRef<number | null>(null);
  const submitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [isAtBottom, setIsAtBottom] = useState(true);
  const [newMessageCount, setNewMessageCount] = useState(0);
  const prevCommentCountRef = useRef(comments.length);

  const checkIfAtBottom = useCallback(() => {
    const el = scrollRef.current;
    if (!el) return true;
    return el.scrollHeight - el.scrollTop - el.clientHeight < 80;
  }, []);

  const scrollToBottom = useCallback(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      setIsAtBottom(true);
      setNewMessageCount(0);
    }
  }, []);

  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const handler = () => setIsAtBottom(checkIfAtBottom());
    el.addEventListener('scroll', handler, { passive: true });
    return () => el.removeEventListener('scroll', handler);
  }, [checkIfAtBottom]);

  useEffect(() => {
    const delta = comments.length - prevCommentCountRef.current;
    prevCommentCountRef.current = comments.length;
    if (delta <= 0) return;
    if (isAtBottom) {
      requestAnimationFrame(() => {
        if (scrollRef.current) {
          scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
        }
      });
    } else {
      setNewMessageCount(prev => prev + delta);
    }
  }, [comments.length, isAtBottom]);

  const handleFiles = async (files: FileList | null) => {
    if (!files || files.length === 0) return;
    const converted = await Promise.all(
      Array.from(files).map(async f => ({ name: f.name, contentBase64: await toBase64(f) }))
    );
    setAttachments(prev => [...prev, ...converted]);
  };

  const removeAttachment = (name: string) => {
    setAttachments(prev => prev.filter(a => a.name !== name));
  };

  const releaseSubmitting = () => {
    if (submitTimeoutRef.current) {
      clearTimeout(submitTimeoutRef.current);
      submitTimeoutRef.current = null;
    }
    submitBaselineRef.current = null;
    setSubmitting(false);
  };

  const handleSubmit = async () => {
    if ((!draft.trim() && attachments.length === 0) || submitting) return;
    setSendError(null);
    setSubmitting(true);
    submitBaselineRef.current = comments.length;
    try {
      await onSubmit(draft, attachments.length > 0 ? attachments : undefined);
      setDraft('');
      setAttachments([]);
      // Hold the lock until the agent is visibly responding (see effect below).
      // Safety net: if the worker never picks the job up (queue stuck, server
      // crash, etc.), release the lock after 10s so the user isn't trapped.
      if (submitTimeoutRef.current) clearTimeout(submitTimeoutRef.current);
      submitTimeoutRef.current = setTimeout(releaseSubmitting, 10000);
    } catch (e) {
      setSendError(e instanceof Error && e.message ? e.message : '送信に失敗しました');
      releaseSubmitting();
    }
  };

  const handlePaste = async (e: React.ClipboardEvent) => {
    const items = e.clipboardData?.items;
    if (!items) return;
    const files: File[] = [];
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      if (item.kind === 'file') {
        const file = item.getAsFile();
        if (file) files.push(file);
      }
    }
    if (files.length === 0) return;
    e.preventDefault();
    const converted = await Promise.all(
      files.map(async f => {
        const name = f.name === 'image.png' ? `paste-${Date.now()}.png` : f.name;
        return { name, contentBase64: await toBase64(f) };
      })
    );
    setAttachments(prev => [...prev, ...converted]);
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
      e.preventDefault();
      handleSubmit();
    }
  };

  const handleCancel = async () => {
    if (!onCancel || cancelling) return;
    setCancelling(true);
    try {
      await onCancel();
    } finally {
      setCancelling(false);
    }
  };

  const jobStatus = task.latestJob?.status;
  const { promptProgress, streamingText, toolCallStream, connected } = useJobStream(task.id, jobStatus);

  // Most-recent content-field tool with decoded content to show live.
  const liveToolContent = useMemo(() => {
    const entries = Object.values(toolCallStream).filter(e => e.name in CONTENT_FIELD);
    for (let k = entries.length - 1; k >= 0; k--) {
      const text = extractStreamingField(entries[k].name, entries[k].rawArgs);
      if (text) return { name: entries[k].name, text };
    }
    return null;
  }, [toolCallStream]);
  const liveToolRef = useRef<HTMLPreElement | null>(null);
  useEffect(() => {
    if (liveToolRef.current) liveToolRef.current.scrollTop = liveToolRef.current.scrollHeight;
  }, [liveToolContent?.text]);
  const isBusy = jobStatus === 'running' || jobStatus === 'dispatching' || jobStatus === 'waiting_subtasks';
  const isWaitingSubtasks = jobStatus === 'waiting_subtasks';
  const canInterject = jobStatus === 'running' || jobStatus === 'waiting_subtasks';
  const inputLocked = jobStatus === 'dispatching';

  // Release the submit lock once the agent is visibly responding: the new user
  // comment is reflected in the list AND the job has been picked up by a worker
  // (isBusy=true). This bridges the queued->dispatching gap where a stale
  // re-enabled send button would otherwise allow a double submit.
  useEffect(() => {
    if (!submitting) return;
    const baseline = submitBaselineRef.current;
    if (baseline === null) return;
    if (comments.length > baseline && isBusy) {
      releaseSubmitting();
    }
  }, [submitting, comments.length, isBusy]);

  // Clear the safety-net timeout on unmount.
  useEffect(() => {
    return () => {
      if (submitTimeoutRef.current) clearTimeout(submitTimeoutRef.current);
    };
  }, []);

  // During an active run, suppress the trailing thinking comment so the
  // live SSE preview is the single source of truth for in-flight text.
  // We keep the comment in history (MovementGroup will render it once the
  // movement completes). On SSE disconnect we keep it visible as fallback.
  const visibleComments = useMemo(() => {
    if (!isBusy) return comments;
    if (!connected) return comments; // SSE disconnect fallback
    if (!hasTrailingThinking(comments)) return comments;
    return comments.slice(0, -1);
  }, [comments, isBusy, connected]);

  const groupedItems = useMemo(() => groupCommentsByMovement(visibleComments), [visibleComments]);
  const animatingIdx = isBusy && hasTrailingThinking(visibleComments) ? visibleComments.length - 1 : -1;

  return (
    <div className="relative flex flex-col h-full overflow-hidden">
      {/* Tablet+ only. Mobile renders its own app-level instance so the
          pet is visible across all mobile tabs (Progress / Files / Trace /
          Browser / SSH), not just Chat. */}
      <ChatPetOverlay
        taskId={task.id}
        taskStatus={task.latestJob?.status ?? null}
        currentActivity={task.latestJob?.currentActivity ?? null}
        workerId={task.latestJob?.workerId ?? null}
        lastBackendId={task.latestJob?.lastBackendId ?? null}
        className="hidden sm:block"
      />
      {/* Header */}
      <div className="flex-shrink-0 border-b border-hairline bg-white px-4 py-2.5">
        <div className="flex items-center justify-between gap-2">
          <div className="min-w-0">
            <h2 className="text-sm font-semibold text-slate-900 truncate">{task.title}</h2>
            <div className="text-[10px] text-slate-400 font-mono tabular-nums">#{task.id} · {task.pieceName}</div>
          </div>
          <div className="flex items-center gap-1.5 flex-shrink-0">
            {isBusy && (
              <div className={`inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded border ${
                isWaitingSubtasks
                  ? 'border-indigo-100 bg-indigo-50'
                  : 'border-emerald-100 bg-emerald-50'
              }`}>
                <span className={`w-1.5 h-1.5 rounded-full animate-pulse ${
                  isWaitingSubtasks ? 'bg-indigo-500' : 'bg-emerald-500'
                }`} />
                <span className={`text-[10px] font-medium ${
                  isWaitingSubtasks ? 'text-indigo-700' : 'text-emerald-700'
                }`}>
                  {isWaitingSubtasks ? 'subtasks' : 'running'}
                </span>
              </div>
            )}
            {onOpenDetail && (
              <button
                onClick={onOpenDetail}
                className="px-2.5 h-7 text-2xs font-medium text-slate-700 border border-hairline bg-white hover:bg-surface rounded-md transition-colors"
                title="詳細を表示"
              >
                詳細
              </button>
            )}
          </div>
        </div>
      </div>

      {/* Messages */}
      <div className="flex-1 relative min-h-0 overflow-x-hidden">
      <div ref={scrollRef} className="absolute inset-0 overflow-y-auto overflow-x-hidden p-4">
        <div className="max-w-3xl mx-auto min-w-0 flex flex-col gap-3">
          {comments.length === 0 && (
            <div className="text-center text-slate-400 text-[13px] py-8">
              メッセージはまだありません
            </div>
          )}
          {(() => {
            let commentIdx = 0;
            return groupedItems.map((item, gi) => {
              if (item.type === 'movement') {
                const startIdx = commentIdx;
                commentIdx += item.inner.length;
                return (
                  <MovementGroupExpanded
                    key={`mg-${gi}`}
                    item={item}
                    taskId={task.id}
                    isLast={gi === groupedItems.length - 1}
                    isRunning={isBusy}
                    animatingIdx={animatingIdx}
                    startIdx={startIdx}
                  />
                );
              }
              const idx = commentIdx;
              commentIdx++;
              return (
                <ChatMessage
                  key={item.comment.id}
                  comment={item.comment}
                  taskId={task.id}
                  isStaleThinking={isThinkingComment(item.comment) && idx !== animatingIdx}
                />
              );
            });
          })()}

          {isWaitingSubtasks && task.subtasks && task.subtasks.length > 0 && (
            <SubtaskInlineCard
              subtasks={task.subtasks}
              subtaskCount={task.subtaskCount ?? task.subtasks.length}
              subtaskCompleted={task.subtaskCompleted ?? 0}
            />
          )}

          {isBusy && !isWaitingSubtasks && (
            <div className="flex justify-start">
              {promptProgress ? (
                <div className="inline-flex items-center gap-2 px-3 py-1.5 bg-surface border border-hairline rounded-md text-2xs text-slate-600 min-w-[180px]">
                  <div className="flex-1 min-w-0">
                    <div className="flex items-center justify-between mb-1">
                      <span>Processing</span>
                      <span className="font-mono tabular-nums">{promptProgress.percent}%</span>
                    </div>
                    <div className="w-full bg-slate-200 rounded-full h-1">
                      <div
                        className="bg-emerald-500 h-1 rounded-full transition-all duration-300"
                        style={{ width: `${promptProgress.percent}%` }}
                      />
                    </div>
                  </div>
                </div>
              ) : streamingText ? (
                <div className="max-w-[80%] min-w-0 px-3 py-2 bg-white border border-hairline rounded-lg text-[13px] text-slate-800 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] opacity-70">
                  {streamingText}
                  <span className="inline-block w-0.5 h-3.5 bg-slate-400 animate-pulse ml-0.5 align-text-bottom" />
                </div>
              ) : liveToolContent ? (
                <div className="max-w-[80%] min-w-0 w-full px-3 py-2 bg-slate-50 border border-hairline rounded-lg">
                  <div className="text-2xs text-slate-500 mb-1 font-mono">{liveToolContent.name} 生成中…</div>
                  <pre ref={liveToolRef} className="max-h-64 overflow-auto text-[12px] text-slate-800 whitespace-pre-wrap break-words [overflow-wrap:anywhere] m-0">
                    {liveToolContent.text}
                    <span className="inline-block w-0.5 h-3.5 bg-slate-400 animate-pulse ml-0.5 align-text-bottom" />
                  </pre>
                </div>
              ) : (
                <div className="inline-flex items-center gap-2 px-2.5 py-1 bg-surface border border-hairline rounded-md text-2xs text-slate-600">
                  <svg className="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                    <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                    <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
                  </svg>
                  エージェントが応答を生成中...
                </div>
              )}
            </div>
          )}
        </div>
      </div>

        {/* Scroll-to-bottom button */}
        {!isAtBottom && (
          <button
            onClick={scrollToBottom}
            className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-200 rounded-full shadow-md text-xs text-slate-600 hover:bg-slate-50 transition-colors z-10"
          >
            <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M4 6l4 4 4-4" />
            </svg>
            {newMessageCount > 0 ? (
              <span className="text-blue-600 font-medium">{newMessageCount} 件の新着</span>
            ) : (
              <span>最新へ</span>
            )}
          </button>
        )}
      </div>

      {/* Composer */}
      <div className="flex-shrink-0 border-t border-hairline bg-white p-3" style={{ paddingBottom: 'calc(12px + env(safe-area-inset-bottom, 0px))' }}>
        {isBusy && (
          <div className={`flex items-center gap-2 mb-2 px-2.5 py-1 rounded-md text-2xs ${
            canInterject
              ? 'bg-amber-50 border border-amber-100 text-amber-700'
              : 'bg-blue-50 border border-blue-100 text-blue-700'
          }`}>
            <svg className="w-3 h-3 animate-spin flex-shrink-0" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
            </svg>
            <span>{canInterject ? 'エージェント実行中 — メッセージで指示を送れます' : 'エージェントがタスクを実行中です。少々お待ちください。'}</span>
          </div>
        )}
        {sendError && !isBusy && (
          <div className="flex items-center justify-between gap-2 mb-2 px-2.5 py-1 bg-red-50 border border-red-100 rounded-md text-2xs text-red-700">
            <span className="truncate">⚠ {sendError}</span>
            <button
              type="button"
              onClick={() => void handleSubmit()}
              disabled={submitting}
              className="flex-shrink-0 px-2 h-6 bg-white border border-red-200 rounded text-[10px] font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
            >
              再送信
            </button>
          </div>
        )}
        {attachments.length > 0 && (
          <div className="flex flex-wrap gap-1 mb-2">
            {attachments.map(a => (
              <span key={a.name} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-surface-2 border border-hairline rounded text-[10px] text-slate-700 font-mono">
                {a.name}
                <button onClick={() => removeAttachment(a.name)} className="text-slate-400 hover:text-slate-700 ml-0.5">&times;</button>
              </span>
            ))}
          </div>
        )}
        <div className="flex gap-1.5 items-end">
          <input
            ref={fileInputRef}
            type="file"
            multiple
            className="hidden"
            onChange={e => { void handleFiles(e.target.files); e.target.value = ''; }}
          />
          <button
            onClick={() => fileInputRef.current?.click()}
            disabled={inputLocked || submitting}
            className="flex-shrink-0 w-9 h-9 flex items-center justify-center text-slate-500 hover:text-slate-900 hover:bg-surface rounded-md transition-colors disabled:opacity-50 disabled:hover:bg-transparent disabled:cursor-not-allowed"
            title="ファイルを添付"
            aria-label="ファイルを添付"
          >
            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
            </svg>
          </button>
          <textarea
            value={draft}
            onChange={e => setDraft(e.target.value)}
            onKeyDown={handleKeyDown}
            onPaste={e => void handlePaste(e)}
            rows={2}
            disabled={inputLocked}
            placeholder={inputLocked ? 'ジョブ割り当て中...' : canInterject ? '実行中のエージェントに指示...' : 'メッセージを入力... (Ctrl+Enter で送信)'}
            className="flex-1 resize-y border border-hairline rounded-md px-2.5 py-2 text-sm text-slate-900 outline-none focus:border-accent focus:ring-2 focus:ring-accent-ring min-h-[56px] disabled:bg-surface disabled:text-slate-400 disabled:cursor-not-allowed transition-shadow"
          />
          {isBusy && onCancel ? (
            <div className="flex gap-1.5">
              {canInterject && (
                <button
                  disabled={submitting || (!draft.trim() && attachments.length === 0)}
                  onClick={handleSubmit}
                  className="px-3 h-9 bg-amber-500 text-white rounded-md text-xs font-semibold disabled:opacity-50 hover:bg-amber-600 flex-shrink-0 transition-colors"
                >
                  割り込み
                </button>
              )}
              <button
                disabled={cancelling}
                onClick={() => void handleCancel()}
                className="px-3 h-9 bg-white border border-red-200 text-red-700 rounded-md text-xs font-semibold disabled:opacity-50 hover:bg-red-50 flex-shrink-0 transition-colors"
                title="エージェントの実行を停止"
              >
                {cancelling ? '停止中...' : '停止'}
              </button>
            </div>
          ) : (
            <button
              disabled={submitting || inputLocked || (!draft.trim() && attachments.length === 0)}
              onClick={handleSubmit}
              className="px-3 h-9 bg-accent text-accent-fg rounded-md text-xs font-semibold disabled:opacity-50 hover:bg-accent-deep flex-shrink-0 transition-colors"
            >
              送信
            </button>
          )}
        </div>
      </div>
    </div>
  );
}
