import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Check, ChevronDown, RefreshCw, Loader2, Shield, ShieldAlert, Eye, Brain, Bot, Star } from 'lucide-react';
import { toast } from 'sonner';
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,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { BottomSheet } from '@/components/BottomSheet';
import { useViewport } from '@/hooks/useViewport';
import { formatModelLabel } from '@/lib/model-label';
import {
  availablePermissionModes,
  permissionForModeId,
  nativeModeForPermission,
  type PermissionMode,
} from '@/lib/permission-mode';
import { cn } from '@/lib/utils';

// Permission picker icon — varies with the active mode so the (icon-only) control
// is glanceable: Eye = Plan (read-only), Shield = Ask, ShieldAlert = Bypass.
function permissionIcon(mode: PermissionMode): React.ReactNode {
  if (mode === 'plan') return <Eye className="size-3 shrink-0" />;
  if (mode === 'bypass') return <ShieldAlert className="size-3 shrink-0 text-amber-500" />;
  return <Shield className="size-3 shrink-0" />;
}

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;
  /** Grow to fill the row's free space and render the value brighter — used for
   *  the Model picker so the active model is the most visible control. */
  flexible?: boolean;
  /** Grouped rendering: renders sections with labels (Favorites-first, then
   *  per-provider). When provided, `options` is ignored. */
  groups?: ModelGroup[];
  /** When set, each row shows a star toggle; `favorites` marks the filled ones.
   *  Used by the Model picker to add/remove models from the Favorites section. */
  favorites?: Set<string>;
  onToggleFavorite?: (id: string) => void;
}

interface ModelGroup {
  label: string;
  options: Array<{ id: string; label: string }>;
}

// Star toggle rendered inside a picker row. Stops pointer/click propagation so
// hitting the star favorites the model without selecting it / closing the menu.
function FavoriteStar({ id, isFav, onToggle }: { id: string; isFav: boolean; onToggle: (id: string) => void }) {
  return (
    <button
      type="button"
      tabIndex={-1}
      aria-label={isFav ? 'Remove from favorites' : 'Add to favorites'}
      title={isFav ? 'Remove from favorites' : 'Add to favorites'}
      onPointerDown={(e) => e.stopPropagation()}
      onClick={(e) => {
        e.preventDefault();
        e.stopPropagation();
        onToggle(id);
      }}
      className="shrink-0 -mr-0.5 p-0.5 rounded hover:bg-foreground/10"
    >
      <Star className={cn('size-3', isFav ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/40')} />
    </button>
  );
}

// One selectable row in the mobile (BottomSheet) list. Shared by the flat and
// grouped renderers so the two stay identical.
function PickerRow({ o, selected, isFav, onSelect, onToggleFavorite }: {
  o: { id: string; label: string };
  selected: boolean;
  isFav: boolean;
  onSelect: (id: string) => void;
  onToggleFavorite?: (id: string) => void;
}) {
  return (
    <button
      type="button"
      onClick={() => onSelect(o.id)}
      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', selected ? 'opacity-100' : 'opacity-0')} />
      <span className="truncate flex-1">{o.label}</span>
      {onToggleFavorite && <FavoriteStar id={o.id} isFav={isFav} onToggle={onToggleFavorite} />}
    </button>
  );
}

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

  const flatList = (
    <div className="py-1">
      {options.map((o) => (
        <PickerRow key={o.id} o={o} selected={o.id === value} isFav={isFav(o.id)} onSelect={select} onToggleFavorite={onToggleFavorite} />
      ))}
    </div>
  );

  const groupedList = (
    <div className="py-1">
      {(groups ?? []).map((g, gi) => {
        if (g.options.length === 0) return null;
        return (
          <div key={g.label}>
            {gi > 0 && <div className="h-px bg-border mx-2 my-1" />}
            <div className="text-[10px] font-medium text-muted-foreground px-2 py-0.5 uppercase tracking-wider">{g.label}</div>
            {g.options.map((o) => (
              <PickerRow key={o.id} o={o} selected={o.id === value} isFav={isFav(o.id)} onSelect={select} onToggleFavorite={onToggleFavorite} />
            ))}
          </div>
        );
      })}
    </div>
  );

  const list = groups ? groupedList : flatList;

  // Desktop (DropdownMenu) row. Shared by the flat and grouped renderers.
  const renderDesktopItem = (o: { id: string; label: string }) => (
    <DropdownMenuItem key={o.id} onSelect={() => onPick(o.id)} className="text-xs gap-2">
      <Check className={cn('size-3 shrink-0', o.id === value ? 'opacity-100' : 'opacity-0')} />
      <span className="truncate flex-1">{o.label}</span>
      {onToggleFavorite && <FavoriteStar id={o.id} isFav={isFav(o.id)} onToggle={onToggleFavorite} />}
    </DropdownMenuItem>
  );

  if (isMobile) {
    return (
      <>
        <button
          type="button"
          disabled={disabled}
          onClick={() => setOpen(true)}
          aria-label={`${label}: ${currentLabel}`}
          className={cn(
            'inline-flex items-center gap-1 min-h-[36px] rounded-lg border border-border bg-muted/50 hover:bg-muted text-xs hover:text-foreground disabled:opacity-40',
            iconOnly ? 'px-1.5' : 'px-2.5',
            flexible ? 'flex-1 min-w-0 text-foreground' : 'text-muted-foreground',
          )}
        >
          {icon}
          {!iconOnly && (
            <span className={cn('truncate', flexible ? 'flex-1 text-left' : '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={cn(
            'text-xs hover:text-foreground flex items-center gap-1 py-1 rounded-lg border border-border bg-muted/50 hover:bg-muted disabled:opacity-40',
            iconOnly ? 'px-1.5' : 'px-2.5',
            flexible ? 'flex-1 min-w-0 text-foreground' : 'text-muted-foreground',
          )}
        >
          {icon}
          <span className={cn('truncate', flexible ? 'flex-1 text-left' : '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-[180px]">
        {groups
          ? groups.map((g, gi) =>
              g.options.length === 0 ? null : (
                <Fragment key={g.label}>
                  {gi > 0 && <DropdownMenuSeparator />}
                  <DropdownMenuLabel className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground py-0.5">
                    {g.label}
                  </DropdownMenuLabel>
                  {g.options.map(renderDesktopItem)}
                </Fragment>
              ),
            )
          : options.map(renderDesktopItem)}
      </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)}
    />
  );
}

const FAVORITE_MODELS_KEY = 'favorite_models';

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 [favoriteModels, setFavoriteModels] = useState<string[]>([]);

  const hydratedRef = useRef(false);

  // Fetch favorites from settings for the grouped model picker (W5).
  useEffect(() => {
    api.settings.get().then((settings) => {
      const raw = settings[FAVORITE_MODELS_KEY];
      if (Array.isArray(raw)) {
        setFavoriteModels(raw.filter((m): m is string => typeof m === 'string'));
      }
    }).catch(() => { /* settings fetch is best-effort */ });
  }, []);

  const favoriteSet = useMemo(() => new Set(favoriteModels), [favoriteModels]);

  // Toggle a model in/out of the persisted favorites list: optimistic local
  // update + best-effort settings PATCH. The Favorites section re-sorts live.
  const toggleFavorite = useCallback((id: string) => {
    const next = favoriteModels.includes(id)
      ? favoriteModels.filter((m) => m !== id)
      : [...favoriteModels, id];
    setFavoriteModels(next);
    void api.settings.patch({ [FAVORITE_MODELS_KEY]: next }).catch(() => {
      toast.error('Failed to save favorite');
    });
  }, [favoriteModels]);

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

  // Build grouped model options for the native boocode provider (W5).
  // For other providers, use a flat list. Groups: Favorites first, then
  // one section per local provider prefix (matching BooChat's ModelPicker).
  const modelGroups = useMemo<ModelGroup[] | null>(() => {
    if (!currentEntry || currentEntry.name !== 'boocode') return null;
    const models = currentEntry.models;
    if (models.length === 0) return [];

    const favSet = new Set(favoriteModels);
    const modelMap = new Map(models.map((m) => [m.id, m]));

    // Group models by provider prefix (the part before the first slash)
    const byProvider = new Map<string, Array<{ id: string; label: string }>>();
    for (const m of models) {
      const slash = m.id.indexOf('/');
      const providerPrefix = slash > 0 ? m.id.slice(0, slash) : 'other';
      const formatted = { id: m.id, label: formatModelLabel(m.label) };
      const arr = byProvider.get(providerPrefix) ?? [];
      arr.push(formatted);
      byProvider.set(providerPrefix, arr);
    }

    const groups: ModelGroup[] = [];

    // Favorites section: only models that exist in the live inventory
    const favModels = [...favSet]
      .filter((id) => modelMap.has(id))
      .map((id) => ({ id, label: formatModelLabel(modelMap.get(id)!.label) }));
    if (favModels.length > 0) {
      groups.push({ label: 'Favorites', options: favModels });
    }

    // One section per provider group
    for (const [provider, opts] of byProvider) {
      groups.push({ label: provider, options: opts });
    }

    return groups;
  }, [currentEntry, favoriteModels]);

  // Flat model options for non-boocode providers
  const modelOptions = useMemo(
    () => (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: formatModelLabel(m.label) })),
    [currentEntry],
  );

  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 }));
  // Unified permission ladder (Plan / Ask / Bypass) mapped onto this provider's
  // native modes. `value.modeId` stays the wire field; the active unified mode is
  // derived from it.
  const permissionModes = availablePermissionModes(currentEntry?.modes ?? []);
  const currentPermission = permissionForModeId(value.modeId, currentEntry?.modes ?? []);
  const thinkingOpts = thinkingOptions.map((t) => ({ id: t.id, label: t.label }));

  return (
    <div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/20 shrink-0">
      <CompactPicker
        label="Provider"
        value={value.provider}
        options={providerOptions}
        onPick={pickProvider}
        icon={
          <>
            {/* Status indicator lives inside the agent button, to its left:
                normalized agent status (external agents only) + WS liveness. */}
            {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'}
              />
            )}
            {currentEntry?.status === 'loading'
              ? <Loader2 size={13} className="shrink-0 animate-spin" />
              : providerIcon(value.provider)}
          </>
        }
      />
      {/* Permission ladder (Plan / Ask / Bypass) — shown when the provider exposes
          modes. Picks the unified mode; we resolve it to the provider's native
          modeId. Icon varies with the active mode (Bypass is amber). */}
      {permissionModes.length > 0 && (
        <CompactPicker
          label="Permission"
          value={currentPermission}
          options={permissionModes}
          onPick={(perm) =>
            persist({
              ...value,
              modeId: nativeModeForPermission(
                perm as PermissionMode,
                currentEntry?.modes ?? [],
                currentEntry?.defaultModeId ?? null,
              ),
            })
          }
          icon={permissionIcon(currentPermission)}
          iconOnly
        />
      )}
      <CompactPicker
        label="Model"
        value={value.model}
        disabled={modelGroups ? modelGroups.every((g) => g.options.length === 0) : modelOptions.length === 0}
        options={modelOptions}
        groups={modelGroups ?? undefined}
        favorites={favoriteSet}
        onToggleFavorite={modelGroups ? toggleFavorite : undefined}
        onPick={pickModel}
        icon={<Bot size={13} className="shrink-0" />}
        flexible
      />
      {thinkingOpts.length > 0 && (
        <CompactPicker
          label="Thinking"
          value={value.thinkingOptionId ?? ''}
          options={thinkingOpts}
          onPick={(thinkingOptionId) => persist({ ...value, thinkingOptionId })}
          icon={<Brain className="size-3 shrink-0" />}
          iconOnly
        />
      )}
      {/* Refresh — pinned right. Status now lives in the agent button (left). */}
      <div className="ml-auto flex items-center shrink-0">
        <button
          type="button"
          onClick={() => void handleRefresh()}
          disabled={refreshing}
          className="inline-flex items-center justify-center size-9 max-md:min-h-[36px] max-md:min-w-[44px] rounded-lg border border-border bg-muted/50 hover:bg-muted 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>
  );
}
