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 type { AgentStatusEntry } from '@/hooks/useAgentStatus';
import { providerIcon } from '@/components/coder/providerIcons';
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;
  // #10: normalized status (working|blocked|idle|error) for the active external
  // agent in this chat, or null for native boocode / before any frame. Renders
  // a status dot DISTINCT from the WS-liveness `connected` dot. Undefined for
  // non-coder callers — no dot.
  agentStatus?: AgentStatusEntry | null;
}

// #10: normalized external-agent status dot. Mirrors StatusDot's visual
// language but on the four normalized buckets (working|blocked|idle|error),
// and is DISTINCT from the WS-liveness `connected` dot beside it:
//   working — emerald spinning ring (subtle motion, like chat streaming)
//   blocked — amber dot (matches the permission/blocked state colour)
//   idle    — gray dot
//   error   — red dot
function AgentStatusDot({ entry, agent }: { entry: AgentStatusEntry; agent: string }) {
  const title =
    `${agent}: ${entry.status}` + (entry.reason ? ` — ${entry.reason}` : '');

  if (entry.status === 'working') {
    return (
      <span
        aria-label={`Agent status: working${entry.reason ? ` — ${entry.reason}` : ''}`}
        title={title}
        className="inline-block w-3 h-3 rounded-full border-2 border-emerald-500 border-t-transparent animate-spin shrink-0"
      />
    );
  }

  const bg =
    entry.status === 'blocked' ? 'bg-amber-500'
      : entry.status === 'error' ? 'bg-destructive'
      : 'bg-muted-foreground/40';

  return (
    <span
      aria-label={`Agent status: ${entry.status}${entry.reason ? ` — ${entry.reason}` : ''}`}
      title={title}
      className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', bg)}
    />
  );
}

export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected, agentStatus }: 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);

  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 }));

  return (
    <div className="flex 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)
        }
      />
      <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 — pinned right (ml-auto), never on its own line. */}
      <div className="ml-auto flex items-center gap-1 shrink-0">
        {/* #10: normalized agent status — only for an external agent with a
            live status frame. Distinct from the WS-liveness dot that follows. */}
        {agentStatus && value.provider !== 'boocode' && (
          <AgentStatusDot entry={agentStatus} agent={value.provider} />
        )}
        {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>
  );
}
