import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import i18n from '../../../i18n';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useConsoleSession } from '../../../hooks/useConsoleSession';
import type { ConsoleSessionSummary } from '../../../lib/ssh-console-types';
import type { SshConnection } from '../../../lib/ssh-types';
import { TerminalView, type TerminalViewHandle } from './console/TerminalView';
import { ConsoleHeader } from './console/ConsoleHeader';
import { ConsoleTabStrip } from './console/ConsoleTabStrip';
import { MobileKeyboardBar } from './console/MobileKeyboardBar';
import { ScrollToBottomButton } from './console/ScrollToBottomButton';
import { useViewportNarrow } from '../../layout/TopBar';

async function fetchConnections(): Promise<{ list: SshConnection[]; sshDisabled: boolean }> {
  const res = await fetch('/api/ssh/connections', { credentials: 'include' });
  if (res.status === 404) return { list: [], sshDisabled: true };
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = (await res.json()) as { connections: SshConnection[] };
  return { list: data.connections ?? [], sshDisabled: false };
}

/** Map a structured `{ error }` code from the session endpoint to a user-facing message. */
function describeSessionError(code: string): { msg: string; hardStop: boolean } {
  switch (code) {
    case 'host_key_not_verified':
      return {
        msg: i18n.t('detail:console.errors.hostKeyNotVerified'),
        hardStop: false,
      };
    case 'no_grant':
      return {
        msg: i18n.t('detail:console.errors.noGrant'),
        hardStop: false,
      };
    case 'host_key_mismatch':
      return {
        msg: i18n.t('detail:console.errors.hostKeyMismatch'),
        hardStop: true,
      };
    case 'connection_disabled':
      return { msg: i18n.t('detail:console.errors.disabled'), hardStop: false };
    case 'abuse_locked':
      return { msg: i18n.t('detail:console.errors.abuseLocked'), hardStop: false };
    case 'connection_not_found':
      return { msg: i18n.t('detail:console.errors.notFound'), hardStop: false };
    case 'task_session_cap':
      return { msg: i18n.t('detail:console.errors.taskSessionCap'), hardStop: false };
    case 'user_session_cap':
      return { msg: i18n.t('detail:console.errors.userSessionCap'), hardStop: false };
    default:
      return { msg: i18n.t('detail:console.errors.startFailed', { code }), hardStop: false };
  }
}

export function ConsoleTab({ taskId }: { taskId: number }) {
  const qc = useQueryClient();
  const sessionsQueryKey = ['console-sessions', taskId] as const;
  const { data: sessionsData, isFetching: sessionsFetching } = useQuery<{ sessions: ConsoleSessionSummary[] }>({
    queryKey: sessionsQueryKey,
    queryFn: async () => {
      const r = await fetch(`/api/local/tasks/${taskId}/console/sessions`);
      return r.ok ? r.json() : { sessions: [] };
    },
    refetchInterval: 5000,
  });
  const sessions = sessionsData?.sessions ?? [];

  const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null);
  // Whether the add-connection panel is showing on top of an existing
  // terminal. When there are zero sessions the empty-state picker always
  // shows regardless of this flag.
  const [showAddPanel, setShowAddPanel] = useState(false);

  // Keep the selection valid: default to the most-recent session when none
  // is selected yet, and fall back to another session (or empty state) if
  // the selected one disappears (closed elsewhere, evicted, etc).
  //
  // Gated on `!sessionsFetching`: right after the add-connection flow calls
  // `onStarted(newConnectionId)` (which sets selectedConnectionId AND
  // invalidates this query), a refetch is in flight but `sessions` is still
  // the STALE pre-add list — it doesn't contain the new id yet. Without the
  // gate, this effect would see "selected id doesn't exist" on that stale
  // list and immediately revert the selection back to the old tab, so the
  // just-added tab never shows until the NEXT unrelated refetch (5s poll)
  // happens to include it. Skipping reconciliation while a fetch is
  // in-flight and re-running once it resolves (sessions + isFetching both
  // change) lets the just-set selection survive until fresh data confirms
  // whether it's actually still missing.
  useEffect(() => {
    if (sessionsFetching) return;
    if (sessions.length === 0) {
      if (selectedConnectionId !== null) setSelectedConnectionId(null);
      return;
    }
    const stillExists = sessions.some((s) => s.connection_id === selectedConnectionId);
    if (!stillExists) {
      const mostRecent = [...sessions].sort(
        (a, b) => new Date(b.last_activity_at).getTime() - new Date(a.last_activity_at).getTime(),
      )[0];
      setSelectedConnectionId(mostRecent.connection_id);
    }
  }, [sessions, selectedConnectionId, sessionsFetching]);

  const selectedSession = sessions.find((s) => s.connection_id === selectedConnectionId) ?? null;

  // Only the selected tab gets a live WS — switching tabs tears down the old
  // socket and connects to the new one (see useConsoleSession).
  const session = useConsoleSession(taskId, selectedConnectionId);
  const terminalRef = useRef<TerminalViewHandle>(null);
  // 768px = Tailwind md breakpoint. Below this we consider the user to be on
  // a phone/tablet without a physical keyboard, so the on-screen keyboard bar
  // and scroll-to-bottom FAB become useful.
  const compactMode = useViewportNarrow(768);

  const showEmptyState = sessions.length === 0;
  const showPicker = showEmptyState || showAddPanel;

  async function handleClose(connectionId: string) {
    try {
      await fetch(`/api/local/tasks/${taskId}/console/session/close`, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ connection_id: connectionId }),
      });
    } finally {
      // The selection-fixing effect above picks another tab (or the empty
      // state) once the refreshed list no longer contains this connection.
      qc.invalidateQueries({ queryKey: sessionsQueryKey });
    }
  }

  return (
    <div className="flex flex-col flex-1 min-h-0">
      {sessions.length > 0 && (
        <ConsoleTabStrip
          sessions={sessions}
          selectedConnectionId={selectedConnectionId}
          onSelect={setSelectedConnectionId}
          onClose={handleClose}
          onAddConnection={() => setShowAddPanel(true)}
        />
      )}
      <ConsoleHeader state={session.state} session={selectedSession} />
      <div className="flex-1 min-h-0 relative">
        {showPicker ? (
          <ConnectionPicker
            taskId={taskId}
            onCancel={showEmptyState ? undefined : () => setShowAddPanel(false)}
            onStarted={(connectionId) => {
              // The session now exists server-side; refresh the list and
              // immediately select the newly-added connection's tab instead
              // of waiting for the "most-recent" selection-fixing effect (or
              // the 5s sessions poll) to pick it up. Changing
              // selectedConnectionId re-points useConsoleSession, which opens
              // the WS for the new connection on its own — no explicit
              // reconnect needed here.
              setShowAddPanel(false);
              setSelectedConnectionId(connectionId);
              qc.invalidateQueries({ queryKey: sessionsQueryKey });
            }}
          />
        ) : (
          <>
            <TerminalView ref={terminalRef} session={session} />
            {compactMode && <ScrollToBottomButton terminalRef={terminalRef} />}
          </>
        )}
      </div>
      {compactMode && !showPicker && <MobileKeyboardBar session={session} />}
    </div>
  );
}

export function ConnectionPicker({
  taskId,
  onStarted,
  onCancel,
}: {
  taskId: number;
  /** Called once the server confirms the new session exists, with the
   * connection_id that was just added — the caller uses this to select the
   * new tab immediately instead of waiting for a list refresh. */
  onStarted: (connectionId: string) => void;
  /** Present only when this picker is shown as an add-connection panel on
   * top of an existing terminal (not the full-screen empty state) — lets
   * the user back out without starting a session. */
  onCancel?: () => void;
}) {
  const { t } = useTranslation('detail');
  const { data, isLoading, error } = useQuery({
    queryKey: ['ssh', 'connections'],
    queryFn: fetchConnections,
    staleTime: 15_000,
  });

  const connections = useMemo(
    () => (data?.list ?? []).filter((c) => c.enabled && !c.disabledByAdmin),
    [data],
  );

  const [selectedId, setSelectedId] = useState<string>('');
  const [submitting, setSubmitting] = useState(false);
  const [errMsg, setErrMsg] = useState<{ msg: string; hardStop: boolean } | null>(null);

  // Default the select to the first connection once loaded.
  const effectiveId = selectedId || connections[0]?.id || '';

  async function start() {
    if (!effectiveId) return;
    setSubmitting(true);
    setErrMsg(null);
    try {
      const res = await fetch(`/api/local/tasks/${taskId}/console/session`, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ connection_id: effectiveId }),
      });
      if (res.ok) {
        onStarted(effectiveId);
        return;
      }
      let code = `HTTP ${res.status}`;
      try {
        const j = (await res.json()) as { error?: string };
        if (j?.error) code = j.error;
      } catch {
        // non-JSON body; keep HTTP status as the code
      }
      setErrMsg(describeSessionError(code));
    } catch (e) {
      setErrMsg({ msg: e instanceof Error ? e.message : String(e), hardStop: false });
    } finally {
      setSubmitting(false);
    }
  }

  if (data?.sshDisabled) {
    return (
      <div className="absolute inset-0 flex items-center justify-center p-6 bg-[#0b1020]">
        <div className="max-w-md text-xs text-slate-300 bg-surface/10 border border-hairline rounded-md p-4 leading-relaxed">
          {t('console.sshDisabled')}
        </div>
      </div>
    );
  }

  return (
    <div className="absolute inset-0 flex items-center justify-center p-6 bg-[#0b1020]">
      <div className="w-full max-w-md space-y-3">
        <div className="flex items-start justify-between gap-2">
          <div>
            <h3 className="text-sm font-semibold text-slate-100">{t('console.startTitle')}</h3>
            <p className="text-2xs text-slate-400 mt-0.5">
              {t('console.startDesc')}
            </p>
          </div>
          {onCancel && (
            <button
              type="button"
              onClick={onCancel}
              aria-label={t('console.cancelAddConnection')}
              className="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded text-slate-400 hover:text-slate-100 hover:bg-surface"
            >
              ✕
            </button>
          )}
        </div>

        {isLoading && <div className="text-xs text-slate-400">{t('console.loadingConnections')}</div>}
        {error && <div className="text-xs text-red-400">{t('console.loadFailed')}: {String(error)}</div>}

        {!isLoading && connections.length === 0 ? (
          <div className="text-xs text-slate-300 bg-surface/10 border border-hairline rounded-md p-3 leading-relaxed">
            {t('console.noConnections')}
          </div>
        ) : (
          <>
            <select
              value={effectiveId}
              onChange={(e) => { setSelectedId(e.target.value); setErrMsg(null); }}
              disabled={submitting}
              className="w-full text-xs px-2 py-1.5 bg-surface border border-hairline rounded text-slate-100 disabled:opacity-50"
            >
              {connections.map((c) => (
                <option key={c.id} value={c.id}>
                  {c.label} — {c.username}@{c.host}:{c.port}
                </option>
              ))}
            </select>

            <button
              type="button"
              onClick={() => start()}
              disabled={submitting || !effectiveId}
              className="w-full px-3 h-8 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50"
            >
              {submitting ? t('console.starting') : t('console.startSession')}
            </button>
          </>
        )}

        {errMsg && (
          <div
            className={
              errMsg.hardStop
                ? 'text-xs text-red-200 bg-red-500/20 border border-red-500/50 rounded-md p-3 leading-relaxed'
                : 'text-xs text-amber-200 bg-amber-500/10 border border-amber-500/30 rounded-md p-3 leading-relaxed'
            }
          >
            {errMsg.msg}
          </div>
        )}
      </div>
    </div>
  );
}
