/**
 * Settings -> AI Profiles. This is the durable CRUD surface for named runtime
 * profiles; first-run stays a small default-profile bootstrap.
 */

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

import type {
  AgentProfile,
  GatewayError,
  ModelCompatibility,
  ProfileModelCatalog,
  ProfileModelCatalogEntry,
} from "@/domain";
import { useGateways } from "@/app/di";
import { Button, Input, Panel, cn } from "@/shared";
import {
  ModelServersPanel,
  useModelServers,
} from "@/features/model-servers";
import {
  OpenCodeModeFields,
  useOpenCodeProviderCatalog,
} from "./OpenCodeModeFields";
import { validateProfile } from "./profile";

type ProfileTab = "codex" | "claude" | "openCode";
type ModelTab = "codex" | "claude";
type CatalogState = Record<ModelTab, ProfileModelCatalog & { unavailable: boolean }>;

const TABS: Array<{ id: ProfileTab; label: string }> = [
  { id: "codex", label: "Codex" },
  { id: "claude", label: "Claude" },
  { id: "openCode", label: "OpenCode-local" },
];

const EMPTY_MODEL_CATALOG: ProfileModelCatalog & { unavailable: boolean } = {
  models: [],
  cliVersion: null,
  warnings: [],
  unavailable: false,
};

const EMPTY_CATALOGUE: CatalogState = {
  codex: EMPTY_MODEL_CATALOG,
  claude: EMPTY_MODEL_CATALOG,
};

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

function tabFor(profile: AgentProfile): ProfileTab | null {
  if (profile.structuredAdapter === "codex") return "codex";
  if (profile.structuredAdapter === "claude") return "claude";
  if (
    profile.structuredAdapter === "openCode" &&
    (profile.opencode || profile.opencodeProvider)
  ) {
    return "openCode";
  }
  return null;
}

function modelOf(profile: AgentProfile): string {
  if (profile.structuredAdapter === "openCode") {
    return profile.opencode?.model ?? profile.opencodeProvider?.model ?? "";
  }
  return profile.model ?? "";
}

function withModel(profile: AgentProfile, model: string): AgentProfile {
  const nextModel = model.trim() || undefined;
  if (profile.structuredAdapter === "openCode" && profile.opencode) {
    return {
      ...profile,
      opencode: { ...profile.opencode, model: model.trim() },
    };
  }
  return { ...profile, model: nextModel };
}

function optionLabel(entry: ProfileModelCatalogEntry): string {
  return entry.recommended
    ? `${entry.displayName} (${entry.modelId}, recommande)`
    : `${entry.displayName} (${entry.modelId})`;
}

function compatibilityLabel(compatibility: ModelCompatibility): string {
  if (compatibility === "compatible") return "Compatible";
  if (compatibility === "likelyTooRecent") return "Probablement trop récent";
  return "Inconnu";
}

function engineLabel(tab: ModelTab): string {
  return tab === "codex" ? "Codex CLI" : "Claude CLI";
}

function attentionBadge(
  compatibility: ModelCompatibility,
  cliVersion: string | null,
): string | null {
  if (!cliVersion) return "CLI non détecté";
  if (compatibility === "compatible") return null;
  return compatibilityLabel(compatibility);
}

function catalogEntryFor(
  model: string,
  models: ProfileModelCatalogEntry[],
): ProfileModelCatalogEntry | null {
  const normalized = model.trim().toLowerCase();
  if (!normalized) return null;
  return models.find((entry) => entry.modelId.toLowerCase() === normalized) ?? null;
}

function compatibilityFor(
  model: string,
  models: ProfileModelCatalogEntry[],
): ModelCompatibility {
  return catalogEntryFor(model, models)?.compatibility ?? "unknown";
}

function modelHelp(
  tab: ModelTab,
  model: string,
  catalog: ProfileModelCatalog & { unavailable: boolean },
): string {
  const cli = engineLabel(tab);
  if (catalog.unavailable) {
    return "Catalogue provider indisponible ; vous pouvez saisir le modèle manuellement.";
  }
  if (!catalog.cliVersion) {
    return `Version du ${cli} non détectée ; IdeA ne peut pas estimer la compatibilité.`;
  }
  const compatibility = compatibilityFor(model, catalog.models);
  if (compatibility === "compatible") {
    return `Compatible avec ${cli} ${catalog.cliVersion} d'après le catalogue local IdeA.`;
  }
  if (compatibility === "likelyTooRecent") {
    return `Probablement trop récent pour ${cli} ${catalog.cliVersion} ; mettez à jour le CLI si le lancement échoue.`;
  }
  return `Compatibilité non connue pour ${cli} ${catalog.cliVersion} ; la saisie reste autorisée.`;
}

function saveWarningText(tab: ModelTab, cliVersion: string | null): string {
  const cli = engineLabel(tab);
  const version = cliVersion ? ` ${cliVersion}` : "";
  return `Ce modèle semble plus récent que votre ${cli}${version}. Le profil peut être enregistré, mais l'agent pourrait échouer au lancement tant que le CLI n'est pas mis à jour.`;
}

function matchingSuggestions(
  model: string,
  models: ProfileModelCatalogEntry[],
): ProfileModelCatalogEntry[] {
  const q = model.trim().toLowerCase();
  const filtered = q
    ? models.filter((entry) =>
        [entry.modelId, entry.displayName, ...entry.aliases]
          .join(" ")
          .toLowerCase()
          .includes(q),
      )
    : models;
  return filtered.slice(0, 5);
}

function ModelField({
  profileId,
  profileName,
  tab,
  model,
  catalog,
  onChange,
}: {
  profileId: string;
  profileName: string;
  tab: ModelTab;
  model: string;
  catalog: ProfileModelCatalog & { unavailable: boolean };
  onChange: (model: string) => void;
}) {
  const [focused, setFocused] = useState(false);
  const inputId = `profile-model-${tab}-${profileId}`;
  const compatibility = compatibilityFor(model, catalog.models);
  const badge = attentionBadge(compatibility, catalog.cliVersion);
  const suggestions = matchingSuggestions(model, catalog.models);

  return (
    <div className="flex min-w-0 flex-col gap-1">
      <label
        htmlFor={inputId}
        className="flex items-center gap-2 text-xs font-medium text-muted"
      >
        Modèle
        <span
          className="text-faint"
          title="IdeA estime la compatibilité à partir du catalogue maintenu dans l'application et de la version du CLI détectée localement. Le provider peut accepter ou refuser le modèle différemment au moment du lancement."
        >
          compatibilité estimée
        </span>
        {badge && (
          <span
            className={cn(
              "rounded-full px-2 py-0.5 text-[11px] font-medium",
              compatibility === "likelyTooRecent"
                ? "bg-warning/15 text-warning"
                : "bg-raised text-muted",
            )}
          >
            {badge}
          </span>
        )}
      </label>
      <Input
        id={inputId}
        aria-label={`modele du profil ${profileName}`}
        placeholder={
          catalog.models.length > 0
            ? "Choisir ou saisir un modèle"
            : "Saisir un modèle"
        }
        value={model}
        onFocus={() => setFocused(true)}
        onBlur={() => window.setTimeout(() => setFocused(false), 120)}
        onChange={(e) => onChange(e.target.value)}
      />
      <small className="text-xs text-faint">{modelHelp(tab, model, catalog)}</small>
      {focused && suggestions.length > 0 && (
        <div
          role="listbox"
          aria-label={`suggestions de modèles ${profileName}`}
          className="mt-1 max-h-36 overflow-auto rounded-md border border-border bg-raised p-1"
        >
          {suggestions.map((entry) => (
            <button
              key={entry.modelId}
              type="button"
              role="option"
              className="flex w-full items-center justify-between gap-3 rounded px-2 py-1.5 text-left text-xs hover:bg-surface"
              onMouseDown={(e) => {
                e.preventDefault();
                onChange(entry.modelId);
                setFocused(false);
              }}
            >
              <span className="min-w-0 truncate">{optionLabel(entry)}</span>
              <span
                className={cn(
                  "shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium",
                  entry.compatibility === "compatible"
                    ? "bg-primary/10 text-primary"
                    : entry.compatibility === "likelyTooRecent"
                      ? "bg-warning/15 text-warning"
                      : "bg-surface text-muted",
                )}
              >
                {compatibilityLabel(entry.compatibility)}
              </span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

export function ProfilesSettings() {
  const { profile } = useGateways();
  const modelServers = useModelServers();
  const providerCatalog = useOpenCodeProviderCatalog();
  const [profiles, setProfiles] = useState<AgentProfile[]>([]);
  const [references, setReferences] = useState<AgentProfile[]>([]);
  const [catalogue, setCatalogue] = useState(EMPTY_CATALOGUE);
  const [activeTab, setActiveTab] = useState<ProfileTab>("codex");
  const [drafts, setDrafts] = useState<Record<string, AgentProfile>>({});
  const [error, setError] = useState<string | null>(null);
  const [catalogueWarning, setCatalogueWarning] = useState<string | null>(null);
  const [saveWarnings, setSaveWarnings] = useState<Record<string, string>>({});
  const [busy, setBusy] = useState(false);

  const refresh = useCallback(async () => {
    setError(null);
    try {
      const [saved, refs] = await Promise.all([
        profile.listProfiles(),
        profile.referenceProfiles(),
      ]);
      setProfiles(saved);
      setReferences(refs);
      setDrafts(Object.fromEntries(saved.map((p) => [p.id, p])));
    } catch (e) {
      setError(describe(e));
    }
  }, [profile]);

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

  useEffect(() => {
    let cancelled = false;
    async function loadCatalogue() {
      setCatalogueWarning(null);
      const [codex, claude] = await Promise.allSettled([
        profile.listCodexModels(),
        profile.listClaudeModels(),
      ]);
      if (cancelled) return;
      setCatalogue({
        codex:
          codex.status === "fulfilled"
            ? { ...codex.value, unavailable: false }
            : { ...EMPTY_MODEL_CATALOG, unavailable: true },
        claude:
          claude.status === "fulfilled"
            ? { ...claude.value, unavailable: false }
            : { ...EMPTY_MODEL_CATALOG, unavailable: true },
      });
      if (codex.status === "rejected" || claude.status === "rejected") {
        setCatalogueWarning(
          "Catalogue provider indisponible ; vous pouvez saisir le modèle manuellement.",
        );
      }
    }
    void loadCatalogue();
    return () => {
      cancelled = true;
    };
  }, [profile]);

  const visibleProfiles = useMemo(
    () => profiles.filter((p) => tabFor(p) === activeTab),
    [profiles, activeTab],
  );

  const seed = useMemo(
    () => references.find((p) => tabFor(p) === activeTab) ?? null,
    [references, activeTab],
  );

  function updateDraft(id: string, updater: (profile: AgentProfile) => AgentProfile) {
    setDrafts((prev) => {
      const current = prev[id] ?? profiles.find((p) => p.id === id);
      if (!current) return prev;
      return { ...prev, [id]: updater(current) };
    });
  }

  async function createFromSeed() {
    if (!seed) return;
    setBusy(true);
    setError(null);
    try {
      if (activeTab === "openCode") {
        const created = await profile.cloneOpenCodeProfileFromSeed({
          name: `${seed.name} copy`,
          opencode: seed.opencode,
        });
        await profile.saveProfile(created);
        await refresh();
        return;
      }

      const models =
        activeTab === "codex" || activeTab === "claude"
          ? catalogue[activeTab].models
          : [];
      const recommended = models.find((m) => m.recommended)?.modelId;
      await profile.cloneProfileFromSeed({
        seedProfileId: seed.id,
        name: `${seed.name} copy`,
        model: recommended,
      });
      await refresh();
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
    }
  }

  async function save(id: string) {
    const draft = drafts[id];
    if (!draft) return;
    setBusy(true);
    setError(null);
    setSaveWarnings((prev) => {
      const { [id]: _ignored, ...rest } = prev;
      return rest;
    });
    try {
      const tab = tabFor(draft);
      const warning =
        tab === "codex" || tab === "claude"
          ? compatibilityFor(modelOf(draft), catalogue[tab].models) ===
            "likelyTooRecent"
            ? saveWarningText(tab, catalogue[tab].cliVersion)
            : null
          : null;
      await profile.saveProfile(draft);
      await refresh();
      if (warning) {
        setSaveWarnings((prev) => ({ ...prev, [id]: warning }));
      }
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
    }
  }

  async function duplicate(source: AgentProfile) {
    setBusy(true);
    setError(null);
    try {
      if (source.structuredAdapter === "openCode") {
        const created = await profile.cloneOpenCodeProfileFromSeed({
          name: `${source.name} copy`,
          opencode: source.opencode,
        });
        await profile.saveProfile(created);
        await refresh();
        return;
      }

      await profile.cloneProfileFromSeed({
        seedProfileId: source.id,
        name: `${source.name} copy`,
        model:
          source.structuredAdapter === "codex" ||
          source.structuredAdapter === "claude"
            ? modelOf(source) || undefined
            : undefined,
      });
      await refresh();
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
    }
  }

  async function del(source: AgentProfile) {
    setBusy(true);
    setError(null);
    try {
      await profile.deleteProfile(source.id);
      await refresh();
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
    }
  }

  return (
    <Panel
      aria-label="ai profiles settings"
      title="Profils IA"
      actions={
        <Button size="sm" onClick={() => void createFromSeed()} disabled={!seed || busy}>
          Creer un profil
        </Button>
      }
    >
      <div className="flex flex-col gap-4">
        <div
          role="tablist"
          aria-label="types de profils IA"
          className="inline-flex w-fit rounded-md border border-border bg-raised p-0.5"
        >
          {TABS.map((tab) => (
            <button
              key={tab.id}
              type="button"
              role="tab"
              aria-selected={activeTab === tab.id}
              onClick={() => setActiveTab(tab.id)}
              className={cn(
                "h-7 rounded px-3 text-xs font-medium transition-colors",
                activeTab === tab.id
                  ? "bg-surface text-content shadow-sm"
                  : "text-muted hover:text-content",
              )}
            >
              {tab.label}
            </button>
          ))}
        </div>

        {error && (
          <p role="alert" className="text-sm text-danger">
            {error}
          </p>
        )}
        {catalogueWarning && (
          <p className="text-xs text-muted">{catalogueWarning}</p>
        )}

        {(activeTab === "codex" || activeTab === "claude") &&
          catalogue[activeTab].warnings.map((warning) => (
            <p key={warning} className="text-xs text-warning">
              {warning}
            </p>
          ))}

        {activeTab === "openCode" && <ModelServersPanel vm={modelServers} />}

        {visibleProfiles.length === 0 ? (
          <p className="text-sm text-muted">
            Aucun profil {TABS.find((tab) => tab.id === activeTab)?.label} configure.
          </p>
        ) : (
          <ul className="flex flex-col gap-3">
            {visibleProfiles.map((saved) => {
              const draft = drafts[saved.id] ?? saved;
              const model = modelOf(draft);
              const dirty = JSON.stringify(draft) !== JSON.stringify(saved);
              const errors = validateProfile(draft);
              return (
                <li
                  key={saved.id}
                  className="rounded-md border border-border bg-surface p-3"
                >
                  <div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
                    <label className="flex min-w-0 flex-col gap-1">
                      <span className="text-xs font-medium text-muted">Nom</span>
                      <Input
                        aria-label={`nom du profil ${saved.name}`}
                        value={draft.name}
                        onChange={(e) =>
                          updateDraft(saved.id, (p) => ({
                            ...p,
                            name: e.target.value,
                          }))
                        }
                      />
                    </label>

                    {activeTab === "codex" || activeTab === "claude" ? (
                      <ModelField
                        profileId={saved.id}
                        profileName={saved.name}
                        tab={activeTab}
                        model={model}
                        catalog={catalogue[activeTab]}
                        onChange={(value) =>
                          updateDraft(saved.id, (p) => withModel(p, value))
                        }
                      />
                    ) : null}
                  </div>

                  {draft.structuredAdapter === "openCode" && (
                    <OpenCodeModeFields
                      profile={draft}
                      errors={errors}
                      servers={modelServers.servers}
                      providerCatalog={providerCatalog}
                      onChange={(next) =>
                        updateDraft(saved.id, () => next)
                      }
                    />
                  )}

                  {saveWarnings[saved.id] && (
                    <p className="mt-2 rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
                      {saveWarnings[saved.id]}
                    </p>
                  )}

                  <div className="mt-2 flex flex-wrap items-center justify-between gap-2">
                    <code className="min-w-0 truncate text-xs text-muted">
                      {draft.command}
                      {model ? ` · ${model}` : ""}
                    </code>
                    <span className="flex flex-wrap gap-1.5">
                      <Button
                        size="sm"
                        variant="primary"
                        disabled={!dirty || busy || draft.name.trim() === ""}
                        onClick={() => void save(saved.id)}
                      >
                        Enregistrer
                      </Button>
                      <Button
                        size="sm"
                        variant="secondary"
                        disabled={busy}
                        onClick={() => void duplicate(saved)}
                      >
                        Dupliquer
                      </Button>
                      <Button
                        size="sm"
                        variant="ghost"
                        disabled={busy}
                        className="text-danger hover:text-danger"
                        aria-label={`supprimer ${saved.name}`}
                        onClick={() => void del(saved)}
                      >
                        Supprimer
                      </Button>
                    </span>
                  </div>
                </li>
              );
            })}
          </ul>
        )}
      </div>
    </Panel>
  );
}
