/**
 * @file LaneConsolePane.tsx
 * @description One lane's run console: the RunSetup ↔ TerminalView switcher,
 * moved out of Workspace.tsx so the Workspace page can render 1, 2, or 4 of
 * these side by side (split terminal view). Owns its own prompt/cwd/model/
 * permissionMode/effort/resumeSession/handle/busy/runHistory state — nothing
 * is shared between panes. `lanes`, `binaryStatus`, `cwdSuggestions`,
 * `activeRuns`, and `externalSessions` are supplied as props because they are global, not
 * lane-specific, and fetching them per pane would mean N redundant identical
 * requests for an N-pane layout.
 *
 * That state is bound to the lane the pane currently shows: switching `laneId`
 * swaps the whole pane over to the new lane — its cwd, its history, and its
 * live tmux session — instead of leaving the previous lane's terminal on
 * screen under a new lane's header.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Play, AlertCircle } from "lucide-react";
import { api } from "../../lib/api";
import type {
  CwdSuggestion,
  DashboardRunHistoryItem,
  EffortLevel,
  PermissionMode,
  RunHandle,
  RunListResponse,
  RunStartArgs,
} from "../../lib/api";
import type { Session, Lane } from "../../lib/types";
import { TerminalView } from "./TerminalView";
import { RunSetup } from "./RunSetup";
import { ActiveRunsSwitcher } from "./RunHistory";

export interface LaneConsolePaneProps {
  lanes: Lane[];
  laneId: number | null;
  showLaneSelector: boolean;
  onLaneIdChange: (id: number) => void;
  onLaneCreated: (lane: Lane) => void;
  binaryStatus: { found: boolean; path: string | null } | null;
  cwdSuggestions: CwdSuggestion[];
  activeRuns: RunListResponse | null;
  /** Active Claude Code sessions started outside the dashboard, listed in the
   *  active-runs switcher alongside dashboard runs. */
  externalSessions?: Session[];
  wsConnected: boolean;
  defaultCwd?: string;
  onHasActiveRunChange?: (active: boolean) => void;
}

export function LaneConsolePane({
  lanes,
  laneId,
  showLaneSelector,
  onLaneIdChange,
  onLaneCreated,
  binaryStatus,
  cwdSuggestions,
  activeRuns,
  externalSessions,
  wsConnected,
  defaultCwd,
  onHasActiveRunChange,
}: LaneConsolePaneProps) {
  const { t } = useTranslation("run");
  const { t: tLanes } = useTranslation("lanes");
  const { t: tCommon } = useTranslation("common");

  const [prompt, setPrompt] = useState("");
  const [model, setModel] = useState("");
  const [permissionMode, setPermissionMode] = useState<PermissionMode>("acceptEdits");
  const [effort, setEffort] = useState<EffortLevel>("");
  const [cwd, setCwd] = useState(() => lanes.find((l) => l.id === laneId)?.cwd ?? defaultCwd ?? "");
  const [resumeSession, setResumeSession] = useState<Session | null>(null);
  const [handle, setHandle] = useState<RunHandle | null>(null);
  const [busy, setBusy] = useState<"start" | "kill" | "attach" | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [runHistory, setRunHistory] = useState<DashboardRunHistoryItem[]>([]);

  const currentLane = laneId !== null ? lanes.find((l) => l.id === laneId) : null;

  useEffect(() => {
    onHasActiveRunChange?.(handle !== null);
  }, [handle, onHasActiveRunChange]);

  useEffect(() => {
    if (!currentLane && defaultCwd && cwd === "") {
      setCwd(defaultCwd);
    }
  }, [defaultCwd, currentLane, cwd]);

  const refreshList = useCallback(() => {
    if (laneId !== null) {
      api.run
        .history(50, { laneId })
        .then((r) => setRunHistory(r.items))
        .catch(() => undefined);
    } else {
      api.run
        .history(50)
        .then((r) => setRunHistory(r.items))
        .catch(() => undefined);
    }
  }, [laneId]);

  // `lanes` and `activeRuns` are re-fetched every few seconds by the page, so
  // reading them through a ref keeps the lane-switch effect below off their
  // identity — a background poll must not wipe a half-typed prompt.
  const latest = useRef({ lanes, activeRuns, defaultCwd });
  latest.current = { lanes, activeRuns, defaultCwd };

  // A pane's prompt, cwd, history and terminal all belong to the lane it
  // shows, so switching lanes has to swap every one of them. Re-attach right
  // away when the new lane already has a live run: each lane sticks to its own
  // tmux session, and the switch should land on that session rather than on an
  // empty setup form the user then has to Start out of.
  const autoAttachedForLane = useRef<number | null>(null);
  useEffect(() => {
    const { activeRuns: runs } = latest.current;
    setPrompt("");
    setResumeSession(null);
    setError(null);
    setBusy(null);
    setHandle(runs?.items.find((r) => r.laneId === laneId && r.status === "running") ?? null);
    autoAttachedForLane.current = null;
    refreshList();
  }, [laneId, refreshList]);

  // The switch effect above only sees whatever `activeRuns` the page already
  // had loaded at that instant. Remounting this page (navigating away and
  // back) starts `activeRuns` at null again, so a lane with a live run would
  // otherwise show the setup form until the user switched lanes and back —
  // the only path that re-ran the effect after the poll caught up. Re-check
  // once `activeRuns` actually arrives, but only once per lane so it never
  // fights a user-initiated "New Run".
  useEffect(() => {
    if (handle || laneId === null) return;
    if (autoAttachedForLane.current === laneId) return;
    const running = activeRuns?.items.find((r) => r.laneId === laneId && r.status === "running");
    if (running) {
      autoAttachedForLane.current = laneId;
      setHandle(running);
    }
  }, [activeRuns, laneId, handle]);

  // The cwd tracks the lane's own folder separately, keyed on the resolved
  // path rather than on `laneId` alone: the pane can mount before the lane
  // list has loaded (split view restores its pane lanes from localStorage),
  // and `laneId` never changes afterwards, so a laneId-only effect would leave
  // the console pointing at the default directory. RunSetup submits this
  // string verbatim, so a stale one starts the run in the wrong folder.
  const laneCwd = currentLane?.cwd ?? null;
  useEffect(() => {
    if (laneCwd) setCwd(laneCwd);
    else if (laneId === null) setCwd(latest.current.defaultCwd ?? "");
  }, [laneId, laneCwd]);

  const attachToRun = useCallback(
    async (id: string) => {
      if (busy) return;
      setBusy("attach");
      setError(null);
      try {
        const fetched = await api.run.get(id);
        setHandle(fetched);
      } catch (err: unknown) {
        const m = err instanceof Error ? err.message : "unknown";
        setError(t("errors.attachFailed", { message: m }));
      } finally {
        setBusy(null);
      }
    },
    [busy, t]
  );

  const onStartFromSetup = useCallback(
    async (args: RunStartArgs) => {
      if (busy) return;
      setBusy("start");
      setError(null);
      try {
        const effectiveCwd = args.cwd || undefined;

        if (!effectiveCwd) {
          throw new Error(t("errors.cwdRequired"));
        }

        // Resolve the lane from the cwd the user actually typed, not from
        // args.laneId — RunSetup always supplies this pane's laneId (a
        // required prop), which would otherwise silently start a run in the
        // wrong lane whenever the user types a cwd different from the one
        // this pane currently shows.
        const ownedLane = lanes.find((l) => l.cwd === effectiveCwd);
        let targetLaneId: number;
        if (ownedLane) {
          targetLaneId = ownedLane.id;
          if (ownedLane.id !== laneId) onLaneIdChange(ownedLane.id);
        } else {
          try {
            const ensureResult = await api.lanes.ensure({ cwd: effectiveCwd });
            targetLaneId = ensureResult.lane.id;
            onLaneIdChange(ensureResult.lane.id);
            onLaneCreated(ensureResult.lane);
          } catch (err) {
            throw new Error(
              t("errors.laneCreateFailed", {
                message: err instanceof Error ? err.message : "unknown",
              })
            );
          }
        }

        let laneStartResult;
        try {
          laneStartResult = await api.lanes.action(targetLaneId, "start", {
            prompt: args.initialPrompt || "",
            model: args.model || undefined,
            permissionMode: args.permissionMode,
            resumeSessionId: args.resumeSessionId,
            effort: args.effort || undefined,
          });
        } catch (laneErr: unknown) {
          const msg = laneErr instanceof Error ? laneErr.message : String(laneErr);
          if (msg.includes("409") || msg.includes("ERUNLIVE")) {
            const fresh = await api.lanes.list().catch(() => null);
            const updatedLane = fresh?.lanes.find((l) => l.id === targetLaneId);
            if (updatedLane?.run_id) {
              await attachToRun(updatedLane.run_id);
              return;
            }
          }
          throw laneErr;
        }

        if (!laneStartResult.lane?.run_id) {
          throw new Error(t("errors.noRunIdReturned"));
        }

        try {
          const fetched = await api.run.get(laneStartResult.lane.run_id);
          setHandle(fetched);
          refreshList();
        } catch {
          try {
            await attachToRun(laneStartResult.lane.run_id);
            refreshList();
          } catch (fallbackErr: unknown) {
            const attachMsg = fallbackErr instanceof Error ? fallbackErr.message : "unknown";
            throw new Error(t("errors.runStartedButNotAttached", { message: attachMsg }));
          }
        }
      } catch (err: unknown) {
        const m = err instanceof Error ? err.message : "unknown";
        setError(t("errors.startFailed", { message: m }));
      } finally {
        setBusy(null);
      }
    },
    [busy, t, lanes, laneId, onLaneIdChange, onLaneCreated, attachToRun, refreshList]
  );

  const onResumeFromHistory = useCallback(
    async (item: DashboardRunHistoryItem) => {
      if (!item.session_id) return;
      if (busy) return;
      setBusy("start");
      setError(null);
      try {
        let fetched: RunHandle;

        if (item.cwd) {
          const effectiveCwd = item.cwd;
          let targetLaneId = lanes.find((l) => l.cwd === effectiveCwd)?.id;

          if (!targetLaneId) {
            const ensureResult = await api.lanes.ensure({ cwd: effectiveCwd });
            targetLaneId = ensureResult.lane.id;
            onLaneCreated(ensureResult.lane);
          }

          const laneStartResult = await api.lanes.action(targetLaneId, "start", {
            prompt: "",
            model: item.model || undefined,
            permissionMode: item.permission_mode || undefined,
            effort: item.effort || undefined,
            resumeSessionId: item.session_id,
          });

          if (!laneStartResult.lane?.run_id) {
            throw new Error("No run_id returned from lane start");
          }

          fetched = await api.run.get(laneStartResult.lane.run_id);
          onLaneIdChange(targetLaneId);
        } else {
          fetched = await api.run.start({
            laneId: 0,
            initialPrompt: "",
            cwd: undefined,
            model: item.model || undefined,
            permissionMode: item.permission_mode || undefined,
            effort: item.effort || undefined,
            resumeSessionId: item.session_id,
          });
        }

        setHandle(fetched);
        setResumeSession(null);
        refreshList();
      } catch (err) {
        const msg = err instanceof Error ? err.message : "unknown";
        setError(t("errors.startFailed", { message: msg }));
      } finally {
        setBusy(null);
      }
    },
    [busy, refreshList, t, lanes, onLaneCreated, onLaneIdChange]
  );

  const onViewFromHistory = useCallback(
    (item: DashboardRunHistoryItem) => {
      if (item.session_id) void onResumeFromHistory(item);
    },
    [onResumeFromHistory]
  );

  const newRun = useCallback(() => {
    setHandle(null);
    setPrompt("");
    setResumeSession(null);
    setError(null);
  }, []);

  if (laneId === null && showLaneSelector) {
    return (
      <div data-testid="pane-empty" className="flex min-h-0 flex-1 flex-col gap-2 p-4">
        <select
          data-testid="pane-lane-select"
          aria-label={tLanes("splitView.paneLaneLabel")}
          className="rounded border border-border bg-surface-1 px-2 py-1 text-xs text-fg-secondary"
          value=""
          onChange={(e) => e.target.value && onLaneIdChange(Number(e.target.value))}
        >
          <option value="">{tLanes("splitView.pickLane")}</option>
          {lanes.map((l) => (
            <option key={l.id} value={l.id}>
              {l.title || l.cwd}
            </option>
          ))}
        </select>
        <p className="text-xs text-fg-muted">{tLanes("splitView.emptyPane")}</p>
      </div>
    );
  }

  return (
    <div data-testid="console-body" className="flex min-h-0 flex-1 flex-col gap-5">
      {showLaneSelector && (
        <select
          data-testid="pane-lane-select"
          aria-label={tLanes("splitView.paneLaneLabel")}
          className="rounded border border-border bg-surface-1 px-2 py-1 text-xs text-fg-secondary"
          value={laneId ?? ""}
          onChange={(e) => e.target.value && onLaneIdChange(Number(e.target.value))}
        >
          <option value="">{tLanes("splitView.pickLane")}</option>
          {lanes.map((l) => (
            <option key={l.id} value={l.id}>
              {l.title || l.cwd}
            </option>
          ))}
        </select>
      )}

      <header className="flex items-start gap-3">
        <div className="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center flex-shrink-0">
          <Play className="w-4.5 h-4.5 text-accent" />
        </div>
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2">
            <h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
            {wsConnected ? (
              <span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
                <span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
                {tCommon("live")}
              </span>
            ) : (
              <span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
                <span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
                {tCommon("offline")}
              </span>
            )}
          </div>
          <p className="text-xs text-fg-muted max-w-3xl">{t("subtitle")}</p>
        </div>
        <ActiveRunsSwitcher
          activeRuns={activeRuns}
          currentHandleId={handle?.id || null}
          onAttach={attachToRun}
          runHistory={runHistory}
          externalSessions={externalSessions}
          onResumeFromHistory={onResumeFromHistory}
          onViewFromHistory={onViewFromHistory}
          onRefresh={refreshList}
        />
      </header>

      {binaryStatus && !binaryStatus.found && (
        <div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
          <AlertCircle className="w-4 h-4 flex-shrink-0" />
          <span>{t("binary.missing")}</span>
        </div>
      )}

      {error && (
        <div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
          <AlertCircle className="w-4 h-4 flex-shrink-0" />
          <span className="flex-1 break-all">{error}</span>
        </div>
      )}

      {!handle ? (
        <RunSetup
          laneId={laneId ?? 0}
          prompt={prompt}
          onPromptChange={setPrompt}
          cwd={cwd}
          onCwdChange={setCwd}
          cwdSuggestions={cwdSuggestions}
          model={model}
          onModelChange={setModel}
          permissionMode={permissionMode}
          onPermissionModeChange={setPermissionMode}
          effort={effort}
          onEffortChange={setEffort}
          binaryFound={binaryStatus?.found ?? true}
          busy={busy === "start"}
          onStart={onStartFromSetup}
          activeRuns={activeRuns}
          laneCwd={currentLane?.cwd}
          resumeSession={resumeSession}
          onResumeSessionChange={setResumeSession}
          runHistory={runHistory}
          onResumeFromHistory={onResumeFromHistory}
        />
      ) : (
        <div className="flex-1 min-h-0 flex flex-col">
          <TerminalView
            runId={handle!.id}
            wsBaseUrl={window.location.origin.replace(/^http/, "ws")}
          />
          <button
            onClick={newRun}
            className="mt-3 px-3 py-1.5 text-sm rounded border border-border hover:border-border-light text-fg-secondary hover:text-fg-primary transition-colors"
          >
            {t("actions.newRun")}
          </button>
        </div>
      )}
    </div>
  );
}
