/**
 * First-run wizard (L5). Shown on the very first IDE launch: it offers the
 * pre-filled, selectable reference profiles (Claude/Codex — only AIs drivable in
 * structured mode, §17.6/D7) with **editable** commands, lets the user detect
 * which CLIs are installed (✓/✗), then saves the chosen profiles and closes the
 * first run.
 *
 * The candidate list is already filtered server-side (`reference_profiles` only
 * exposes selectable profiles), so the wizard renders whatever it receives.
 * Adding an arbitrary custom profile is no longer offered, since we cannot drive
 * it in structured mode.
 *
 * Pure presentation: all behaviour comes from {@link useFirstRun} (the
 * {@link ProfileGateway} port). Profile validation is the pure logic in
 * `./profile`.
 */

import { useCallback, useEffect, useState } from "react";

import type {
  AgentProfile,
  ClaudeProviderCatalogEntry,
  CodexCustomProviderConfig,
  CodexProviderCatalogEntry,
  GatewayError,
  HttpChatConfig,
  LocalModelServerConfig,
  OpenCodeConfig,
  OpenCodeProviderCatalogEntry,
} from "@/domain";
import { useGateways } from "@/app/di";
import { Button, IconButton, Input, Panel, Toolbar, cn } from "@/shared";
import {
  ModelServersPanel,
  ModelServerSelect,
  useModelServers,
} from "@/features/model-servers";
import { useFirstRun, type WizardEntry } from "./useFirstRun";
import {
  defaultHttpChatConfig,
  defaultOpenCodeConfig,
  parseArgs,
  validateProfile,
  type ProfileErrors,
} from "./profile";

/** A small caption above a control. */
function Caption({ children }: { children: React.ReactNode }) {
  return <span className="text-xs font-medium text-muted">{children}</span>;
}

function describeError(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as GatewayError).message);
  }
  return String(e);
}

/** View-model for the OpenCode cloud-provider catalogue (ticket #92). */
interface OpenCodeProviderCatalog {
  providers: OpenCodeProviderCatalogEntry[] | null;
  loading: boolean;
  error: string | null;
  reload: () => void;
}

interface CodexProviderCatalog {
  providers: CodexProviderCatalogEntry[] | null;
  loading: boolean;
  error: string | null;
  reload: () => void;
}

interface ClaudeProviderCatalog {
  providers: ClaudeProviderCatalogEntry[] | null;
  loading: boolean;
  error: string | null;
  reload: () => void;
}

/**
 * Loads the static OpenCode cloud-provider catalogue once for the whole
 * wizard (every Cloud row shares it), so the provider/model pickers can be
 * populated. Exposes a `reload` for the blocking "Réessayer" state.
 */
function useOpenCodeProviderCatalog(): OpenCodeProviderCatalog {
  const { profile } = useGateways();
  const [providers, setProviders] = useState<OpenCodeProviderCatalogEntry[] | null>(
    null,
  );
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      setProviders(await profile.listOpenCodeProviders());
    } catch (e) {
      setProviders(null);
      setError(describeError(e));
    } finally {
      setLoading(false);
    }
  }, [profile]);

  useEffect(() => {
    void load();
  }, [load]);

  return { providers, loading, error, reload: () => void load() };
}

function useCodexProviderCatalog(): CodexProviderCatalog {
  const { profile } = useGateways();
  const [providers, setProviders] = useState<CodexProviderCatalogEntry[] | null>(
    null,
  );
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      setProviders(await profile.listCodexProviders());
    } catch (e) {
      setProviders(null);
      setError(describeError(e));
    } finally {
      setLoading(false);
    }
  }, [profile]);

  useEffect(() => {
    void load();
  }, [load]);

  return { providers, loading, error, reload: () => void load() };
}

function useClaudeProviderCatalog(): ClaudeProviderCatalog {
  const { profile } = useGateways();
  const [providers, setProviders] = useState<ClaudeProviderCatalogEntry[] | null>(
    null,
  );
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      setProviders(await profile.listClaudeProviders());
    } catch (e) {
      setProviders(null);
      setError(describeError(e));
    } finally {
      setLoading(false);
    }
  }, [profile]);

  useEffect(() => {
    void load();
  }, [load]);

  return { providers, loading, error, reload: () => void load() };
}

/**
 * Renders the wizard when it is the first run. Calls `onDone` once the user
 * finishes (so the host can drop the wizard and show the normal UI). Returns
 * Returns `null` while loading. By default it also returns `null` once the first
 * run is done (auto-show path in {@link App}); pass `forceOpen` to render it
 * regardless — used by "Settings ▸ Configure profiles" to reopen the wizard after
 * the first run.
 */
export function FirstRunWizard({
  onDone,
  forceOpen = false,
}: {
  onDone?: () => void;
  /** Render the wizard even when it is no longer the first run. */
  forceOpen?: boolean;
}) {
  // Mode is explicit (ticket #44), driven by the entry point — never inferred
  // from `isFirstRun`: reopening from Settings (`forceOpen`) is the "edit" mode,
  // which pre-loads and pre-selects the already-configured profiles.
  const vm = useFirstRun(forceOpen ? "edit" : "firstRun");
  const modelServers = useModelServers();
  const providerCatalog = useOpenCodeProviderCatalog();
  const codexProviderCatalog = useCodexProviderCatalog();
  const claudeProviderCatalog = useClaudeProviderCatalog();

  if (vm.isFirstRun === null) return null;
  if (!forceOpen && vm.isFirstRun === false) return null;

  async function finish() {
    await vm.finish();
    onDone?.();
  }

  return (
    <Panel
      aria-label="first run setup"
      className="border-primary/40"
      title={
        <div className="flex flex-col">
          <h2 className="text-base font-semibold text-content">Welcome to IdeA</h2>
          <p className="text-sm text-muted">
            Choose which AI CLIs to configure. Commands are pre-filled and
            editable.
          </p>
        </div>
      }
    >
      <div className="flex flex-col gap-4">
        {vm.error && (
          <p role="alert" className="text-sm text-danger">
            {vm.error}
          </p>
        )}

        <Toolbar>
          {/* `detecting` deliberately does NOT disable this button (ticket #28):
              detection is best-effort and may never answer. */}
          <Button
            onClick={() => void vm.detect()}
            disabled={vm.busy}
            className="whitespace-nowrap"
          >
            Detect installed CLIs
          </Button>
          {vm.detecting && (
            <span role="status" className="text-xs text-muted">
              Detecting…
            </span>
          )}
          {/* F36: declare several local OpenCode profiles. Each click clones the
              canonical `opencode-llamacpp` seed into a new, editable row. */}
          <Button
            onClick={() => void vm.addOpenCodeProfile()}
            disabled={vm.busy}
            className="whitespace-nowrap"
          >
            Add OpenCode profile
          </Button>
        </Toolbar>

        {/* F35.2 — declare/edit/delete the local llama.cpp servers an OpenCode
            profile can bind to. Sits above the profile list so a server exists
            before it is picked in the OpenCode dropdown. */}
        <ModelServersPanel vm={modelServers} />

        <ul className="flex list-none flex-col gap-3 p-0">
          {vm.entries.map((entry) => (
            <ProfileRow
              key={entry.profile.id}
              entry={entry}
              servers={modelServers.servers}
              providerCatalog={providerCatalog}
              codexProviderCatalog={codexProviderCatalog}
              claudeProviderCatalog={claudeProviderCatalog}
              onToggle={() => vm.toggle(entry.profile.id)}
              onChange={(p) => vm.updateProfile(entry.profile.id, p)}
              onRemove={() => vm.remove(entry.profile.id)}
              onDuplicate={
                entry.profile.structuredAdapter === "openCode"
                  ? () =>
                      vm.addOpenCodeProfile({
                        name: `${entry.profile.name} (copy)`,
                        opencode: entry.profile.opencode,
                      })
                  : undefined
              }
            />
          ))}
        </ul>

        <footer className="flex gap-2">
          <Button variant="primary" onClick={() => void finish()} disabled={vm.busy}>
            Save and continue
          </Button>
        </footer>
      </div>
    </Panel>
  );
}

/** One editable candidate row: select, edit command/args, see availability. */
function ProfileRow({
  entry,
  servers,
  providerCatalog,
  codexProviderCatalog,
  claudeProviderCatalog,
  onToggle,
  onChange,
  onRemove,
  onDuplicate,
}: {
  entry: WizardEntry;
  /** Declared local model servers (F35.2), for the OpenCode binding dropdown. */
  servers: LocalModelServerConfig[];
  /** OpenCode cloud-provider catalogue (ticket #92), shared across rows. */
  providerCatalog: OpenCodeProviderCatalog;
  codexProviderCatalog: CodexProviderCatalog;
  claudeProviderCatalog: ClaudeProviderCatalog;
  onToggle: () => void;
  onChange: (p: AgentProfile) => void;
  onRemove: () => void;
  /** Present only for OpenCode rows: clone this row into a new profile (F36). */
  onDuplicate?: () => void;
}) {
  const { profile, selected, available } = entry;
  const errors = validateProfile(profile);
  const isOpenCode = profile.structuredAdapter === "openCode";

  return (
    <li className="flex flex-col gap-2 rounded-md border border-border bg-raised p-3">
      <div className="flex items-center gap-2">
        <label className="flex items-center gap-2">
          <input
            type="checkbox"
            checked={selected}
            onChange={onToggle}
            aria-label={`use ${profile.name}`}
            className="accent-primary"
          />
          <strong className="text-sm text-content">{profile.name}</strong>
        </label>
        <span
          aria-label={`${profile.name} availability`}
          className={cn(
            "text-xs",
            available === null
              ? "text-faint"
              : available
                ? "text-success"
                : "text-danger",
          )}
        >
          {available === null ? "—" : available ? "✓ installed" : "✗ not found"}
        </span>
        <div className="ml-auto flex items-center gap-1">
          {onDuplicate && (
            <Button
              size="sm"
              aria-label={`duplicate ${profile.name}`}
              onClick={onDuplicate}
            >
              Duplicate
            </Button>
          )}
          <IconButton
            size="sm"
            aria-label={`remove ${profile.name}`}
            onClick={onRemove}
          >
            ×
          </IconButton>
        </div>
      </div>

      {/* F36: an OpenCode profile's name is identity-neutral but user-facing, so
          it is editable per profile (several local models coexist). */}
      {isOpenCode && (
        <label className="flex flex-col gap-1">
          <Caption>Name</Caption>
          <Input
            aria-label={`${profile.name} name`}
            value={profile.name}
            invalid={Boolean(errors.name)}
            onChange={(e) => onChange({ ...profile, name: e.target.value })}
          />
          {errors.name && <small className="text-xs text-danger">{errors.name}</small>}
        </label>
      )}

      <label className="flex flex-col gap-1">
        <Caption>Command</Caption>
        <Input
          aria-label={`${profile.name} command`}
          value={profile.command}
          invalid={Boolean(errors.command)}
          onChange={(e) => onChange({ ...profile, command: e.target.value })}
        />
        {errors.command && <small className="text-xs text-danger">{errors.command}</small>}
      </label>

      <label className="flex flex-col gap-1">
        <Caption>Arguments</Caption>
        <Input
          aria-label={`${profile.name} args`}
          value={profile.args.join(" ")}
          onChange={(e) => onChange({ ...profile, args: parseArgs(e.target.value) })}
        />
      </label>

      {profile.structuredAdapter === "openAiCompatible" && (
        <HttpChatFields profile={profile} errors={errors} onChange={onChange} />
      )}

      {profile.structuredAdapter === "openCode" && (
        <OpenCodeModeFields
          profile={profile}
          errors={errors}
          servers={servers}
          providerCatalog={providerCatalog}
          onChange={onChange}
        />
      )}

      {profile.structuredAdapter === "codex" &&
        (selected || profile.codexProvider) && (
          <CodexProviderFields
            profile={profile}
            catalog={codexProviderCatalog}
            onChange={onChange}
          />
        )}

      {profile.structuredAdapter === "claude" &&
        (selected || profile.claudeProvider) && (
          <ClaudeProviderFields
            profile={profile}
            catalog={claudeProviderCatalog}
            onChange={onChange}
          />
        )}
    </li>
  );
}

/**
 * Segmented control (F — ticket #92) choosing whether an OpenCode profile runs
 * against the local `llama.cpp` endpoint or a cloud provider from the OpenCode
 * registry, and renders the matching sub-form. The two sub-forms never overlap;
 * switching segments keeps the inactive one's draft in memory (component-local
 * state) without touching `profile` until it is actually submitted.
 */
function OpenCodeModeFields({
  profile,
  errors,
  servers,
  providerCatalog,
  onChange,
}: {
  profile: AgentProfile;
  errors: ProfileErrors;
  servers: LocalModelServerConfig[];
  providerCatalog: OpenCodeProviderCatalog;
  onChange: (p: AgentProfile) => void;
}) {
  const [mode, setMode] = useState<"local" | "cloud">(
    profile.opencodeProvider ? "cloud" : "local",
  );

  return (
    <div className="mt-1 flex flex-col gap-2">
      <div
        role="radiogroup"
        aria-label="mode du profil OpenCode"
        className="flex w-fit gap-1 rounded-md border border-border bg-raised p-0.5"
      >
        {(
          [
            { id: "local", label: "Local (llama.cpp)" },
            { id: "cloud", label: "Provider cloud" },
          ] as const
        ).map((seg) => {
          const active = mode === seg.id;
          return (
            <button
              key={seg.id}
              type="button"
              role="radio"
              aria-checked={active}
              onClick={() => setMode(seg.id)}
              className={cn(
                "rounded px-2.5 py-1 text-xs font-medium transition-colors",
                active
                  ? "bg-primary text-on-primary"
                  : "text-muted hover:text-content",
              )}
            >
              {seg.label}
            </button>
          );
        })}
      </div>

      {mode === "local" && (
        <OpenCodeFields
          profile={profile}
          errors={errors}
          servers={servers}
          onChange={onChange}
        />
      )}

      {mode === "cloud" && (
        <OpenCodeProviderFields
          profile={profile}
          catalog={providerCatalog}
          onChange={onChange}
        />
      )}
    </div>
  );
}

/** Field-keyed validation errors for the Cloud sub-form, surfaced on submit. */
interface CloudFieldErrors {
  providerId?: string;
  model?: string;
  apiKey?: string;
  npm?: string;
  baseUrl?: string;
}

/** Sentinel `<option>` value picking the custom-provider sub-form. */
const CUSTOM_PROVIDER_VALUE = "__custom__";

/** Default AI SDK package pre-filled for a fresh custom-provider draft. */
const DEFAULT_CUSTOM_NPM = "@ai-sdk/openai-compatible";

/**
 * The OpenCode **cloud** provider config section (ticket #92): provider ➜
 * model (cascading selects fed by the dynamic catalogue, now up to ~170
 * entries) ➜ API key, or — via the "Autre / personnalisé" catalogue entry — a
 * free-form OpenAI-compatible endpoint (provider id, npm SDK package, base
 * URL, model id, optional display name). The key is never pre-filled (create
 * or edit) since the backend never returns it — it reseals whatever literal
 * it receives on every save, so editing an existing cloud profile requires
 * re-entering it every time (§3 of the spec).
 */
function OpenCodeProviderFields({
  profile,
  catalog,
  onChange,
}: {
  profile: AgentProfile;
  catalog: OpenCodeProviderCatalog;
  onChange: (p: AgentProfile) => void;
}) {
  const { profile: profileGateway } = useGateways();
  const existing = profile.opencodeProvider;
  const [mode, setMode] = useState<"catalog" | "custom">(
    existing?.custom ? "custom" : "catalog",
  );
  const [providerId, setProviderId] = useState(existing?.providerId ?? "");
  const [model, setModel] = useState(existing?.model ?? "");
  const [providerFilter, setProviderFilter] = useState("");
  const [customNpm, setCustomNpm] = useState(existing?.custom?.npm ?? DEFAULT_CUSTOM_NPM);
  const [customBaseUrl, setCustomBaseUrl] = useState(existing?.custom?.baseUrl ?? "");
  const [customDisplayName, setCustomDisplayName] = useState(
    existing?.custom?.displayName ?? "",
  );
  const [apiKey, setApiKey] = useState("");
  const [showKey, setShowKey] = useState(false);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [fieldErrors, setFieldErrors] = useState<CloudFieldErrors>({});

  const isEditing = existing !== undefined;
  const models =
    catalog.providers?.find((p) => p.providerId === providerId)?.models ?? [];
  const catalogReady = catalog.providers !== null && !catalog.loading;
  const filteredProviders = (catalog.providers ?? []).filter((p) => {
    // Always keep the current selection visible even if it no longer matches
    // the filter, so the <select> doesn't silently lose its value.
    if (p.providerId === providerId) return true;
    const q = providerFilter.trim().toLowerCase();
    if (q.length === 0) return true;
    return (
      p.displayName.toLowerCase().includes(q) ||
      p.providerId.toLowerCase().includes(q)
    );
  });
  const saveDisabled =
    saving ||
    apiKey.length === 0 ||
    (mode === "catalog" && (!catalogReady || Boolean(catalog.error)));

  async function save() {
    const errors: CloudFieldErrors = {};
    if (providerId.trim().length === 0) {
      errors.providerId = "Le provider est obligatoire.";
    }
    if (model.trim().length === 0) errors.model = "Le modèle est obligatoire.";
    if (mode === "custom") {
      if (customNpm.trim().length === 0) errors.npm = "Le paquet npm est obligatoire.";
      if (customBaseUrl.trim().length === 0) {
        errors.baseUrl = "L'URL de base est obligatoire.";
      }
    }
    if (apiKey.length === 0) errors.apiKey = "La clé API est obligatoire.";
    setFieldErrors(errors);
    if (Object.keys(errors).length > 0) return;

    setSaving(true);
    setSaveError(null);
    try {
      const saved = await profileGateway.saveOpenCodeProviderProfile({
        profile,
        providerId: providerId.trim(),
        model: model.trim(),
        apiKey,
        ...(mode === "custom"
          ? {
              custom: {
                npm: customNpm.trim(),
                baseUrl: customBaseUrl.trim(),
                displayName:
                  customDisplayName.trim().length > 0
                    ? customDisplayName.trim()
                    : undefined,
              },
            }
          : {}),
      });
      onChange(saved);
      setApiKey("");
    } catch (e) {
      setSaveError(describeError(e));
    } finally {
      setSaving(false);
    }
  }

  return (
    <fieldset className="mt-1 flex flex-col gap-2 rounded-md border border-border/70 p-2">
      <legend className="px-1 text-xs font-medium text-muted">
        Provider cloud (OpenCode)
      </legend>

      {saveError && (
        <p role="alert" className="text-sm text-danger">
          {saveError}
        </p>
      )}

      {catalog.loading && (
        <p className="text-xs text-faint">Chargement des providers…</p>
      )}
      {catalog.error && (
        <div className="flex flex-col gap-1">
          <p role="alert" className="text-sm text-danger">
            Impossible de charger la liste des providers cloud.
          </p>
          <Button size="sm" onClick={() => catalog.reload()} className="w-fit">
            Réessayer
          </Button>
        </div>
      )}

      {mode === "catalog" && (
        <>
          <label className="flex flex-col gap-1">
            <Caption>Provider</Caption>
            {catalogReady && (
              <input
                type="text"
                aria-label={`${profile.name} provider search`}
                placeholder="Rechercher un provider…"
                value={providerFilter}
                onChange={(e) => setProviderFilter(e.target.value)}
                className="h-8 w-full rounded-md border border-border bg-raised px-3 text-xs text-content outline-none"
              />
            )}
            <select
              aria-label={`${profile.name} provider`}
              value={providerId}
              disabled={!catalogReady}
              onChange={(e) => {
                const v = e.target.value;
                if (v === CUSTOM_PROVIDER_VALUE) {
                  setMode("custom");
                  setProviderId("");
                  setModel("");
                } else {
                  setProviderId(v);
                  setModel("");
                }
                setFieldErrors((prev) => ({ ...prev, providerId: undefined }));
              }}
              className={cn(
                "h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
                "disabled:cursor-not-allowed disabled:opacity-50",
                fieldErrors.providerId ? "border-danger" : "border-border",
              )}
            >
              <option value="" disabled>
                {catalog.loading ? "Chargement des providers…" : "Choisir un provider…"}
              </option>
              {filteredProviders.map((p) => (
                <option key={p.providerId} value={p.providerId}>
                  {p.displayName}
                </option>
              ))}
              <option value={CUSTOM_PROVIDER_VALUE}>Autre / personnalisé…</option>
            </select>
            {fieldErrors.providerId && (
              <small className="text-xs text-danger">{fieldErrors.providerId}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Modèle</Caption>
            <select
              aria-label={`${profile.name} model`}
              value={model}
              disabled={providerId.length === 0}
              onChange={(e) => {
                setModel(e.target.value);
                setFieldErrors((prev) => ({ ...prev, model: undefined }));
              }}
              className={cn(
                "h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
                "disabled:cursor-not-allowed disabled:opacity-50",
                fieldErrors.model ? "border-danger" : "border-border",
              )}
            >
              <option value="" disabled>
                {providerId.length === 0 ? "—" : "Choisir un modèle…"}
              </option>
              {models.map((m) => (
                <option key={m} value={m}>
                  {m}
                </option>
              ))}
            </select>
            {fieldErrors.model && (
              <small className="text-xs text-danger">{fieldErrors.model}</small>
            )}
          </label>
        </>
      )}

      {mode === "custom" && (
        <fieldset className="flex flex-col gap-2 rounded-md border border-border/50 p-2">
          <legend className="px-1 text-xs font-medium text-muted">
            Provider personnalisé
          </legend>

          <Button
            variant="ghost"
            size="sm"
            className="w-fit"
            onClick={() => {
              setMode("catalog");
              setProviderId("");
              setModel("");
              setFieldErrors({});
            }}
          >
            ← Choisir un provider du catalogue
          </Button>

          <label className="flex flex-col gap-1">
            <Caption>Identifiant du provider</Caption>
            <Input
              aria-label={`${profile.name} custom provider id`}
              value={providerId}
              placeholder="ex. mon-provider"
              invalid={Boolean(fieldErrors.providerId)}
              onChange={(e) => {
                setProviderId(e.target.value);
                setFieldErrors((prev) => ({ ...prev, providerId: undefined }));
              }}
            />
            {fieldErrors.providerId && (
              <small className="text-xs text-danger">{fieldErrors.providerId}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Paquet npm</Caption>
            <Input
              aria-label={`${profile.name} custom npm package`}
              value={customNpm}
              placeholder={DEFAULT_CUSTOM_NPM}
              invalid={Boolean(fieldErrors.npm)}
              onChange={(e) => {
                setCustomNpm(e.target.value);
                setFieldErrors((prev) => ({ ...prev, npm: undefined }));
              }}
            />
            {fieldErrors.npm && (
              <small className="text-xs text-danger">{fieldErrors.npm}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>URL de base</Caption>
            <Input
              aria-label={`${profile.name} custom base url`}
              value={customBaseUrl}
              placeholder="https://api.mon-provider.example/v1"
              invalid={Boolean(fieldErrors.baseUrl)}
              onChange={(e) => {
                setCustomBaseUrl(e.target.value);
                setFieldErrors((prev) => ({ ...prev, baseUrl: undefined }));
              }}
            />
            {fieldErrors.baseUrl && (
              <small className="text-xs text-danger">{fieldErrors.baseUrl}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Modèle</Caption>
            <Input
              aria-label={`${profile.name} model`}
              value={model}
              placeholder="ex. mon-modele-1"
              invalid={Boolean(fieldErrors.model)}
              onChange={(e) => {
                setModel(e.target.value);
                setFieldErrors((prev) => ({ ...prev, model: undefined }));
              }}
            />
            {fieldErrors.model && (
              <small className="text-xs text-danger">{fieldErrors.model}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Libellé du modèle (optionnel)</Caption>
            <Input
              aria-label={`${profile.name} custom display name`}
              value={customDisplayName}
              placeholder="ex. Mon modèle"
              onChange={(e) => setCustomDisplayName(e.target.value)}
            />
          </label>
        </fieldset>
      )}

      <label className="flex flex-col gap-1">
        <Caption>Clé API</Caption>
        <div className="flex items-center gap-1">
          <Input
            aria-label={`${profile.name} api key`}
            type={showKey ? "text" : "password"}
            value={apiKey}
            placeholder={
              isEditing
                ? "Ressaisissez la clé API pour confirmer l'enregistrement"
                : "ex. sk-ant-…"
            }
            invalid={Boolean(fieldErrors.apiKey)}
            onChange={(e) => {
              setApiKey(e.target.value);
              setFieldErrors((prev) => ({ ...prev, apiKey: undefined }));
            }}
          />
          <IconButton
            size="sm"
            aria-label="afficher/masquer la clé API"
            onClick={() => setShowKey((v) => !v)}
          >
            {showKey ? "🙈" : "👁"}
          </IconButton>
        </div>
        {fieldErrors.apiKey && (
          <small className="text-xs text-danger">{fieldErrors.apiKey}</small>
        )}
        <small className="text-xs text-faint">
          Jamais affichée ni renvoyée par IdeA une fois enregistrée ; stockée
          chiffrée localement.
        </small>
        {isEditing && (
          <small className="text-xs text-muted">
            Pour des raisons de sécurité, la clé n&apos;est jamais réaffichée :
            ressaisissez-la à chaque modification de ce profil.
          </small>
        )}
      </label>

      <Button
        variant="primary"
        size="sm"
        loading={saving}
        disabled={saveDisabled}
        onClick={() => void save()}
        className="w-fit"
      >
        Enregistrer
      </Button>
    </fieldset>
  );
}

type SimpleProviderCatalogEntry =
  | CodexProviderCatalogEntry
  | ClaudeProviderCatalogEntry;

interface ProviderModelSecretFieldsProps {
  engine: "Codex" | "Claude";
  profile: AgentProfile;
  catalog: {
    providers: SimpleProviderCatalogEntry[] | null;
    loading: boolean;
    error: string | null;
    reload: () => void;
  };
  existing:
    | AgentProfile["codexProvider"]
    | AgentProfile["claudeProvider"]
    | undefined;
  customSupported: boolean;
  saveProfile: (input: {
    providerId: string;
    model: string;
    apiKey: string;
    custom?: CodexCustomProviderConfig;
  }) => Promise<AgentProfile>;
  onChange: (p: AgentProfile) => void;
}

function ProviderModelSecretFields({
  engine,
  profile,
  catalog,
  existing,
  customSupported,
  saveProfile,
  onChange,
}: ProviderModelSecretFieldsProps) {
  const [mode, setMode] = useState<"catalog" | "custom">(
    existing && "custom" in existing && existing.custom ? "custom" : "catalog",
  );
  const [providerId, setProviderId] = useState(existing?.providerId ?? "");
  const [model, setModel] = useState(existing?.model ?? "");
  const [providerFilter, setProviderFilter] = useState("");
  const existingCustom =
    existing && "custom" in existing ? existing.custom : undefined;
  const [customBaseUrl, setCustomBaseUrl] = useState(existingCustom?.baseUrl ?? "");
  const [customDisplayName, setCustomDisplayName] = useState(
    existingCustom?.displayName ?? "",
  );
  const [apiKey, setApiKey] = useState("");
  const [showKey, setShowKey] = useState(false);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [fieldErrors, setFieldErrors] = useState<CloudFieldErrors>({});

  const isEditing = existing !== undefined;
  const catalogReady = catalog.providers !== null && !catalog.loading;
  const models =
    catalog.providers?.find((p) => p.providerId === providerId)?.models ?? [];
  const filteredProviders = (catalog.providers ?? []).filter((p) => {
    if (p.providerId === providerId) return true;
    const q = providerFilter.trim().toLowerCase();
    if (q.length === 0) return true;
    return (
      p.displayName.toLowerCase().includes(q) ||
      p.providerId.toLowerCase().includes(q)
    );
  });
  const saveDisabled =
    saving ||
    apiKey.length === 0 ||
    (mode === "catalog" && (!catalogReady || Boolean(catalog.error)));

  async function save() {
    const errors: CloudFieldErrors = {};
    if (providerId.trim().length === 0) {
      errors.providerId = "Le provider est obligatoire.";
    }
    if (model.trim().length === 0) errors.model = "Le modèle est obligatoire.";
    if (mode === "custom" && customBaseUrl.trim().length === 0) {
      errors.baseUrl = "L'URL de base est obligatoire.";
    }
    if (apiKey.length === 0) errors.apiKey = "La clé API est obligatoire.";
    setFieldErrors(errors);
    if (Object.keys(errors).length > 0) return;

    setSaving(true);
    setSaveError(null);
    try {
      const saved = await saveProfile({
        providerId: providerId.trim(),
        model: model.trim(),
        apiKey,
        ...(mode === "custom"
          ? {
              custom: {
                baseUrl: customBaseUrl.trim(),
                displayName:
                  customDisplayName.trim().length > 0
                    ? customDisplayName.trim()
                    : undefined,
              },
            }
          : {}),
      });
      onChange(saved);
      setApiKey("");
    } catch (e) {
      setSaveError(describeError(e));
    } finally {
      setSaving(false);
    }
  }

  return (
    <fieldset className="mt-1 flex flex-col gap-2 rounded-md border border-border/70 p-2">
      <legend className="px-1 text-xs font-medium text-muted">
        Provider cloud ({engine})
      </legend>

      {saveError && (
        <p role="alert" className="text-sm text-danger">
          {saveError}
        </p>
      )}

      {catalog.loading && (
        <p className="text-xs text-faint">Chargement des providers…</p>
      )}
      {catalog.error && (
        <div className="flex flex-col gap-1">
          <p role="alert" className="text-sm text-danger">
            Impossible de charger la liste des providers cloud.
          </p>
          <Button size="sm" onClick={() => catalog.reload()} className="w-fit">
            Réessayer
          </Button>
        </div>
      )}

      {mode === "catalog" && (
        <>
          <label className="flex flex-col gap-1">
            <Caption>Provider</Caption>
            {catalogReady && (
              <input
                type="text"
                aria-label={`${profile.name} provider search`}
                placeholder="Rechercher un provider…"
                value={providerFilter}
                onChange={(e) => setProviderFilter(e.target.value)}
                className="h-8 w-full rounded-md border border-border bg-raised px-3 text-xs text-content outline-none"
              />
            )}
            <select
              aria-label={`${profile.name} provider`}
              value={providerId}
              disabled={!catalogReady}
              onChange={(e) => {
                const v = e.target.value;
                if (v === CUSTOM_PROVIDER_VALUE) {
                  setMode("custom");
                  setProviderId("");
                } else {
                  setProviderId(v);
                }
                setModel("");
                setFieldErrors((prev) => ({
                  ...prev,
                  providerId: undefined,
                  model: undefined,
                }));
              }}
              className={cn(
                "h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
                "disabled:cursor-not-allowed disabled:opacity-50",
                fieldErrors.providerId ? "border-danger" : "border-border",
              )}
            >
              <option value="" disabled>
                {catalog.loading ? "Chargement des providers…" : "Choisir un provider…"}
              </option>
              {filteredProviders.map((p) => (
                <option key={p.providerId} value={p.providerId}>
                  {p.displayName}
                </option>
              ))}
              {customSupported && (
                <option value={CUSTOM_PROVIDER_VALUE}>Autre / personnalisé…</option>
              )}
            </select>
            {fieldErrors.providerId && (
              <small className="text-xs text-danger">{fieldErrors.providerId}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Modèle</Caption>
            <select
              aria-label={`${profile.name} model`}
              value={model}
              disabled={providerId.length === 0}
              onChange={(e) => {
                setModel(e.target.value);
                setFieldErrors((prev) => ({ ...prev, model: undefined }));
              }}
              className={cn(
                "h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
                "disabled:cursor-not-allowed disabled:opacity-50",
                fieldErrors.model ? "border-danger" : "border-border",
              )}
            >
              <option value="" disabled>
                {providerId.length === 0 ? "—" : "Choisir un modèle…"}
              </option>
              {models.map((m) => (
                <option key={m} value={m}>
                  {m}
                </option>
              ))}
            </select>
            {fieldErrors.model && (
              <small className="text-xs text-danger">{fieldErrors.model}</small>
            )}
          </label>
        </>
      )}

      {mode === "custom" && customSupported && (
        <fieldset className="flex flex-col gap-2 rounded-md border border-border/50 p-2">
          <legend className="px-1 text-xs font-medium text-muted">
            Provider personnalisé
          </legend>

          <Button
            variant="ghost"
            size="sm"
            className="w-fit"
            onClick={() => {
              setMode("catalog");
              setProviderId("");
              setModel("");
              setFieldErrors({});
            }}
          >
            ← Choisir un provider du catalogue
          </Button>

          <label className="flex flex-col gap-1">
            <Caption>Identifiant du provider</Caption>
            <Input
              aria-label={`${profile.name} custom provider id`}
              value={providerId}
              placeholder="ex. mon-provider"
              invalid={Boolean(fieldErrors.providerId)}
              onChange={(e) => {
                setProviderId(e.target.value);
                setFieldErrors((prev) => ({ ...prev, providerId: undefined }));
              }}
            />
            {fieldErrors.providerId && (
              <small className="text-xs text-danger">{fieldErrors.providerId}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>URL de base</Caption>
            <Input
              aria-label={`${profile.name} custom base url`}
              value={customBaseUrl}
              placeholder="https://api.mon-provider.example/v1"
              invalid={Boolean(fieldErrors.baseUrl)}
              onChange={(e) => {
                setCustomBaseUrl(e.target.value);
                setFieldErrors((prev) => ({ ...prev, baseUrl: undefined }));
              }}
            />
            {fieldErrors.baseUrl && (
              <small className="text-xs text-danger">{fieldErrors.baseUrl}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Modèle</Caption>
            <Input
              aria-label={`${profile.name} model`}
              value={model}
              placeholder="ex. mon-modele-1"
              invalid={Boolean(fieldErrors.model)}
              onChange={(e) => {
                setModel(e.target.value);
                setFieldErrors((prev) => ({ ...prev, model: undefined }));
              }}
            />
            {fieldErrors.model && (
              <small className="text-xs text-danger">{fieldErrors.model}</small>
            )}
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Libellé du provider (optionnel)</Caption>
            <Input
              aria-label={`${profile.name} custom display name`}
              value={customDisplayName}
              placeholder="ex. Mon provider"
              onChange={(e) => setCustomDisplayName(e.target.value)}
            />
          </label>
        </fieldset>
      )}

      <label className="flex flex-col gap-1">
        <Caption>Clé API</Caption>
        <div className="flex items-center gap-1">
          <Input
            aria-label={`${profile.name} api key`}
            type={showKey ? "text" : "password"}
            value={apiKey}
            placeholder={
              isEditing
                ? "Ressaisissez la clé API pour confirmer l'enregistrement"
                : "ex. sk-…"
            }
            invalid={Boolean(fieldErrors.apiKey)}
            onChange={(e) => {
              setApiKey(e.target.value);
              setFieldErrors((prev) => ({ ...prev, apiKey: undefined }));
            }}
          />
          <IconButton
            size="sm"
            aria-label="afficher/masquer la clé API"
            onClick={() => setShowKey((v) => !v)}
          >
            {showKey ? "🙈" : "👁"}
          </IconButton>
        </div>
        {fieldErrors.apiKey && (
          <small className="text-xs text-danger">{fieldErrors.apiKey}</small>
        )}
        <small className="text-xs text-faint">
          Jamais affichée ni renvoyée par IdeA une fois enregistrée ; stockée
          chiffrée localement.
        </small>
        {isEditing && (
          <small className="text-xs text-muted">
            Le profil conserve sa référence de secret existante ; la clé n'est
            jamais réaffichée côté UI.
          </small>
        )}
      </label>

      <Button
        variant="primary"
        size="sm"
        aria-label={`Enregistrer ${engine}`}
        loading={saving}
        disabled={saveDisabled}
        onClick={() => void save()}
        className="w-fit"
      >
        Enregistrer
      </Button>
    </fieldset>
  );
}

function CodexProviderFields({
  profile,
  catalog,
  onChange,
}: {
  profile: AgentProfile;
  catalog: CodexProviderCatalog;
  onChange: (p: AgentProfile) => void;
}) {
  const { profile: profileGateway } = useGateways();
  return (
    <ProviderModelSecretFields
      engine="Codex"
      profile={profile}
      catalog={catalog}
      existing={profile.codexProvider}
      customSupported={catalog.providers?.some((p) => p.customSupported) ?? false}
      saveProfile={(input) =>
        profileGateway.saveCodexProviderProfile({
          profile,
          providerId: input.providerId,
          model: input.model,
          apiKey: input.apiKey,
          custom: input.custom,
        })
      }
      onChange={onChange}
    />
  );
}

function ClaudeProviderFields({
  profile,
  catalog,
  onChange,
}: {
  profile: AgentProfile;
  catalog: ClaudeProviderCatalog;
  onChange: (p: AgentProfile) => void;
}) {
  const { profile: profileGateway } = useGateways();
  return (
    <ProviderModelSecretFields
      engine="Claude"
      profile={profile}
      catalog={catalog}
      existing={profile.claudeProvider}
      customSupported={false}
      saveProfile={(input) =>
        profileGateway.saveClaudeProviderProfile({
          profile,
          providerId: input.providerId,
          model: input.model,
          apiKey: input.apiKey,
        })
      }
      onChange={onChange}
    />
  );
}

/**
 * The OpenCode + llama.cpp config section: the base URL of the local
 * `llama-server` (`/v1`), the model it serves, and an optional API key (the key
 * itself — a local llama.cpp usually needs none). Rendered only for a `openCode`
 * profile; edits flow back through `onChange` as a patched `opencode`.
 */
function OpenCodeFields({
  profile,
  errors,
  servers,
  onChange,
}: {
  profile: AgentProfile;
  errors: ProfileErrors;
  /** Declared local model servers (F35.2), for the binding dropdown. */
  servers: LocalModelServerConfig[];
  onChange: (p: AgentProfile) => void;
}) {
  const opencode = profile.opencode ?? defaultOpenCodeConfig();
  const patch = (next: Partial<OpenCodeConfig>) =>
    onChange({ ...profile, opencode: { ...opencode, ...next } });

  return (
    <fieldset className="mt-1 flex flex-col gap-2 rounded-md border border-border/70 p-2">
      <legend className="px-1 text-xs font-medium text-muted">
        Local model (OpenCode + llama.cpp)
      </legend>

      <label className="flex flex-col gap-1">
        <Caption>Base URL</Caption>
        <Input
          aria-label={`${profile.name} base url`}
          value={opencode.baseURL}
          placeholder="http://localhost:8080/v1"
          invalid={Boolean(errors.baseURL)}
          onChange={(e) => patch({ baseURL: e.target.value })}
        />
        {errors.baseURL && (
          <small className="text-xs text-danger">{errors.baseURL}</small>
        )}
      </label>

      <label className="flex flex-col gap-1">
        <Caption>Model</Caption>
        <Input
          aria-label={`${profile.name} model`}
          value={opencode.model}
          placeholder="qwen3-coder-30b"
          invalid={Boolean(errors.model)}
          onChange={(e) => patch({ model: e.target.value })}
        />
        {errors.model && <small className="text-xs text-danger">{errors.model}</small>}
      </label>

      <label className="flex flex-col gap-1">
        <Caption>API key (optional — a local llama.cpp needs none)</Caption>
        <Input
          aria-label={`${profile.name} api key`}
          value={opencode.apiKey ?? ""}
          placeholder="e.g. sk-no-key"
          onChange={(e) => {
            const v = e.target.value;
            patch({ apiKey: v.length === 0 ? undefined : v });
          }}
        />
      </label>

      <div className="flex flex-wrap gap-4">
        <label className="flex items-center gap-2">
          <input
            type="checkbox"
            aria-label={`${profile.name} reasoning`}
            // Effective backend default is `true`; treat omitted as enabled.
            checked={opencode.reasoning ?? true}
            onChange={(e) => patch({ reasoning: e.target.checked })}
            className="accent-primary"
          />
          <Caption>Reasoning</Caption>
        </label>

        <label className="flex items-center gap-2">
          <input
            type="checkbox"
            aria-label={`${profile.name} attachment`}
            // Effective backend default is `false`.
            checked={opencode.attachment ?? false}
            onChange={(e) => patch({ attachment: e.target.checked })}
            className="accent-primary"
          />
          <Caption>Attachments</Caption>
        </label>
      </div>

      <label className="flex flex-col gap-1">
        <Caption>
          Local model server (bind to a managed llama.cpp server, or none)
        </Caption>
        <ModelServerSelect
          ariaLabel={`${profile.name} local model server`}
          servers={servers}
          value={opencode.localModelServerId}
          onChange={(serverId) => patch({ localModelServerId: serverId })}
        />
      </label>
    </fieldset>
  );
}

/** Parses a number field: blank ⇒ `undefined` (backend applies its default). */
function parseOptInt(raw: string): number | undefined {
  const trimmed = raw.trim();
  if (trimmed.length === 0) return undefined;
  const n = Number(trimmed);
  return Number.isFinite(n) ? n : NaN;
}

/**
 * The OpenAI-compatible (local/LAN) HTTP config section: endpoint, model, the
 * *name* of the API-key env var (never the key), and optional timeouts / tool
 * guard. Rendered only for a `openAiCompatible` profile; edits flow back through
 * `onChange` as a patched `chatHttp`.
 */
function HttpChatFields({
  profile,
  errors,
  onChange,
}: {
  profile: AgentProfile;
  errors: ProfileErrors;
  onChange: (p: AgentProfile) => void;
}) {
  const http = profile.chatHttp ?? defaultHttpChatConfig();
  const patch = (next: Partial<HttpChatConfig>) =>
    onChange({ ...profile, chatHttp: { ...http, ...next } });

  return (
    <fieldset className="mt-1 flex flex-col gap-2 rounded-md border border-border/70 p-2">
      <legend className="px-1 text-xs font-medium text-muted">
        Local / LAN model (OpenAI-compatible)
      </legend>

      <label className="flex flex-col gap-1">
        <Caption>Endpoint</Caption>
        <Input
          aria-label={`${profile.name} endpoint`}
          value={http.endpoint}
          placeholder="http://localhost:11434/v1"
          invalid={Boolean(errors.endpoint)}
          onChange={(e) => patch({ endpoint: e.target.value })}
        />
        {errors.endpoint && (
          <small className="text-xs text-danger">{errors.endpoint}</small>
        )}
      </label>

      <label className="flex flex-col gap-1">
        <Caption>Model</Caption>
        <Input
          aria-label={`${profile.name} model`}
          value={http.model}
          placeholder="qwen2.5-coder"
          invalid={Boolean(errors.model)}
          onChange={(e) => patch({ model: e.target.value })}
        />
        {errors.model && <small className="text-xs text-danger">{errors.model}</small>}
      </label>

      <label className="flex flex-col gap-1">
        <Caption>API key — environment variable name (never the key itself)</Caption>
        <Input
          aria-label={`${profile.name} api key env`}
          value={http.apiKeyEnv ?? ""}
          placeholder="e.g. OPENAI_API_KEY (variable name, not the secret)"
          invalid={Boolean(errors.apiKeyEnv)}
          onChange={(e) => {
            const v = e.target.value.trim();
            patch({ apiKeyEnv: v.length === 0 ? undefined : v });
          }}
        />
        {errors.apiKeyEnv && (
          <small className="text-xs text-danger">{errors.apiKeyEnv}</small>
        )}
      </label>

      <div className="flex flex-wrap gap-2">
        <label className="flex min-w-[8rem] flex-1 flex-col gap-1">
          <Caption>Request timeout (ms)</Caption>
          <Input
            aria-label={`${profile.name} request timeout`}
            value={http.requestTimeoutMs?.toString() ?? ""}
            inputMode="numeric"
            invalid={Boolean(errors.requestTimeoutMs)}
            onChange={(e) => patch({ requestTimeoutMs: parseOptInt(e.target.value) })}
          />
          {errors.requestTimeoutMs && (
            <small className="text-xs text-danger">{errors.requestTimeoutMs}</small>
          )}
        </label>

        <label className="flex min-w-[8rem] flex-1 flex-col gap-1">
          <Caption>Connect timeout (ms)</Caption>
          <Input
            aria-label={`${profile.name} connect timeout`}
            value={http.connectTimeoutMs?.toString() ?? ""}
            inputMode="numeric"
            invalid={Boolean(errors.connectTimeoutMs)}
            onChange={(e) => patch({ connectTimeoutMs: parseOptInt(e.target.value) })}
          />
          {errors.connectTimeoutMs && (
            <small className="text-xs text-danger">{errors.connectTimeoutMs}</small>
          )}
        </label>

        <label className="flex min-w-[8rem] flex-1 flex-col gap-1">
          <Caption>Max tool iterations</Caption>
          <Input
            aria-label={`${profile.name} max tool iterations`}
            value={http.maxToolIterations?.toString() ?? ""}
            inputMode="numeric"
            invalid={Boolean(errors.maxToolIterations)}
            onChange={(e) => patch({ maxToolIterations: parseOptInt(e.target.value) })}
          />
          {errors.maxToolIterations && (
            <small className="text-xs text-danger">{errors.maxToolIterations}</small>
          )}
        </label>
      </div>
    </fieldset>
  );
}
