/**
 * @file RunHistory.tsx
 * @description Past and live dashboard runs. Moved verbatim out of
 * `pages/Run.tsx` so the Run page and the Workspace page can both mount the
 * same run list.
 *
 * Two exported pieces, rendered exactly where the page rendered them before:
 *   - `ActiveRunsSwitcher` — the header button (live count, or total when
 *     nothing is running) that opens the modal;
 *   - `RunsModal` — the merged list itself: live in-memory handles from
 *     `activeRuns` deduped against persistent `runHistory`, newest first, with
 *     status / mode chip filters, a free-text search, and the per-row Attach /
 *     Resume / View actions.
 *
 * `externalSessions` (active Claude Code sessions this dashboard did NOT spawn —
 * e.g. `claude` started by hand in a terminal tab) are merged in as live rows so
 * "Active runs" counts everything actually running. They carry no tmux session
 * the dashboard can attach to, so their only action is Resume, which spawns a
 * fresh tmux-backed `claude --resume <session>` in that cwd.
 *
 * Props only: no API call of its own. The page passes `activeRuns`,
 * `runHistory` and `externalSessions` in and gets attach / resume / view /
 * refresh back out through callbacks; the 2 s refresh ticker the modal runs
 * just calls `onRefresh`.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
  Play,
  RefreshCw,
  Info,
  ExternalLink,
  X,
  ListOrdered,
  Search,
  RotateCcw,
  Eye,
} from "lucide-react";
import type { DashboardRunHistoryItem, RunListResponse, RunStatus } from "../../lib/api";
import type { Session } from "../../lib/types";

// Minimal StatusPill component (from deleted RunConsole)
function StatusPill({
  status,
}: {
  status: RunStatus | "completed" | "error" | "killed" | "abandoned";
}) {
  const colors: Record<string, string> = {
    running: "bg-status-success/10 text-status-success border-status-success/30",
    gone: "bg-surface-3 text-fg-secondary border-border",
    completed: "bg-sky-500/10 text-sky-300 border-sky-500/30",
    error: "bg-status-danger/10 text-status-danger border-status-danger/30",
    killed: "bg-surface-3 text-fg-secondary border-border",
    abandoned: "bg-surface-3 text-fg-secondary border-border",
  };
  const color = colors[status] || colors.abandoned;
  return (
    <span className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${color}`}>{status}</span>
  );
}

type RunStatusFilter =
  | "all"
  | "running"
  | "spawning"
  | "completed"
  | "error"
  | "killed"
  | "abandoned";

export interface UnifiedRunRow {
  id: string;
  sessionId: string | null;
  cwd: string;
  model: string | null;
  status: RunStatus | "completed" | "error" | "killed" | "abandoned";
  promptPreview: string;
  startedAt: number;
  endedAt: number | null;
  isLive: boolean;
  /** Live Claude Code session this dashboard did not spawn — no tmux session to
   *  attach to, so Resume (a fresh `claude --resume` in its cwd) is the only
   *  action. */
  external?: boolean;
}

export function ActiveRunsSwitcher({
  activeRuns,
  currentHandleId,
  onAttach,
  runHistory,
  externalSessions = [],
  onResumeFromHistory,
  onViewFromHistory,
  onRefresh,
}: {
  activeRuns: RunListResponse | null;
  currentHandleId: string | null;
  onAttach: (id: string) => void;
  runHistory: DashboardRunHistoryItem[];
  /** Sessions with `status: "active"` from GET /api/sessions. Remote-source and
   *  cwd-less sessions are ignored — neither can be resumed on this machine. */
  externalSessions?: Session[];
  onResumeFromHistory: (item: DashboardRunHistoryItem) => void;
  onViewFromHistory: (item: DashboardRunHistoryItem) => void;
  onRefresh: () => void;
}) {
  const { t } = useTranslation("run");
  const [open, setOpen] = useState(false);

  // Lock body scroll while the modal is open and let Esc close it.
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") setOpen(false);
    };
    document.addEventListener("keydown", onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = prev;
    };
  }, [open]);

  // Merge live in-memory handles + persistent history + externally started
  // sessions into one row list. Live entries dedupe past-history entries with
  // the same id; a session id already covered by a run row is never repeated as
  // an external row.
  const { rows, historyItems } = useMemo(() => {
    const out: UnifiedRunRow[] = [];
    const seen = new Set<string>();
    const seenSessions = new Set<string>();
    if (activeRuns) {
      for (const r of activeRuns.items) {
        seen.add(r.id);
        if (r.sessionId) seenSessions.add(r.sessionId);
        out.push({
          id: r.id,
          sessionId: r.sessionId,
          cwd: r.cwd || "",
          model: r.model,
          status: r.status,
          promptPreview: r.promptPreview || "",
          startedAt: r.startedAt ? new Date(r.startedAt).getTime() : 0,
          endedAt: null,
          isLive: r.status === "running",
        });
      }
    }
    for (const h of runHistory) {
      if (seen.has(h.id)) continue;
      seen.add(h.id);
      if (h.session_id) seenSessions.add(h.session_id);
      const startedTs = new Date(h.started_at).getTime() || 0;
      const endedTs = h.ended_at ? new Date(h.ended_at).getTime() : null;
      out.push({
        id: h.id,
        sessionId: h.session_id,
        cwd: h.cwd,
        model: h.model,
        status: h.status,
        promptPreview: h.prompt_preview || "",
        startedAt: startedTs,
        endedAt: endedTs,
        isLive: h.isLive,
      });
    }
    // Externally started sessions: shown as live rows, and mirrored as
    // synthetic history items so the existing resume path (which only reads
    // session_id / cwd / model) works on them unchanged.
    const synthetic: DashboardRunHistoryItem[] = [];
    for (const s of externalSessions) {
      if (!s.cwd) continue;
      if (s.source && s.source !== "local") continue;
      if (seenSessions.has(s.id)) continue;
      seenSessions.add(s.id);
      synthetic.push({
        id: `session:${s.id}`,
        session_id: s.id,
        cwd: s.cwd,
        model: s.model,
        permission_mode: null,
        effort: null,
        resume_session_id: null,
        prompt_preview: s.name,
        status: "running",
        exit_code: null,
        started_at: s.started_at,
        ended_at: null,
        isLive: true,
      });
      out.push({
        id: `session:${s.id}`,
        sessionId: s.id,
        cwd: s.cwd,
        model: s.model,
        status: "running",
        promptPreview: s.name || "",
        startedAt: new Date(s.started_at).getTime() || 0,
        endedAt: null,
        isLive: true,
        external: true,
      });
    }
    out.sort((a, b) => b.startedAt - a.startedAt);
    return {
      rows: out,
      historyItems: synthetic.length ? [...runHistory, ...synthetic] : runHistory,
    };
  }, [activeRuns, runHistory, externalSessions]);

  const liveCount = rows.filter((r) => r.isLive).length;
  const totalCount = rows.length;

  return (
    <>
      <button
        onClick={() => setOpen(true)}
        disabled={totalCount === 0}
        className={`inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
          liveCount > 0
            ? "border-status-success/40 bg-status-success/10 text-status-success hover:bg-status-success/15"
            : "border-border bg-surface-2 text-fg-secondary hover:bg-surface-3"
        }`}
      >
        <ListOrdered className="w-3.5 h-3.5" />
        {liveCount > 0 ? (
          <>
            <span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse" />
            {t("runs.viewActive_other", { count: liveCount })}
          </>
        ) : (
          <>
            {t("runs.switcher")}
            {totalCount > 0 && <span className="text-fg-muted font-mono">{totalCount}</span>}
          </>
        )}
      </button>
      {open && (
        <RunsModal
          rows={rows}
          currentHandleId={currentHandleId}
          onAttach={(id) => {
            setOpen(false);
            onAttach(id);
          }}
          onResume={(item) => {
            setOpen(false);
            onResumeFromHistory(item);
          }}
          onView={(item) => {
            setOpen(false);
            onViewFromHistory(item);
          }}
          runHistory={historyItems}
          onClose={() => setOpen(false)}
          onRefresh={onRefresh}
        />
      )}
    </>
  );
}

export function RunsModal({
  rows,
  currentHandleId,
  onAttach,
  onResume,
  onView,
  runHistory,
  onClose,
  onRefresh,
}: {
  rows: UnifiedRunRow[];
  currentHandleId: string | null;
  onAttach: (id: string) => void;
  onResume: (item: DashboardRunHistoryItem) => void;
  onView: (item: DashboardRunHistoryItem) => void;
  runHistory: DashboardRunHistoryItem[];
  onClose: () => void;
  onRefresh: () => void;
}) {
  const { t } = useTranslation("run");
  const [statusFilter, setStatusFilter] = useState<RunStatusFilter>("all");
  const [search, setSearch] = useState("");

  // Snappy refresh while the modal is the foreground UI: pull immediately
  // on open + every 2 s after that. Combined with the page-level 5 s poll
  // and the WS run_status broadcasts, this guarantees that any state
  // change - lifecycle event, sibling tab, manual DB tweak, boot
  // reconciliation - surfaces here within a couple of seconds.
  useEffect(() => {
    onRefresh();
    const tick = setInterval(onRefresh, 2000);
    return () => clearInterval(tick);
  }, [onRefresh]);

  const counts = useMemo(() => {
    const byStatus: Record<string, number> = { all: rows.length };
    for (const r of rows) {
      byStatus[r.status] = (byStatus[r.status] || 0) + 1;
    }
    return { byStatus };
  }, [rows]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    return rows.filter((r) => {
      if (statusFilter !== "all" && r.status !== statusFilter) return false;
      if (!q) return true;
      const hay =
        r.promptPreview + "\n" + r.cwd + "\n" + (r.sessionId || "") + "\n" + (r.model || "");
      return hay.toLowerCase().includes(q);
    });
  }, [rows, statusFilter, search]);

  const historyById = useMemo(() => {
    const m = new Map<string, DashboardRunHistoryItem>();
    for (const h of runHistory) m.set(h.id, h);
    return m;
  }, [runHistory]);

  const STATUSES: RunStatusFilter[] = [
    "all",
    "running",
    "completed",
    "error",
    "killed",
    "abandoned",
  ];

  return (
    <div
      className="fixed inset-0 z-50 flex items-start justify-center px-4 py-10 overflow-y-auto bg-black/60 backdrop-blur-sm animate-fade-in"
      onMouseDown={(e) => {
        if (e.target === e.currentTarget) onClose();
      }}
    >
      <div className="w-full max-w-4xl rounded-xl border border-border bg-surface-1 shadow-2xl shadow-black/60 flex flex-col max-h-[85vh]">
        {/* Header */}
        <div className="flex items-center gap-3 px-5 py-3 border-b border-border flex-shrink-0">
          <div className="w-8 h-8 rounded-lg bg-accent/15 inline-flex items-center justify-center">
            <ListOrdered className="w-4 h-4 text-accent" />
          </div>
          <div className="min-w-0 flex-1">
            <h2 className="text-sm font-semibold text-fg-primary">
              {t("runs.modalTitle", "Dashboard runs")}
            </h2>
            <p className="text-[11px] text-fg-muted">
              {t(
                "runs.modalSubtitle",
                "Every run started from this dashboard, regardless of status"
              )}
            </p>
          </div>
          <button
            onClick={onRefresh}
            className="w-7 h-7 rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 inline-flex items-center justify-center"
            aria-label={t("runs.refresh", "Refresh")}
            title={t("runs.refresh", "Refresh")}
          >
            <RefreshCw className="w-3.5 h-3.5" />
          </button>
          <Link
            to="/sessions"
            onClick={onClose}
            className="text-[11px] text-accent hover:text-accent/80 inline-flex items-center gap-1 mr-1"
          >
            {t("runs.allSessionsLink")}
          </Link>
          <button
            onClick={onClose}
            className="w-7 h-7 rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 inline-flex items-center justify-center"
            aria-label={t("limitations.dismiss")}
          >
            <X className="w-4 h-4" />
          </button>
        </div>

        {/* Filter bar */}
        <div className="px-5 py-3 border-b border-border flex flex-col gap-2.5 flex-shrink-0">
          <div className="flex items-center gap-2 bg-surface-2 border border-border rounded-md px-2.5 py-1.5">
            <Search className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
            <input
              autoFocus
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder={t("runs.searchPlaceholder", "Search prompt, cwd, model, or session id…")}
              className="flex-1 bg-transparent text-[12px] text-fg-primary placeholder:text-fg-muted focus:outline-none"
            />
            {search && (
              <button
                onClick={() => setSearch("")}
                className="text-fg-muted hover:text-fg-secondary text-[10px]"
                aria-label="Clear"
              >
                <X className="w-3 h-3" />
              </button>
            )}
          </div>
          <div className="flex flex-wrap gap-3 items-center">
            <FilterChipGroup
              label={t("runs.filterStatus", "Status")}
              value={statusFilter}
              options={STATUSES.map((s) => ({
                value: s,
                label: s === "all" ? t("runs.allLabel", "All") : t(`status.${s}`),
                count: counts.byStatus[s] || 0,
              }))}
              onChange={(v) => setStatusFilter(v as RunStatusFilter)}
            />
          </div>
        </div>

        {/* List */}
        <div className="flex-1 min-h-0 overflow-auto divide-y divide-border">
          {filtered.length === 0 ? (
            <div className="px-5 py-12 text-center text-[12px] text-fg-muted">
              {rows.length === 0
                ? t(
                    "runs.modalEmpty",
                    "No dashboard runs yet. Start one below to populate this list."
                  )
                : t("runs.modalEmptyFiltered", "No runs match these filters.")}
            </div>
          ) : (
            filtered.map((r) => {
              const isCurrent = r.id === currentHandleId;
              return (
                <UnifiedRunRowView
                  key={r.id}
                  row={r}
                  isCurrent={isCurrent}
                  onAttach={() => onAttach(r.id)}
                  onResume={() => {
                    const h = historyById.get(r.id);
                    if (h) onResume(h);
                  }}
                  onView={() => {
                    const h = historyById.get(r.id);
                    if (h) onView(h);
                  }}
                />
              );
            })
          )}
        </div>

        {/* Footer */}
        <div className="px-5 py-2.5 border-t border-border bg-surface-2/40 flex items-center gap-2 flex-shrink-0">
          <Info className="w-3 h-3 text-fg-muted flex-shrink-0" />
          <span className="text-[10.5px] text-fg-muted leading-relaxed flex-1">
            {t("runs.scopeNote")}
          </span>
          <span className="text-[10.5px] text-fg-muted font-mono">
            {filtered.length} / {rows.length}
          </span>
        </div>
      </div>
    </div>
  );
}

function FilterChipGroup<T extends string>({
  label,
  value,
  options,
  onChange,
}: {
  label: string;
  value: T;
  options: { value: T; label: string; count: number }[];
  onChange: (v: T) => void;
}) {
  return (
    <div className="flex items-center gap-1.5 flex-wrap">
      <span className="text-[10px] uppercase tracking-wider font-semibold text-fg-muted mr-1">
        {label}
      </span>
      {options.map((opt) => {
        const active = value === opt.value;
        const dim = opt.count === 0 && opt.value !== "all";
        return (
          <button
            key={opt.value}
            onClick={() => onChange(opt.value)}
            disabled={dim}
            className={`text-[10.5px] font-medium px-2 py-0.5 rounded-full border transition-colors disabled:opacity-40 ${
              active
                ? "bg-accent/15 border-accent/50 text-accent"
                : "bg-surface-2 border-border text-fg-secondary hover:bg-surface-3 hover:border-border-strong"
            }`}
          >
            {opt.label}
            <span className="ml-1 text-fg-muted font-mono">{opt.count}</span>
          </button>
        );
      })}
    </div>
  );
}

function UnifiedRunRowView({
  row,
  isCurrent,
  onAttach,
  onResume,
  onView,
}: {
  row: UnifiedRunRow;
  isCurrent: boolean;
  onAttach: () => void;
  onResume: () => void;
  onView: () => void;
}) {
  const { t } = useTranslation("run");
  const startedDate = new Date(row.startedAt);
  const startedLabel = isNaN(startedDate.getTime())
    ? "-"
    : startedDate.toLocaleString(undefined, {
        month: "short",
        day: "numeric",
        hour: "2-digit",
        minute: "2-digit",
      });
  // Without mode distinction, offer resume for any finished run with a session.
  // An external session is live but has no attachable tmux session, so Resume
  // (a new tmux-backed `claude --resume` in its cwd) is what it gets instead.
  const canResume = !!row.sessionId && (!row.isLive || !!row.external);
  const canView = !!row.sessionId && !row.isLive;
  return (
    <div
      className={`px-5 py-3 transition-colors ${
        isCurrent ? "bg-accent/[0.06]" : "hover:bg-surface-2/50"
      }`}
    >
      <div className="flex items-center gap-2 mb-1.5 flex-wrap">
        <StatusPill status={row.status} />
        {row.isLive && (
          <span className="text-[10px] font-semibold text-status-success bg-status-success/10 border border-status-success/25 px-1.5 py-0.5 rounded-full inline-flex items-center gap-1">
            <span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse" />
            {t("runs.liveBadge", "live")}
          </span>
        )}
        {row.external && (
          <span
            className="text-[10px] font-semibold text-amber-300 bg-amber-500/10 border border-amber-500/25 px-1.5 py-0.5 rounded-full"
            title={t("runs.externalHint")}
          >
            {t("runs.externalBadge")}
          </span>
        )}
        {isCurrent && (
          <span className="text-[10px] font-semibold text-accent bg-accent/10 border border-accent/25 px-1.5 py-0.5 rounded-full">
            {t("runs.currentBadge", "current")}
          </span>
        )}
        <span className="ml-auto inline-flex items-center gap-1.5">
          {row.isLive && !row.external && !isCurrent && (
            <button
              onClick={onAttach}
              className="inline-flex items-center gap-1 rounded-md border border-status-success/40 bg-status-success/10 hover:bg-status-success/20 text-status-success px-2 py-0.5 text-[10.5px] font-medium transition-colors"
            >
              <Play className="w-3 h-3" />
              {t("runs.attachLabel", "Attach")}
            </button>
          )}
          {canResume && (
            <button
              onClick={onResume}
              title={row.external ? t("runs.externalHint") : undefined}
              className="inline-flex items-center gap-1 rounded-md border border-accent/40 bg-accent/15 hover:bg-accent/25 text-accent px-2 py-0.5 text-[10.5px] font-medium transition-colors"
            >
              <RotateCcw className="w-3 h-3" />
              {t("resume.resumeOption", "Resume")}
            </button>
          )}
          {canView && (
            <button
              onClick={onView}
              className="inline-flex items-center gap-1 rounded-md border border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary px-2 py-0.5 text-[10.5px] font-medium transition-colors"
            >
              <Eye className="w-3 h-3" />
              {t("runs.viewLabel", "View")}
            </button>
          )}
        </span>
      </div>
      {row.promptPreview && (
        <div className="text-[12px] text-fg-secondary line-clamp-2 leading-snug">
          {row.promptPreview}
        </div>
      )}
      <div className="font-mono text-[10px] text-fg-muted truncate mt-1">{row.cwd}</div>
      <div className="text-[10px] text-fg-muted mt-0.5 flex items-center gap-2 flex-wrap">
        <span>{startedLabel}</span>
        {row.model && <span className="font-mono text-fg-muted">· {row.model}</span>}
        {row.sessionId && (
          <Link
            to={`/sessions/${encodeURIComponent(row.sessionId)}`}
            className="inline-flex items-center gap-1 text-fg-muted hover:text-fg-secondary transition-colors"
            title={t("actions.viewSession")}
          >
            <ExternalLink className="w-2.5 h-2.5" />
            <span className="font-mono">{row.sessionId.slice(0, 8)}</span>
          </Link>
        )}
      </div>
    </div>
  );
}
