/**
 * `ModelServersPanel` (F35, wizard V2) — declare / edit / delete the local
 * `llama.cpp` servers an OpenCode profile can bind to. Presentational: all I/O
 * state comes from a {@link ModelServersViewModel} passed in (`useModelServers`),
 * so the panel renders and is testable in isolation.
 *
 * The editor is a plain-language wizard: a source segmented control (download
 * from Hugging Face vs. a local `.gguf` file), a shared "model name in IdeA"
 * field, the server settings, a collapsed advanced section for the raw
 * llama.cpp knobs, and a read-only command preview built by the backend (never
 * reconstructed here).
 */

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

import type {
  LocalModelServerConfig,
  ModelArtifact,
  ModelServerCommandPreview,
  ModelSource,
  StopPolicy,
} from "@/domain";
import { Button, IconButton, Input, Panel, cn } from "@/shared";
import type { ModelServersViewModel } from "./useModelServers";
import {
  deriveServedModelName,
  emptyModelServer,
  parseArgs,
  reservedFlagsIn,
  validateModelServer,
  type ModelServerErrors,
  type ModelSourceKind,
} from "./modelServer";

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

/** Muted helper line under a control. */
function Hint({ children }: { children: React.ReactNode }) {
  return <small className="text-xs text-muted">{children}</small>;
}

const STOP_POLICIES: { value: StopPolicy; label: string }[] = [
  { value: "stopOnAppExit", label: "Stop on app exit" },
  { value: "keepAlive", label: "Keep alive" },
  { value: "stopWhenUnused", label: "Stop when unused" },
];

function formatBytes(bytes: number): string {
  if (!Number.isFinite(bytes) || bytes < 0) return "";
  const units = ["B", "KB", "MB", "GB", "TB"] as const;
  let value = bytes;
  let unit = 0;
  while (value >= 1000 && unit < units.length - 1) {
    value /= 1000;
    unit += 1;
  }
  const digits = unit === 0 || value >= 10 ? 0 : 1;
  return `${value.toFixed(digits)} ${units[unit]}`;
}

function deleteArtifactConfirmation(
  server: LocalModelServerConfig,
  artifact: Extract<ModelArtifact, { state: "downloaded" }>,
): string {
  const size =
    artifact.sizeBytes == null ? "" : `Espace libéré : ${formatBytes(artifact.sizeBytes)}.`;
  return [
    "Supprimer le modèle téléchargé ?",
    `Serveur : ${server.name}`,
    "Le serveur local restera configuré, mais IdeA devra retélécharger ce modèle au prochain lancement.",
    size,
  ]
    .filter(Boolean)
    .join("\n");
}

export interface ModelServersPanelProps {
  /** The model-server registry view-model (from `useModelServers`). */
  vm: ModelServersViewModel;
}

export function ModelServersPanel({ vm }: ModelServersPanelProps) {
  // The server being edited, or a fresh draft when "Add" is clicked. `null` ⇒
  // no editor open (list-only).
  const [draft, setDraft] = useState<LocalModelServerConfig | null>(null);

  function startAdd() {
    vm.clearError();
    setDraft(emptyModelServer());
  }

  function startEdit(server: LocalModelServerConfig) {
    vm.clearError();
    setDraft({ ...server });
  }

  async function submit(nextDraft?: LocalModelServerConfig) {
    const submitted = nextDraft ?? draft;
    if (!submitted) return;
    const saved = await vm.save(submitted);
    if (saved) setDraft(null);
  }

  async function confirmDeleteArtifact(
    server: LocalModelServerConfig,
    artifact: Extract<ModelArtifact, { state: "downloaded" }>,
  ) {
    if (!window.confirm(deleteArtifactConfirmation(server, artifact))) return;
    await vm.deleteArtifact(server.id);
  }

  return (
    <Panel
      aria-label="local model servers"
      title={
        <div className="flex items-center gap-2">
          <h3 className="text-sm font-semibold text-content">
            Local model servers
          </h3>
          <Button
            size="sm"
            aria-label="add model server"
            onClick={startAdd}
            className="ml-auto"
          >
            Add server
          </Button>
        </div>
      }
    >
      <div className="flex flex-col gap-3">
        {vm.error && (
          <p role="alert" className="text-sm text-danger">
            {vm.error}
          </p>
        )}
        {vm.notice && (
          <p role="status" className="text-sm text-muted">
            {vm.notice}
          </p>
        )}

        {vm.servers.length === 0 && !draft && (
          <p className="text-xs text-muted">
            No local server declared yet. Add one to auto-manage a `llama-server`,
            or keep using an external endpoint.
          </p>
        )}

        <ul className="flex list-none flex-col gap-2 p-0">
          {vm.servers.map((server) => {
            const artifact = server.artifact;
            const downloaded =
              artifact?.state === "downloaded" ? artifact : undefined;
            const deletingArtifact = vm.deletingArtifactId === server.id;
            const downloadingArtifact = artifact?.state === "downloading";
            return (
              <li
                key={server.id}
                className="flex items-center gap-2 rounded-md border border-border bg-raised p-2"
              >
                <span className="flex min-w-0 flex-col">
                  <strong className="truncate text-sm text-content">
                    {server.name}
                  </strong>
                  <span className="truncate text-xs text-muted">
                    {server.baseURL} · {server.servedModelName}
                    {server.autoStart ? " · auto-start" : ""}
                  </span>
                </span>
                <span className="ml-auto flex items-center gap-1">
                  {downloadingArtifact && (
                    <Button
                      size="sm"
                      variant="secondary"
                      aria-label={`download in progress ${server.name}`}
                      disabled
                    >
                      Téléchargement en cours
                    </Button>
                  )}
                  {downloaded && (
                    <Button
                      size="sm"
                      variant="danger"
                      aria-label={`delete downloaded model ${server.name}`}
                      onClick={() => void confirmDeleteArtifact(server, downloaded)}
                      loading={deletingArtifact}
                      disabled={vm.busy && !deletingArtifact}
                    >
                      {deletingArtifact ? "Suppression..." : "Supprimer le modèle téléchargé"}
                    </Button>
                  )}
                  <Button
                    size="sm"
                    aria-label={`edit ${server.name}`}
                    onClick={() => startEdit(server)}
                    disabled={vm.busy}
                  >
                    Edit
                  </Button>
                  <IconButton
                    size="sm"
                    aria-label={`delete ${server.name}`}
                    onClick={() => void vm.remove(server.id)}
                    disabled={vm.busy}
                  >
                    ×
                  </IconButton>
                </span>
              </li>
            );
          })}
        </ul>

        {draft && (
          <ServerEditor
            draft={draft}
            busy={vm.busy}
            preview={vm.preview}
            onChange={setDraft}
            onCancel={() => {
              vm.clearError();
              setDraft(null);
            }}
            onSubmit={(nextDraft) => void submit(nextDraft)}
          />
        )}
      </div>
    </Panel>
  );
}

/** Segmented control choosing the model source (no jargon in the labels). */
function SourceTabs({
  kind,
  onSelect,
}: {
  kind: ModelSourceKind;
  onSelect: (kind: ModelSourceKind) => void;
}) {
  const tab = (value: ModelSourceKind, label: string) => {
    const active = kind === value;
    return (
      <button
        type="button"
        aria-pressed={active}
        onClick={() => onSelect(value)}
        className={cn(
          "h-8 flex-1 rounded-md px-3 text-xs font-medium transition-colors",
          active
            ? "bg-primary text-white"
            : "bg-raised text-muted hover:text-content",
        )}
      >
        {label}
      </button>
    );
  };
  return (
    <div
      role="group"
      aria-label="source du modèle"
      className="flex gap-1 rounded-md border border-border p-1"
    >
      {tab("huggingFace", "Télécharger depuis Hugging Face")}
      {tab("localPath", "Fichier .gguf local")}
    </div>
  );
}

/** The read-only, backend-built command preview block. */
function CommandPreview({
  preview,
  autoStart,
}: {
  preview: ModelServerCommandPreview | null;
  autoStart: boolean;
}) {
  const [copied, setCopied] = useState(false);
  const title = autoStart ? "Commande générée" : "Commande à lancer manuellement";

  async function copy() {
    if (!preview) return;
    try {
      await navigator.clipboard.writeText(preview.display);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      // Clipboard denied — nothing actionable, the text stays selectable.
    }
  }

  return (
    <section
      aria-label="command preview"
      className="flex flex-col gap-1 rounded-md border border-border bg-raised p-2"
    >
      <div className="flex items-center gap-2">
        <Caption>{title}</Caption>
        <Button
          size="sm"
          variant="ghost"
          aria-label="copier la commande"
          onClick={() => void copy()}
          disabled={!preview}
          className="ml-auto"
        >
          {copied ? "Copié" : "Copier"}
        </Button>
      </div>
      {preview ? (
        <code
          aria-label="preview command line"
          className="whitespace-pre-wrap break-all font-mono text-xs text-content"
        >
          {preview.display}
        </code>
      ) : (
        <span className="text-xs text-muted">
          Renseigne une source de modèle pour voir la commande.
        </span>
      )}
    </section>
  );
}

/** The add/edit wizard for one server draft. */
function ServerEditor({
  draft,
  busy,
  preview,
  onChange,
  onCancel,
  onSubmit,
}: {
  draft: LocalModelServerConfig;
  busy: boolean;
  preview: (
    config: LocalModelServerConfig,
  ) => Promise<ModelServerCommandPreview | null>;
  onChange: (next: LocalModelServerConfig) => void;
  onCancel: () => void;
  onSubmit: (nextDraft?: LocalModelServerConfig) => void;
}) {
  const errors: ModelServerErrors = validateModelServer(draft);
  const valid = Object.keys(errors).length === 0;
  const patch = (next: Partial<LocalModelServerConfig>) =>
    onChange({ ...draft, ...next });
  const canonicalArgsText = draft.args.join(" ");
  const [argsText, setArgsText] = useState(canonicalArgsText);

  useEffect(() => {
    setArgsText(canonicalArgsText);
  }, [canonicalArgsText, draft.id]);

  function draftWithCommittedArgs(): LocalModelServerConfig {
    return { ...draft, args: parseArgs(argsText) };
  }

  function commitArgs() {
    const next = draftWithCommittedArgs();
    onChange(next);
  }

  // Sticky source tab: keep the chosen kind even when the field is momentarily
  // blank (an empty field clears `modelSource`, which must not snap the tab).
  const [sourceKind, setSourceKind] = useState<ModelSourceKind>(
    draft.modelSource?.type ?? "huggingFace",
  );

  const hfRepo =
    draft.modelSource?.type === "huggingFace" ? draft.modelSource.repo : "";
  const localPath =
    draft.modelSource?.type === "localPath" ? draft.modelSource.path : "";

  // Apply a new source and pre-fill the served name if the user hasn't set one.
  function setSource(source: ModelSource | undefined) {
    const next: Partial<LocalModelServerConfig> = { modelSource: source };
    if (draft.servedModelName.trim().length === 0) {
      const derived = deriveServedModelName(source);
      if (derived.length > 0) next.servedModelName = derived;
    }
    patch(next);
  }

  function selectKind(kind: ModelSourceKind) {
    if (kind === sourceKind) return;
    setSourceKind(kind);
    // Switching tabs starts the new source empty (⇒ cleared) rather than
    // carrying the previous value into an incompatible field.
    patch({ modelSource: undefined });
  }

  // Debounced backend preview. The backend is the sole authority on the argv.
  const [cmd, setCmd] = useState<ModelServerCommandPreview | null>(null);
  const draftKey = JSON.stringify({
    modelSource: draft.modelSource,
    port: draft.port,
    host: draft.host,
    gpuLayers: draft.gpuLayers,
    contextSize: draft.contextSize,
    jinja: draft.jinja,
    binaryPath: draft.binaryPath,
    args: draft.args,
  });
  const latest = useRef(draft);
  latest.current = draft;
  useEffect(() => {
    if (!draft.modelSource) {
      setCmd(null);
      return;
    }
    let cancelled = false;
    const handle = setTimeout(() => {
      void preview(latest.current).then((result) => {
        if (!cancelled) setCmd(result);
      });
    }, 250);
    return () => {
      cancelled = true;
      clearTimeout(handle);
    };
    // `draftKey` captures the argv-affecting fields; `preview` is stable.
  }, [draftKey, preview]); // eslint-disable-line react-hooks/exhaustive-deps

  const reservedHits = reservedFlagsIn(parseArgs(argsText));

  return (
    <fieldset
      aria-label="model server editor"
      className="flex flex-col gap-3 rounded-md border border-primary/40 p-3"
    >
      <legend className="px-1 text-xs font-medium text-muted">
        {draft.name.trim().length > 0 ? draft.name : "New server"}
      </legend>

      <label className="flex flex-col gap-1">
        <Caption>Nom</Caption>
        <Input
          aria-label="server name"
          value={draft.name}
          invalid={Boolean(errors.name)}
          onChange={(e) => patch({ name: e.target.value })}
        />
        {errors.name && <small className="text-xs text-danger">{errors.name}</small>}
      </label>

      {/* --- Source du modèle --------------------------------------------- */}
      <div className="flex flex-col gap-1">
        <Caption>Source du modèle</Caption>
        <SourceTabs kind={sourceKind} onSelect={selectKind} />
      </div>

      {sourceKind === "huggingFace" ? (
        <label className="flex flex-col gap-1">
          <Caption>Modèle Hugging Face</Caption>
          <Input
            aria-label="hugging face model"
            value={hfRepo}
            placeholder="unsloth/Qwen3.5-9B-GGUF:Q4_K_M"
            invalid={Boolean(errors.modelSource)}
            onChange={(e) => {
              const v = e.target.value;
              setSource(
                v.trim().length === 0
                  ? undefined
                  : { type: "huggingFace", repo: v },
              );
            }}
          />
          <Hint>llama.cpp téléchargera/cache le fichier automatiquement.</Hint>
          {errors.modelSource && (
            <small className="text-xs text-danger">{errors.modelSource}</small>
          )}
        </label>
      ) : (
        <label className="flex flex-col gap-1">
          <Caption>Fichier .gguf</Caption>
          <Input
            aria-label="local gguf file"
            value={localPath}
            placeholder="/home/me/models/qwen.gguf"
            invalid={Boolean(errors.modelSource)}
            onChange={(e) => {
              const v = e.target.value;
              setSource(
                v.trim().length === 0
                  ? undefined
                  : { type: "localPath", path: v },
              );
            }}
          />
          <Hint>Chemin absolu vers un .gguf déjà présent.</Hint>
          {errors.modelSource && (
            <small className="text-xs text-danger">{errors.modelSource}</small>
          )}
        </label>
      )}

      {/* --- Nom du modèle dans IdeA -------------------------------------- */}
      <label className="flex flex-col gap-1">
        <Caption>Nom du modèle dans IdeA</Caption>
        <Input
          aria-label="server served model name"
          value={draft.servedModelName}
          placeholder="qwen3-coder-30b"
          invalid={Boolean(errors.servedModelName)}
          onChange={(e) => patch({ servedModelName: e.target.value })}
        />
        <Hint>Nom envoyé aux clients OpenAI-compatible.</Hint>
        {errors.servedModelName && (
          <small className="text-xs text-danger">{errors.servedModelName}</small>
        )}
      </label>

      {/* --- Serveur ------------------------------------------------------ */}
      <div className="flex flex-wrap gap-2">
        <label className="flex min-w-[12rem] flex-1 flex-col gap-1">
          <Caption>Commande serveur</Caption>
          <Input
            aria-label="server binary path"
            value={draft.binaryPath ?? ""}
            placeholder="llama-server"
            onChange={(e) => {
              const v = e.target.value;
              patch({ binaryPath: v.length === 0 ? undefined : v });
            }}
          />
          <Hint>Laisse vide pour résoudre `llama-server` via le PATH.</Hint>
        </label>

        <label className="flex min-w-[6rem] flex-col gap-1">
          <Caption>Port</Caption>
          <Input
            aria-label="server port"
            value={Number.isNaN(draft.port) ? "" : draft.port.toString()}
            inputMode="numeric"
            invalid={Boolean(errors.port)}
            onChange={(e) => patch({ port: Number.parseInt(e.target.value, 10) })}
          />
          {errors.port && <small className="text-xs text-danger">{errors.port}</small>}
        </label>
      </div>

      <label className="flex flex-col gap-1">
        <Caption>Base URL (OpenAI-compatible)</Caption>
        <Input
          aria-label="server base url"
          value={draft.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 items-start gap-2">
        <input
          type="checkbox"
          aria-label="server auto start"
          checked={draft.autoStart}
          disabled={!draft.modelSource}
          onChange={(e) => patch({ autoStart: e.target.checked })}
          className="mt-1 accent-primary disabled:opacity-50"
        />
        <span className="flex flex-col">
          <Caption>Démarrer automatiquement</Caption>
          {!draft.modelSource && (
            <Hint>Renseigne d'abord une source de modèle pour l'activer.</Hint>
          )}
        </span>
      </label>

      {/* --- Réglages llama.cpp (avancé) --------------------------------- */}
      <details className="rounded-md border border-border p-2">
        <summary className="cursor-pointer text-xs font-medium text-muted">
          Réglages llama.cpp
        </summary>
        <div className="mt-2 flex flex-col gap-2">
          <div className="flex flex-wrap gap-2">
            <label className="flex min-w-[8rem] flex-1 flex-col gap-1">
              <Caption>
                Couches GPU <span className="text-muted">(-ngl)</span>
              </Caption>
              <Input
                aria-label="gpu layers"
                value={draft.gpuLayers === undefined ? "" : String(draft.gpuLayers)}
                inputMode="numeric"
                placeholder="99"
                onChange={(e) => {
                  const v = e.target.value.trim();
                  patch({
                    gpuLayers: v.length === 0 ? undefined : Number.parseInt(v, 10),
                  });
                }}
              />
              <Hint>0 = CPU seul.</Hint>
            </label>

            <label className="flex min-w-[8rem] flex-1 flex-col gap-1">
              <Caption>
                Taille de contexte <span className="text-muted">(-c)</span>
              </Caption>
              <Input
                aria-label="context size"
                value={
                  draft.contextSize === undefined ? "" : String(draft.contextSize)
                }
                inputMode="numeric"
                placeholder="16384"
                onChange={(e) => {
                  const v = e.target.value.trim();
                  patch({
                    contextSize:
                      v.length === 0 ? undefined : Number.parseInt(v, 10),
                  });
                }}
              />
            </label>
          </div>

          <label className="flex flex-col gap-1">
            <Caption>
              Adresse d'écoute <span className="text-muted">(--host)</span>
            </Caption>
            <Input
              aria-label="listen host"
              value={draft.host}
              placeholder="127.0.0.1"
              invalid={Boolean(errors.host)}
              onChange={(e) => patch({ host: e.target.value })}
            />
            {errors.host && (
              <small className="text-xs text-danger">{errors.host}</small>
            )}
          </label>

          <label className="flex items-center gap-2">
            <input
              type="checkbox"
              aria-label="jinja template"
              checked={draft.jinja}
              onChange={(e) => patch({ jinja: e.target.checked })}
              className="accent-primary"
            />
            <Caption>
              Template chat Jinja <span className="text-muted">(--jinja)</span>
            </Caption>
          </label>

          <label className="flex flex-col gap-1">
            <Caption>Arguments supplémentaires</Caption>
            <Input
              aria-label="extra arguments"
              value={argsText}
              placeholder="--flash-attn --parallel 2"
              onChange={(e) => setArgsText(e.target.value)}
              onKeyDown={(e) => e.stopPropagation()}
              onBlur={commitArgs}
            />
            {reservedHits.length > 0 && (
              <small className="text-xs text-warning" role="status">
                {reservedHits.join(", ")} : cette option est déjà pilotée par un
                champ ci-dessus.
              </small>
            )}
          </label>

          <label className="flex items-center gap-2">
            <Caption>Stop policy</Caption>
            <select
              aria-label="server stop policy"
              value={draft.stopPolicy}
              onChange={(e) => patch({ stopPolicy: e.target.value as StopPolicy })}
              className={cn(
                "h-8 rounded-md bg-raised px-2 text-xs text-content",
                "border border-border outline-none focus:border-primary",
              )}
            >
              {STOP_POLICIES.map((p) => (
                <option key={p.value} value={p.value}>
                  {p.label}
                </option>
              ))}
            </select>
          </label>
        </div>
      </details>

      <CommandPreview preview={cmd} autoStart={draft.autoStart} />

      <div className="flex gap-2">
        <Button
          variant="primary"
          size="sm"
          aria-label="save model server"
          onClick={() => onSubmit(draftWithCommittedArgs())}
          disabled={busy || !valid}
        >
          Save server
        </Button>
        <Button
          variant="ghost"
          size="sm"
          aria-label="cancel model server"
          onClick={onCancel}
          disabled={busy}
        >
          Cancel
        </Button>
      </div>
    </fieldset>
  );
}
