import { useEffect, useMemo, useRef, useState } from 'react';
import { Check, ChevronDown, RefreshCw, Loader2, Shield, Brain, Bot } from 'lucide-react';
import { api } from '@/api/client';
import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types';
import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot';
import { providerIcon } from '@/components/coder/providerIcons';
import { useAgentSessions } from '@/hooks/useAgentSessions';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { BottomSheet } from '@/components/BottomSheet';
import { useViewport } from '@/hooks/useViewport';
import { cn } from '@/lib/utils';

const PREFS_KEY = 'boocode.coder.agent-prefs';


type ProviderPrefs = Record<string, {
  model: string;
  modeId: string | null;
  thinkingOptionId: string | null;
}>;

function loadPrefs(): ProviderPrefs {
  try {
    const raw = localStorage.getItem(PREFS_KEY);
    return raw ? (JSON.parse(raw) as ProviderPrefs) : {};
  } catch {
    return {};
  }
}

function savePrefs(prefs: ProviderPrefs): void {
  localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
}

function defaultsForProvider(entry: ProviderSnapshotEntry): AgentSessionConfig {
  const model =
    entry.models.find((m) => m.isDefault)?.id ??
    entry.models[0]?.id ??
    '';
  const selectedModel = entry.models.find((m) => m.id === model);
  const modeId = entry.defaultModeId ?? entry.modes[0]?.id ?? null;
  const thinkingOptionId =
    selectedModel?.defaultThinkingOptionId ??
    selectedModel?.thinkingOptions?.find((t) => t.isDefault)?.id ??
    selectedModel?.thinkingOptions?.[0]?.id ??
    null;

  return {
    provider: entry.name,
    model,
    modeId,
    thinkingOptionId,
  };
}

function resolveConfig(
  entry: ProviderSnapshotEntry,
  prefs: ProviderPrefs,
): AgentSessionConfig {
  const saved = prefs[entry.name];
  const base = defaultsForProvider(entry);

  const model =
    saved?.model && entry.models.some((m) => m.id === saved.model)
      ? saved.model
      : base.model;

  const selectedModel = entry.models.find((m) => m.id === model);
  const modeId =
    saved?.modeId && entry.modes.some((m) => m.id === saved.modeId)
      ? saved.modeId
      : base.modeId;

  const thinkingOptions = selectedModel?.thinkingOptions ?? [];
  const thinkingOptionId =
    saved?.thinkingOptionId &&
    thinkingOptions.some((t) => t.id === saved.thinkingOptionId)
      ? saved.thinkingOptionId
      : base.thinkingOptionId;

  return { provider: entry.name, model, modeId, thinkingOptionId };
}

interface PickerProps {
  label: string;
  value: string;
  disabled?: boolean;
  options: Array<{ id: string; label: string }>;
  onPick: (id: string) => void;
  icon?: React.ReactNode;
  /** Mobile: render icon + chevron only (no value label) to save row width. */
  iconOnly?: boolean;
}

function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly }: PickerProps) {
  const { isMobile } = useViewport();
  const [open, setOpen] = useState(false);
  const currentLabel = options.find((o) => o.id === value)?.label ?? (value || label);

  const list = (
    <div className="py-1">
      {options.map((o) => (
        <button
          key={o.id}
          type="button"
          onClick={() => {
            onPick(o.id);
            setOpen(false);
          }}
          className="w-full text-left flex items-center gap-2 font-mono text-xs px-2 py-1.5 hover:bg-accent rounded"
        >
          <Check className={cn('size-3 shrink-0', o.id === value ? 'opacity-100' : 'opacity-0')} />
          <span className="truncate">{o.label}</span>
        </button>
      ))}
    </div>
  );

  if (isMobile) {
    return (
      <>
        <button
          type="button"
          disabled={disabled}
          onClick={() => setOpen(true)}
          aria-label={`${label}: ${currentLabel}`}
          className="inline-flex items-center gap-1 min-h-[44px] px-1.5 rounded text-xs text-muted-foreground hover:text-foreground disabled:opacity-40"
        >
          {icon}
          {!iconOnly && <span className="truncate max-w-[120px]">{currentLabel}</span>}
          <ChevronDown className="size-3 opacity-70 shrink-0" />
        </button>
        <BottomSheet open={open} onClose={() => setOpen(false)} title={label}>
          <div className="px-2">{list}</div>
        </BottomSheet>
      </>
    );
  }

  return (
    <DropdownMenu open={open} onOpenChange={setOpen}>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          disabled={disabled}
          className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-muted/60 disabled:opacity-40"
        >
          {icon}
          <span className="truncate max-w-[180px]">{currentLabel}</span>
          <ChevronDown className="size-3 opacity-70 shrink-0" />
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="max-h-64 overflow-y-auto min-w-[160px]">
        {options.map((o) => (
          <DropdownMenuItem key={o.id} onSelect={() => onPick(o.id)} className="text-xs">
            <Check className={cn('size-3 shrink-0', o.id === value ? 'opacity-100' : 'opacity-0')} />
            {o.label}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

interface Props {
  projectPath?: string;
  value: AgentSessionConfig;
  onChange: (next: AgentSessionConfig) => void;
  onProviderCommandsChange?: (commands: AgentCommand[]) => void;
  connected?: boolean;
  // v2.6 Phase 1-UX §9b: chat id for the resumed/new-session chip. Optional so
  // BooChat and any other AgentComposerBar caller renders no chip and is
  // otherwise unaffected. When present + connected + the chat has ≥1 prior
  // turn, a chip right of the Provider picker reports whether switching to the
  // current provider resumes an agent session, replays history (boocode), or
  // starts fresh.
  sessionId?: string;
  // True once the chat has at least one prior turn — gates the chip so it stays
  // hidden on a brand-new chat. Defaults to false (no chip).
  hasPriorTurn?: boolean;
}

// Relative-time formatter for the resumed-chip title (e.g. "3m ago").
function relativeTime(iso: string | null): string {
  if (!iso) return 'unknown';
  const then = new Date(iso).getTime();
  if (Number.isNaN(then)) return 'unknown';
  const diffMs = Date.now() - then;
  if (diffMs < 0) return 'just now';
  const sec = Math.floor(diffMs / 1000);
  if (sec < 60) return 'just now';
  const min = Math.floor(sec / 60);
  if (min < 60) return `${min}m ago`;
  const hr = Math.floor(min / 60);
  if (hr < 24) return `${hr}h ago`;
  const day = Math.floor(hr / 24);
  return `${day}d ago`;
}

export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected, sessionId, hasPriorTurn }: Props) {
  const allEntries = useProviderSnapshot(projectPath);
  // 5.5 — the composer picker only offers ENABLED providers that are ready (or
  // still loading). Disabled (enabled:false) and unavailable/error providers are
  // hidden here and managed in Settings → Providers. Native boocode is always
  // enabled+ready, so it always appears.
  const entries = useMemo(
    () => allEntries?.filter((e) => e.enabled && (e.status === 'ready' || e.status === 'loading')) ?? null,
    [allEntries],
  );
  const [refreshing, setRefreshing] = useState(false);

  // v2.6 Phase 1-UX §9b: chat-scoped agent-session rows for the resumed/new
  // chip. Hook is unconditional (hooks rule); it self-no-ops when sessionId is
  // undefined or the chat has no prior turn, so BooChat callers cost nothing.
  const { sessions: agentSessions } = useAgentSessions(
    sessionId && hasPriorTurn ? sessionId : undefined,
  );

  const hydratedRef = useRef(false);

  useEffect(() => {
    hydratedRef.current = false;
  }, [projectPath]);

  useEffect(() => {
    if (!entries?.length || hydratedRef.current) return;
    hydratedRef.current = true;
    const prefs = loadPrefs();
    const entry =
      entries.find((e) => e.name === value.provider) ??
      entries.find((e) => e.name === 'boocode') ??
      entries[0];
    if (!entry) return;
    onChange(resolveConfig(entry, prefs));
  }, [entries, onChange, value.provider]);

  // If the active provider is disabled in the settings drawer it drops out of
  // `entries` (the 5.5 filter) — fall back to boocode so the composer never
  // strands on an unselectable provider with empty model/mode pickers.
  useEffect(() => {
    if (!entries?.length) return;
    if (entries.some((e) => e.name === value.provider)) return;
    const fallback = entries.find((e) => e.name === 'boocode') ?? entries[0];
    if (!fallback) return;
    onChange(resolveConfig(fallback, loadPrefs()));
  }, [entries, value.provider, onChange]);

  // 5.6 — loading poll: while any entry is loading (Phase 2's sync cache-miss
  // return), refetch until terminal. Capped; no provider_snapshot_updated WS
  // frame (deferred Tier-2). Dormant today since the snapshot awaits the build.
  const pollsRef = useRef(0);
  useEffect(() => {
    const anyLoading = allEntries?.some((e) => e.status === 'loading') ?? false;
    if (!anyLoading) {
      pollsRef.current = 0;
      return;
    }
    if (pollsRef.current >= 10) return;
    const t = setTimeout(() => {
      pollsRef.current += 1;
      void refreshProviderSnapshot(projectPath);
    }, 2000);
    return () => clearTimeout(t);
  }, [allEntries, projectPath]);

  const currentEntry = useMemo(
    () => entries?.find((e) => e.name === value.provider),
    [entries, value.provider],
  );

  const currentModel = useMemo(
    () => currentEntry?.models.find((m) => m.id === value.model),
    [currentEntry, value.model],
  );

  const thinkingOptions = currentModel?.thinkingOptions ?? [];

  useEffect(() => {
    onProviderCommandsChange?.(currentEntry?.commands ?? []);
  }, [currentEntry, onProviderCommandsChange]);

  function persist(next: AgentSessionConfig): void {
    const prefs = loadPrefs();
    prefs[next.provider] = {
      model: next.model,
      modeId: next.modeId,
      thinkingOptionId: next.thinkingOptionId,
    };
    savePrefs(prefs);
    onChange(next);
  }

  function pickProvider(name: string): void {
    const entry = entries?.find((e) => e.name === name);
    if (!entry) return;
    persist(resolveConfig(entry, loadPrefs()));
  }

  function pickModel(model: string): void {
    const entry = currentEntry;
    if (!entry) return;
    const selected = entry.models.find((m) => m.id === model);
    const thinkingOptionId =
      selected?.defaultThinkingOptionId ??
      selected?.thinkingOptions?.find((t) => t.isDefault)?.id ??
      selected?.thinkingOptions?.[0]?.id ??
      null;
    persist({ ...value, model, thinkingOptionId });
  }

  async function handleRefresh(): Promise<void> {
    setRefreshing(true);
    try {
      await api.coder.refreshProviders();
      await refreshProviderSnapshot(projectPath);
    } finally {
      setRefreshing(false);
    }
  }

  if (!entries) {
    return (
      <div className="text-xs text-muted-foreground px-2 py-1">Loading agents…</div>
    );
  }

  const providerOptions = entries.map((e) => ({ id: e.name, label: e.label }));
  const modeOptions = (currentEntry?.modes ?? []).map((m) => ({ id: m.id, label: m.label }));
  const modelOptions = (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: m.label }));
  const thinkingOpts = thinkingOptions.map((t) => ({ id: t.id, label: t.label }));

  // v2.6 Phase 1-UX §9b: resumed / history / new-session chip. Only meaningful
  // when this is a real chat (sessionId), the WS is connected, and the chat has
  // ≥1 prior turn — otherwise render nothing so fresh chats and non-coder
  // callers stay clean.
  const sessionRow = agentSessions.find((s) => s.agent === value.provider);
  const sessionChip: { label: string; title: string } | null =
    sessionId && hasPriorTurn && connected
      ? value.provider === 'boocode'
        ? // Native boocode never holds an agent_sessions row — it reconstructs
          // the conversation from the chat transcript each turn.
          { label: 'history', title: 'BooCode replays the chat transcript each turn' }
        : sessionRow?.has_session
          ? {
              label: 'resumed',
              title: `Resuming ${value.provider} · last active ${relativeTime(sessionRow.last_active_at)}`,
            }
          : { label: 'new session', title: `${value.provider} starts a fresh session this turn` }
      : null;

  return (
    <div className="flex flex-wrap items-center gap-1 px-2 py-1 border-b border-border bg-muted/20 shrink-0">
      <CompactPicker
        label="Provider"
        value={value.provider}
        options={providerOptions}
        onPick={pickProvider}
        icon={
          currentEntry?.status === 'loading'
            ? <Loader2 size={13} className="shrink-0 animate-spin" />
            : providerIcon(value.provider)
        }
      />
      {sessionChip && (
        <span
          title={sessionChip.title}
          className="inline-flex items-center rounded-full border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shrink-0"
        >
          {sessionChip.label}
        </span>
      )}
      <CompactPicker
        label="Mode"
        value={value.modeId ?? ''}
        disabled={modeOptions.length === 0}
        options={modeOptions}
        onPick={(modeId) => persist({ ...value, modeId })}
        icon={<Shield className="size-3 shrink-0" />}
        iconOnly
      />
      <CompactPicker
        label="Model"
        value={value.model}
        disabled={modelOptions.length === 0}
        options={modelOptions}
        onPick={pickModel}
        icon={<Bot size={13} className="shrink-0" />}
      />
      {thinkingOpts.length > 0 && (
        <CompactPicker
          label="Thinking"
          value={value.thinkingOptionId ?? ''}
          options={thinkingOpts}
          onPick={(thinkingOptionId) => persist({ ...value, thinkingOptionId })}
          icon={<Brain className="size-3 shrink-0" />}
        />
      )}
      {/* Status dot + refresh as one right-aligned unit so the refresh button
          stays on the top line instead of wrapping past the edge-pinned dot. */}
      <div className="ml-auto flex items-center gap-1 shrink-0">
        {connected !== undefined && (
          <span
            className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', connected ? 'bg-green-500' : 'bg-red-500')}
            title={connected ? 'Connected' : 'Disconnected'}
          />
        )}
        <button
          type="button"
          onClick={() => void handleRefresh()}
          disabled={refreshing}
          className="inline-flex items-center justify-center size-7 max-md:min-h-[44px] max-md:min-w-[44px] rounded text-muted-foreground hover:text-foreground disabled:opacity-40"
          aria-label="Refresh provider list"
          title="Refresh providers"
        >
          <RefreshCw className={cn('size-3.5', refreshing && 'animate-spin')} />
        </button>
      </div>
    </div>
  );
}
