/**
 * TaskPage — the right-pane view for a selected task in document mode.
 *
 * Layout (replaces the old single-column DocumentTaskStream):
 *
 *   ┌───────────────────────────────────────────────────────────┐
 *   │  Header: title · status · branch · Stop                   │
 *   ├────────────────────────┬──────────────────────────────────┤
 *   │ Changed files (~30%)   │  Transcript feed (scrollable)    │
 *   │ src/foo.rs             │  [user] do thing                 │
 *   │ src/bar.rs             │  [tool] Read foo.rs              │
 *   │ (selected file diff)   │                                  │
 *   │ + added                │                                  │
 *   │ - removed              │                                  │
 *   │                        ├──────────────────────────────────┤
 *   │                        │  Composer (sticky bottom)        │
 *   └────────────────────────┴──────────────────────────────────┘
 *
 * Diff data comes from getTaskDiff(); we parse it with parseDiff (reused
 * from OverlayDiffViewer) so we don't duplicate the parser. The file
 * list on the left is the parsed file paths; selecting one filters the
 * diff render to that single file. Refresh button re-fetches on demand;
 * by default the diff loads once on mount + after each task status
 * change so the user sees fresh changes as soon as the daemon commits.
 */
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { SimpleMarkdown } from "../SimpleMarkdown";
import {
  useTaskSubscription,
  type TaskOutputEvent,
} from "../../hooks/useTaskSubscription";
import { getTaskOutput, getTaskDiff, sendTaskMessage, stopTask } from "../../lib/api";
import { parseDiff, DiffFileView, type DiffFile } from "../mesh/OverlayDiffViewer";

interface TaskPageProps {
  taskId: string;
  /** Human label for the task header (e.g. "orchestrator", step name). */
  label: string;
  /** True for tasks spawned via the directive's `+ New ephemeral task`
   *  action (no backing step). Drives the optional "Merge to base"
   *  affordance — step-spawned tasks merge via the directive's PR. */
  ephemeral?: boolean;
  /** Current task status — drives whether merge button is enabled. */
  status?: string;
}

// Module-level caches so navigation between tasks is instant on re-visit.
const entriesCache = new Map<string, TaskOutputEvent[]>();
const diffCache = new Map<string, string>();

export function TaskPage({ taskId, label, ephemeral, status }: TaskPageProps) {
  // ---- Transcript state (mirrors the old DocumentTaskStream) ----
  const [entries, setEntries] = useState<TaskOutputEvent[]>(
    () => entriesCache.get(taskId) ?? [],
  );
  const [loading, setLoading] = useState(!entriesCache.has(taskId));
  const [isStreaming, setIsStreaming] = useState(false);
  const [comment, setComment] = useState("");
  const [sending, setSending] = useState(false);
  const [sendError, setSendError] = useState<string | null>(null);
  const [stopping, setStopping] = useState(false);
  const transcriptRef = useRef<HTMLDivElement>(null);
  const composerRef = useRef<HTMLDivElement>(null);
  const autoScrollRef = useRef(true);
  const [showResumeScroll, setShowResumeScroll] = useState(false);

  // ---- Diff state ----
  const [diffText, setDiffText] = useState<string>(() => diffCache.get(taskId) ?? "");
  const [diffLoading, setDiffLoading] = useState(false);
  const [diffError, setDiffError] = useState<string | null>(null);
  const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null);
  const [collapsedFiles, setCollapsedFiles] = useState<Set<string>>(new Set());

  // Reset both panes when the task id changes.
  useEffect(() => {
    setSelectedFilePath(null);
    setCollapsedFiles(new Set());
    setDiffText(diffCache.get(taskId) ?? "");
  }, [taskId]);

  // ---- Load historical transcript on task change. ----
  useEffect(() => {
    let cancelled = false;
    const cached = entriesCache.get(taskId);
    if (cached) {
      setEntries(cached);
      setLoading(false);
    } else {
      setEntries([]);
      setLoading(true);
    }
    setIsStreaming(false);

    getTaskOutput(taskId)
      .then((res) => {
        if (cancelled) return;
        const mapped: TaskOutputEvent[] = res.entries.map((e) => ({
          taskId: e.taskId,
          messageType: e.messageType,
          content: e.content,
          toolName: e.toolName,
          toolInput: e.toolInput,
          isError: e.isError,
          costUsd: e.costUsd,
          durationMs: e.durationMs,
          isPartial: false,
        }));
        entriesCache.set(taskId, mapped);
        setEntries(mapped);
      })
      .catch((err) => {
        if (cancelled) return;
        // eslint-disable-next-line no-console
        console.error("Failed to load task output history:", err);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [taskId]);

  // ---- Load diff on task change + after each task status update. ----
  const refreshDiff = useCallback(async () => {
    setDiffLoading(true);
    setDiffError(null);
    try {
      const res = await getTaskDiff(taskId);
      if (res.success && res.diff !== null) {
        setDiffText(res.diff);
        diffCache.set(taskId, res.diff);
      } else if (res.error) {
        setDiffError(res.error);
      } else {
        setDiffText("");
        diffCache.set(taskId, "");
      }
    } catch (e) {
      setDiffError(e instanceof Error ? e.message : "Failed to load diff");
    } finally {
      setDiffLoading(false);
    }
  }, [taskId]);

  useEffect(() => {
    void refreshDiff();
  }, [refreshDiff]);

  // ---- Live subscription. ----
  const handleOutput = useCallback(
    (event: TaskOutputEvent) => {
      if (event.isPartial) return;
      setEntries((prev) => {
        const next = [...prev, event];
        entriesCache.set(taskId, next);
        return next;
      });
      setIsStreaming(true);
    },
    [taskId],
  );

  const handleUpdate = useCallback(
    (event: { status: string }) => {
      const terminal = [
        "completed",
        "failed",
        "cancelled",
        "interrupted",
        "merged",
        "done",
      ];
      if (terminal.includes(event.status)) {
        setIsStreaming(false);
        // Daemon may have written final commits — refresh the diff.
        void refreshDiff();
      } else if (event.status === "running") {
        setIsStreaming(true);
      }
    },
    [refreshDiff],
  );

  useTaskSubscription({
    taskId,
    subscribeOutput: true,
    onOutput: handleOutput,
    onUpdate: handleUpdate,
  });

  // ---- Auto-scroll (transcript pane). ----
  useEffect(() => {
    if (autoScrollRef.current && transcriptRef.current) {
      transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight;
    }
  }, [entries]);

  useEffect(() => {
    if (!loading && transcriptRef.current) {
      transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight;
      autoScrollRef.current = true;
      setShowResumeScroll(false);
    }
  }, [loading, taskId]);

  const handleScroll = useCallback(() => {
    if (!transcriptRef.current) return;
    const { scrollTop, scrollHeight, clientHeight } = transcriptRef.current;
    const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
    const atBottom = distanceFromBottom < 80;
    autoScrollRef.current = atBottom;
    setShowResumeScroll(!atBottom);
  }, []);

  const resumeScroll = useCallback(() => {
    if (!transcriptRef.current) return;
    transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight;
    autoScrollRef.current = true;
    setShowResumeScroll(false);
  }, []);

  // ---- Composer + stop. ----
  const submitComment = useCallback(
    async (e: React.FormEvent) => {
      e.preventDefault();
      const trimmed = comment.trim();
      if (!trimmed || sending) return;
      setSending(true);
      setSendError(null);
      setEntries((prev) => {
        const next: TaskOutputEvent[] = [
          ...prev,
          { taskId, messageType: "user_input", content: trimmed, isPartial: false },
        ];
        entriesCache.set(taskId, next);
        return next;
      });
      try {
        await sendTaskMessage(taskId, trimmed);
        setComment("");
      } catch (err) {
        setSendError(err instanceof Error ? err.message : "Failed to send comment");
        window.setTimeout(() => setSendError(null), 5000);
      } finally {
        setSending(false);
      }
    },
    [comment, sending, taskId],
  );

  const handleStop = useCallback(async () => {
    if (stopping || !isStreaming) return;
    if (!window.confirm("Stop this task? It will be marked failed.")) return;
    setStopping(true);
    try {
      await stopTask(taskId);
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error("Failed to stop task", err);
    } finally {
      setStopping(false);
    }
  }, [taskId, stopping, isStreaming]);

  const focusComposer = useCallback(() => {
    const input = composerRef.current?.querySelector("textarea");
    input?.focus();
  }, []);

  // ---- Parse diff once, derive file list. ----
  const parsedFiles = useMemo(() => parseDiff(diffText), [diffText]);

  // Default selection: first file in the parsed list.
  useEffect(() => {
    if (!selectedFilePath && parsedFiles.length > 0) {
      setSelectedFilePath(parsedFiles[0].path);
    }
  }, [parsedFiles, selectedFilePath]);

  const visibleFiles: DiffFile[] = useMemo(() => {
    if (!selectedFilePath) return parsedFiles;
    const match = parsedFiles.find((f) => f.path === selectedFilePath);
    return match ? [match] : parsedFiles;
  }, [parsedFiles, selectedFilePath]);

  const toggleFile = (path: string) => {
    setCollapsedFiles((prev) => {
      const next = new Set(prev);
      if (next.has(path)) next.delete(path);
      else next.add(path);
      return next;
    });
  };

  return (
    <div className="flex-1 flex flex-col h-full overflow-hidden bg-[#0a1628]">
      {/* Header strip — title + live indicator + actions. */}
      <div className="shrink-0 flex items-center gap-3 px-6 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)] bg-[#091428]">
        <h1 className="text-[14px] font-medium text-white tracking-tight">
          {label}
        </h1>
        {isStreaming && (
          <span className="flex items-center gap-1.5 px-2 py-0.5 bg-green-400/10 border border-green-400/30 text-green-400 font-mono text-[10px] uppercase">
            <span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
            Live
          </span>
        )}
        {status && (
          <span className="font-mono text-[10px] uppercase tracking-wide text-[#7788aa]">
            {status}
          </span>
        )}
        <div className="ml-auto flex items-center gap-2">
          <button
            type="button"
            onClick={focusComposer}
            className="px-2 py-1 font-mono text-[10px] uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400"
          >
            Send (⌘↵)
          </button>
          <button
            type="button"
            onClick={handleStop}
            disabled={!isStreaming || stopping}
            className="px-2 py-1 font-mono text-[10px] uppercase tracking-wide text-amber-300 border border-amber-600/60 hover:border-amber-400 disabled:opacity-40 disabled:cursor-not-allowed"
          >
            {stopping ? "Stopping…" : "Stop"}
          </button>
          {ephemeral && isTerminalStatus(status) && (
            <button
              type="button"
              onClick={() => {
                const ok = window.confirm(
                  "Merge this ephemeral task into the base branch? You'll be taken to the standalone task page where the merge runs.",
                );
                if (!ok) return;
                window.open(`/exec/${taskId}#merge`, "_blank");
              }}
              title="Manual merge — opens the merge UI on the standalone task page"
              className="px-2 py-1 font-mono text-[10px] uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400"
            >
              Merge to base ↗
            </button>
          )}
        </div>
      </div>

      {/* Two-column body. */}
      <div className="flex-1 flex min-h-0 overflow-hidden">
        {/* Left: changed files + diff. */}
        <div className="w-[40%] min-w-[320px] max-w-[640px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] flex flex-col bg-[#0a1628] overflow-hidden">
          <div className="shrink-0 flex items-center gap-2 px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.15)]">
            <span className="text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
              Changed files
            </span>
            <span className="text-[10px] font-mono text-[#556677]">
              {parsedFiles.length}
            </span>
            <button
              type="button"
              onClick={() => void refreshDiff()}
              className="ml-auto text-[10px] font-mono text-[#9bc3ff] hover:text-white"
              title="Refresh diff"
            >
              ↻
            </button>
          </div>
          {/* File list (top half of left pane). */}
          <div className="overflow-y-auto max-h-[40%] border-b border-dashed border-[rgba(117,170,252,0.15)]">
            {parsedFiles.length === 0 ? (
              <div className="px-3 py-3 font-mono text-[10px] italic text-[#556677]">
                {diffLoading
                  ? "Loading diff…"
                  : diffError
                  ? diffError
                  : "No changes yet"}
              </div>
            ) : (
              parsedFiles.map((f) => (
                <button
                  key={f.path}
                  type="button"
                  onClick={() => setSelectedFilePath(f.path)}
                  className={`w-full text-left flex items-center gap-2 px-3 py-1 font-mono text-[11px] ${
                    selectedFilePath === f.path
                      ? "bg-[rgba(117,170,252,0.12)] text-white"
                      : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]"
                  }`}
                >
                  <span className={fileStatusClass(f.status)}>
                    {fileStatusInitial(f.status)}
                  </span>
                  <span className="truncate flex-1">{f.path}</span>
                  <span className="font-mono text-[9px]">
                    {f.additions > 0 && (
                      <span className="text-green-400 mr-1">+{f.additions}</span>
                    )}
                    {f.deletions > 0 && (
                      <span className="text-red-400">-{f.deletions}</span>
                    )}
                  </span>
                </button>
              ))
            )}
          </div>
          {/* Diff content (bottom of left pane). */}
          <div className="flex-1 overflow-y-auto p-2">
            {visibleFiles.length === 0 ? (
              <div className="px-2 py-3 font-mono text-[10px] italic text-[#556677]">
                Select a file to view its diff.
              </div>
            ) : (
              visibleFiles.map((file) => (
                <DiffFileView
                  key={file.path}
                  file={file}
                  collapsed={collapsedFiles.has(file.path)}
                  onToggle={() => toggleFile(file.path)}
                />
              ))
            )}
          </div>
        </div>

        {/* Right: transcript + sticky composer. */}
        <div className="flex-1 flex flex-col min-h-0 overflow-hidden relative">
          <div
            ref={transcriptRef}
            onScroll={handleScroll}
            className="flex-1 overflow-y-auto"
          >
            <div className="max-w-3xl mx-auto px-6 py-6 pb-32 text-[#dbe7ff]">
              {loading && entries.length === 0 ? (
                <p className="text-[#556677] font-mono text-xs italic">
                  Loading transcript…
                </p>
              ) : entries.length === 0 ? (
                <p className="text-[#556677] font-mono text-xs italic">
                  {isStreaming ? "Waiting for output…" : "No output yet."}
                </p>
              ) : (
                <div className="space-y-4">
                  {entries.map((entry, idx) => (
                    <FeedEntry key={idx} entry={entry} />
                  ))}
                  {isStreaming && (
                    <span className="inline-block w-2 h-4 bg-[#9bc3ff] animate-pulse align-baseline" />
                  )}
                </div>
              )}
            </div>
          </div>

          {showResumeScroll && (
            <button
              type="button"
              onClick={resumeScroll}
              className="absolute bottom-32 right-6 z-10 px-3 py-1.5 font-mono text-[10px] uppercase tracking-wide text-[#9bc3ff] bg-[#091428] border border-[rgba(117,170,252,0.4)] hover:border-[#75aafc] shadow-lg"
            >
              ↓ Jump to latest
            </button>
          )}

          <div
            ref={composerRef}
            className="absolute bottom-0 left-0 right-0 border-t border-dashed border-[rgba(117,170,252,0.25)] bg-[#091428]/95 backdrop-blur"
          >
            {sendError && (
              <div className="px-6 py-1 bg-red-900/20 text-red-400 text-xs font-mono">
                {sendError}
              </div>
            )}
            <form
              onSubmit={submitComment}
              className="max-w-3xl mx-auto px-6 py-3 flex items-start gap-3"
            >
              <span className="text-[10px] font-mono text-[#556677] uppercase tracking-wide pt-2 shrink-0">
                Comment
              </span>
              <textarea
                value={comment}
                onChange={(e) => setComment(e.target.value)}
                onKeyDown={(e) => {
                  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
                    void submitComment(e as unknown as React.FormEvent);
                  }
                }}
                placeholder={
                  isStreaming
                    ? "Add a comment to interrupt and redirect…"
                    : "Task is not streaming — comments will queue if accepted."
                }
                rows={2}
                disabled={sending}
                className="flex-1 bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none"
              />
              <button
                type="submit"
                disabled={sending || !comment.trim()}
                className="px-3 py-1.5 font-mono text-[10px] uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
              >
                {sending ? "Sending…" : "Send"}
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Feed entry rendering — document-style, not log-style. Mirrors the old
// DocumentTaskStream's DocumentEntry exactly (the user liked that part).
// ---------------------------------------------------------------------------

function FeedEntry({ entry }: { entry: TaskOutputEvent }) {
  switch (entry.messageType) {
    case "user_input":
      return (
        <blockquote className="border-l-2 border-cyan-400/60 pl-4 py-1 italic text-cyan-200">
          <span className="not-italic text-[10px] font-mono text-cyan-400 uppercase tracking-wide block mb-1">
            You
          </span>
          {entry.content}
        </blockquote>
      );

    case "assistant":
      return (
        <div className="leading-relaxed text-[14px]">
          <SimpleMarkdown content={entry.content} className="text-[#e0eaf8]" />
        </div>
      );

    case "system":
      return (
        <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide">
          {entry.content}
        </p>
      );

    case "tool_use":
      return (
        <p className="text-[11px] font-mono text-[#7788aa] flex items-center gap-2">
          <span className="text-yellow-500">·</span>
          <span className="text-[#75aafc]">{entry.toolName || "tool"}</span>
          {firstLineOfInput(entry.toolInput) && (
            <span className="text-[#445566] truncate">
              {firstLineOfInput(entry.toolInput)}
            </span>
          )}
        </p>
      );

    case "tool_result":
      if (!entry.content) return null;
      return (
        <p className="text-[11px] font-mono pl-4">
          <span className={entry.isError ? "text-red-400" : "text-emerald-400"}>
            {entry.isError ? "✗" : "→"}
          </span>{" "}
          <span className="text-[#7788aa]">
            {entry.content.split("\n")[0]}
            {entry.content.includes("\n") && "…"}
          </span>
        </p>
      );

    case "result":
      return (
        <div className="border-t border-[rgba(117,170,252,0.15)] pt-3 mt-6">
          <p className="text-[10px] font-mono text-emerald-400 uppercase tracking-wide mb-2">
            Result
          </p>
          <div className="leading-relaxed text-[13px]">
            <SimpleMarkdown content={entry.content} className="text-[#e0eaf8]" />
          </div>
          {(entry.costUsd !== undefined || entry.durationMs !== undefined) && (
            <p className="text-[10px] font-mono text-[#556677] mt-2">
              {entry.durationMs !== undefined &&
                `Duration: ${(entry.durationMs / 1000).toFixed(1)}s`}
              {entry.costUsd !== undefined && entry.durationMs !== undefined && " · "}
              {entry.costUsd !== undefined &&
                `Cost: $${entry.costUsd.toFixed(4)}`}
            </p>
          )}
        </div>
      );

    case "error":
      return (
        <p className="border-l-2 border-red-400/60 pl-4 py-1 text-red-300 text-[13px]">
          {entry.content}
        </p>
      );

    default:
      if (!entry.content) return null;
      return (
        <p className="text-[11px] font-mono text-[#556677]">
          {entry.content}
        </p>
      );
  }
}

function isTerminalStatus(status?: string): boolean {
  if (!status) return false;
  return ["done", "completed", "merged"].includes(status);
}

function firstLineOfInput(input?: Record<string, unknown>): string {
  if (!input) return "";
  for (const key of ["command", "file_path", "path", "url", "pattern", "query"]) {
    const v = input[key];
    if (typeof v === "string" && v.length > 0) {
      return v.split("\n")[0].slice(0, 96);
    }
  }
  return "";
}

function fileStatusClass(status: DiffFile["status"]): string {
  switch (status) {
    case "added":
      return "text-green-400 bg-green-400/10 px-1 py-0.5 text-[9px] font-bold";
    case "deleted":
      return "text-red-400 bg-red-400/10 px-1 py-0.5 text-[9px] font-bold";
    case "renamed":
      return "text-purple-400 bg-purple-400/10 px-1 py-0.5 text-[9px] font-bold";
    case "modified":
    default:
      return "text-yellow-400 bg-yellow-400/10 px-1 py-0.5 text-[9px] font-bold";
  }
}

function fileStatusInitial(status: DiffFile["status"]): string {
  switch (status) {
    case "added":
      return "A";
    case "deleted":
      return "D";
    case "renamed":
      return "R";
    case "modified":
    default:
      return "M";
  }
}
