"use client";

import { useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
import type { ChartType } from "@enode/core/api/dtos/charts";
import { displayName } from "@enode/core/text-content/display-name";
import { useTranslation, type TranslateFn } from "@enode/core/translations/hooks";
import { isEffectivelyTrainable } from "@enode/core/users/trainable";
import { today } from "@enode/ui/calendar-shared";
import { ConfirmDialog } from "@enode/ui/confirm-dialog";
import { DrawerView } from "@enode/ui/drawer-stack";
import { ErrorAlert } from "@enode/ui/feedback/inline-alert";
import { StalenessHint } from "@enode/ui/feedback/staleness-hint";
import { TextArea } from "@enode/ui/text-area";
import { TextField } from "@enode/ui/text-field";
import { useWizard, WizardDrawerView, type WizardStep } from "@enode/ui/wizard";
import { useChartDemo, useChartTagOf, useChartUsers } from "../../../../components/charts/chart-demo";
import { FlatRecipe, ProfileRecipe, useRecipeNames } from "./builder-recipe";
import {
  exercisesOptionalFor,
  flatChartRequest,
  groupHasAthletes,
  groupHasSelection,
  profileChartRequest,
  type ChartRequest,
  type FlatDraft,
  type GroupDraft,
  type ProfileDraft,
} from "./chart-draft";
import { flatGaps, profileGaps } from "./chart-gaps";
import {
  axesSummaryText,
  FlatPreviewCard,
  profileAxesHint,
  ProfilePreviewCard,
  useProfileGroupText,
} from "./chart-preview-card";
import { saveFlatChart, saveProfileChart, type ChartSaved } from "./chart-save";
import { resolveTemplate, type ChartTemplate, type TemplateShape } from "./chart-templates";
import {
  BundleStep,
  FixedListTagHint,
  LiftsStep,
  OneAthleteStep,
  scopeMembers,
  WhoStep,
} from "./guided-steps";
import { liftActivity, liftCoverage } from "./lift-activity";
import type { ScopeIDs } from "./recent-scope";
import { BuildYourselfButton, QuestionList } from "./template-picker";
import { longerPreset, upToTodayFrom, type Timeframe } from "./timeframe";
import { presetLabel, TimeframePresets } from "./timeframe-field";
import { useLiftActivity } from "./use-lift-activity";
import type { UseMetricOptions } from "./use-metric-options";

// The guided path through chart creation, on the shared wizard (useWizard +
// WizardDrawerView — the frosted header with step breadcrumbs and floating
// Back / Next of the other wizard drawers). Step one is the question; the
// question fixes the chart type, metric(s), timeframe, title and note, and the
// next steps ask only what it leaves open — whose sessions, which lifts (and,
// for a Profile, against whom and on which metrics) — then a check with the
// coach's own data before saving. Every step writes the same drafts the full
// builder edits, so "Adjust everything" opens the builder with all of it in place.
//
// Inside the Insights demo the steps offer the demo's squad, and Save puts the
// chart on the demo's board (chart-demo.tsx). The ways into the full builder
// are left out there: the demo walks the guided path only.

/** A Ranking compares athletes on one lift; fewer than this many is too few to compare. */
const RANKING_MIN_ATHLETES = 3;

export function GuidedFlow({
  applied,
  onApply,
  templates,
  templatesLoading,
  hasWhoOrWhich,
  onBuildYourself,
  alwaysFull,
  onAlwaysFullChange,
  flatDraft,
  setFlatDraft,
  profileDraft,
  setProfileDraft,
  metricOptions,
  onAdjust,
  onClose,
  onSaved,
}: {
  /** The question in use, null until one is picked. */
  applied: ChartTemplate | null;
  /** Seeds the drafts from a question (synchronously). */
  onApply: (template: ChartTemplate) => void;
  /** The questions on offer (already narrowed to those whose metrics resolve). */
  templates: readonly ChartTemplate[];
  templatesLoading: boolean;
  /** Athletes or exercises are already chosen — switching question asks first. */
  hasWhoOrWhich: boolean;
  /** "Build it yourself": the full builder without a question. */
  onBuildYourself: () => void;
  alwaysFull: boolean;
  onAlwaysFullChange: (next: boolean) => void;
  flatDraft: FlatDraft;
  setFlatDraft: Dispatch<SetStateAction<FlatDraft>>;
  profileDraft: ProfileDraft;
  setProfileDraft: Dispatch<SetStateAction<ProfileDraft>>;
  metricOptions: UseMetricOptions;
  /** Opens the full builder with everything chosen so far. */
  onAdjust: () => void;
  /** Guarded close (asks before discarding choices). */
  onClose: () => void;
  /** A successful save — the caller reconciles the board and closes the drawer. */
  onSaved: (saved: ChartSaved) => void;
}) {
  const { t } = useTranslation();
  const type: ChartType = applied?.type ?? "trend";
  const isProfile = type === "profile";
  // The question's shape — which side is many, which is fixed to one — decides
  // the steps (M1). A question without one (none today) falls back to many
  // athletes, with one lift for a Ranking.
  const shape: TemplateShape = applied?.shape ?? {
    athletes: "many",
    lifts: type === "ranking" ? { kind: "one" } : { kind: "many" },
  };
  const oneAthlete = !isProfile && shape.athletes === "one";
  const singleLift = !isProfile && shape.lifts.kind === "one";
  const maxLifts = !isProfile && shape.lifts.kind === "many" ? shape.lifts.max : undefined;
  const prepickLifts = !isProfile && shape.lifts.kind === "many" ? (shape.lifts.prepick ?? 0) : 0;
  // Save stays clickable; an attempt with something missing pins the reason.
  const [showValidation, setShowValidation] = useState(false);
  // A different question picked while athletes/exercises are chosen waits for
  // confirmation — they stay, the rest is replaced.
  const [confirmSwitch, setConfirmSwitch] = useState<ChartTemplate | null>(null);

  const demo = useChartDemo();
  const users = useChartUsers();
  const tagOf = useChartTagOf();
  const roster = (users ?? []).filter(isEffectivelyTrainable);
  const { options, loading, canonicalize } = metricOptions;
  const levelOf = (id: string) => options.find((o) => o.displayBaseID === id)?.level;

  // ── What each step reads and writes ──
  const flatWho: ScopeIDs = { ids: flatDraft.userIDs, tagIDs: flatDraft.userTagIDs };
  const groupWho = (group: GroupDraft): ScopeIDs => ({ ids: group.userIDs, tagIDs: group.userTagIDs });
  const setGroupWho = (key: "profile" | "reference", scope: ScopeIDs) =>
    setProfileDraft((d) => ({ ...d, [key]: { ...d[key], userIDs: scope.ids, userTagIDs: scope.tagIDs } }));

  const flatType = type === "profile" ? null : type;
  const displayBaseID = canonicalize(flatDraft.displayBaseID) ?? "";
  const displayBaseID2 = canonicalize(flatDraft.displayBaseID2) ?? "";
  const liftsOptional =
    flatType != null &&
    exercisesOptionalFor(
      flatType,
      flatType === "correlation" ? [levelOf(displayBaseID), levelOf(displayBaseID2)] : [levelOf(displayBaseID)],
    );
  const profileAxes = profileDraft.displayBaseIDs
    .map((id) => canonicalize(id))
    .filter((id): id is string => id !== null);
  const anyExerciseAxis = profileAxes.some((id) => levelOf(id) !== "user");

  const liftAthletes = isProfile
    ? [
        ...new Set([
          ...scopeMembers(groupWho(profileDraft.profile), roster),
          ...scopeMembers(groupWho(profileDraft.reference), roster),
        ]),
      ]
    : scopeMembers(flatWho, roster);
  const liftIDs = isProfile ? profileDraft.profile.exerciseDefinitionIDs : flatDraft.exerciseDefinitionIDs;
  const timeframe = isProfile ? profileDraft.timeframe : flatDraft.timeframe;
  const setTimeframe = (next: Timeframe) =>
    isProfile
      ? setProfileDraft((d) => ({ ...d, timeframe: next }))
      : setFlatDraft((d) => ({ ...d, timeframe: next }));
  // A Profile compares both groups on the same lifts by default; different
  // lifts per group is the full builder's job.
  const setLifts = (ids: string[]) =>
    isProfile
      ? setProfileDraft((d) => ({
          ...d,
          profile: { ...d.profile, exerciseDefinitionIDs: ids },
          reference: { ...d.reference, exerciseDefinitionIDs: ids },
        }))
      : setFlatDraft((d) => ({ ...d, exerciseDefinitionIDs: ids }));

  const bundleIDs = applied
    ? resolveTemplate(applied, options, today()).displayBaseIDs.filter((id): id is string => id !== null)
    : [];

  // The name a scope goes by on the chart: one tag's name, one athlete's name,
  // or the whole squad.
  function scopeName(scope: ScopeIDs): string | null {
    if (scope.tagIDs.length === 0 && scope.ids.length === 1) {
      const user = roster.find((u) => u.id === scope.ids[0]);
      return user?.name ?? user?.email ?? null;
    }
    if (scope.ids.length === 0 && scope.tagIDs.length === 1) {
      const name = displayName(tagOf(scope.tagIDs[0])).trim();
      return name || null;
    }
    if (scope.tagIDs.length === 0 && roster.length > 0 && scope.ids.length === roster.length) {
      const picked = new Set(scope.ids);
      if (roster.every((user) => picked.has(user.id))) return t("Whole squad");
    }
    return null;
  }

  // Before the check: name the chart after the question and the group, and a
  // Profile's shapes after the groups, unless the coach already renamed them.
  function nameAfterGroups() {
    if (!applied) return;
    if (isProfile) {
      const groupName = scopeName(groupWho(profileDraft.profile));
      const againstName = scopeName(groupWho(profileDraft.reference));
      setProfileDraft((d) => ({
        ...d,
        title: d.title === applied.title && groupName ? `${applied.title} · ${groupName}` : d.title,
        profile: {
          ...d.profile,
          label: groupName && d.profile.label === (applied.groupLabels?.profile ?? "") ? groupName : d.profile.label,
        },
        reference: {
          ...d.reference,
          label:
            againstName && d.reference.label === (applied.groupLabels?.reference ?? "")
              ? againstName
              : d.reference.label,
        },
      }));
    } else {
      const groupName = scopeName(flatWho);
      setFlatDraft((d) => ({
        ...d,
        title: d.title === applied.title && groupName ? `${applied.title} · ${groupName}` : d.title,
      }));
    }
  }

  // ── Save ──
  const gaps = isProfile
    ? profileGaps({
        draft: profileDraft,
        metricCount: profileAxes.length,
        anyExerciseAxis,
        groupName: (key) =>
          profileDraft[key].label.trim() ||
          (key === "profile" ? t("Profile group") : t("Reference group")),
        loading,
        t,
      })
    : flatGaps({
        type: type as Exclude<ChartType, "profile">,
        draft: flatDraft,
        displayBaseID,
        displayBaseID2,
        levelOf,
        loading,
        t,
      });

  async function save() {
    if (gaps.length > 0) {
      setShowValidation(true);
      return;
    }
    if (demo) {
      onSaved(
        demo.save(
          isProfile
            ? profileChartRequest(profileDraft, profileAxes)
            : flatChartRequest(type as Exclude<ChartType, "profile">, flatDraft, { displayBaseID, displayBaseID2 }),
        ),
      );
      return;
    }
    // The wizard shows its busy state while this resolves.
    const saved = isProfile
      ? await saveProfileChart({ mode: "create" }, profileDraft, profileAxes, t)
      : await saveFlatChart(
          type as Exclude<ChartType, "profile">,
          { mode: "create" },
          flatDraft,
          { displayBaseID, displayBaseID2 },
          t,
        );
    if (saved) onSaved(saved);
  }

  // ── Steps ──
  const steps: WizardStep[] = [
    {
      id: "question",
      title: t("Question"),
      isComplete: () => applied != null,
      render: () => (
        <QuestionList
          templates={templates}
          loading={templatesLoading}
          selectedID={applied?.id ?? null}
          onPick={pick}
          alwaysFull={alwaysFull}
          onAlwaysFullChange={onAlwaysFullChange}
        />
      ),
      footer: demo ? undefined : () => <BuildYourselfButton onClick={onBuildYourself} />,
    },
    {
      id: "who",
      title: isProfile ? t("Athletes") : oneAthlete ? t("Athlete") : t("Whose sessions"),
      isComplete: () =>
        isProfile
          ? groupHasAthletes(profileDraft.profile)
          : oneAthlete
            ? flatWho.tagIDs.length === 0 && flatWho.ids.length === 1
            : flatWho.ids.length + flatWho.tagIDs.length > 0,
      // Leaving for "against": the whole squad is the usual baseline — pre-pick it.
      onBeforeNext: () => {
        if (isProfile && !groupHasAthletes(profileDraft.reference) && roster.length > 0) {
          setGroupWho("reference", { ids: roster.map((user) => user.id), tagIDs: [] });
        }
        return true;
      },
      render: () =>
        isProfile ? (
          <WhoStep
            prompt={t("Which athletes do you want to look at?")}
            value={groupWho(profileDraft.profile)}
            onChange={(scope) => setGroupWho("profile", scope)}
            roster={roster}
            pickerTitle={t("Athletes")}
          />
        ) : oneAthlete ? (
          <OneAthleteStep
            prompt={t("Which athlete?")}
            value={flatWho}
            onChange={(scope) => setFlatDraft((d) => ({ ...d, userIDs: scope.ids, userTagIDs: scope.tagIDs }))}
            roster={roster}
          />
        ) : (
          <WhoStep
            prompt={t("Whose sessions?")}
            value={flatWho}
            onChange={(scope) => setFlatDraft((d) => ({ ...d, userIDs: scope.ids, userTagIDs: scope.tagIDs }))}
            roster={roster}
            pickerTitle={t("Athletes")}
          />
        ),
    },
    {
      id: "against",
      title: t("Against"),
      when: () => isProfile,
      isComplete: () => groupHasAthletes(profileDraft.reference),
      render: () => (
        <WhoStep
          prompt={t("Compare them against…")}
          value={groupWho(profileDraft.reference)}
          onChange={(scope) => setGroupWho("reference", scope)}
          roster={roster}
          pickerTitle={t("Athletes")}
        />
      ),
    },
    {
      id: "metrics",
      title: t("Metrics"),
      when: () => isProfile,
      isComplete: () => profileAxes.length > 0,
      render: () => (
        <BundleStep
          prompt={t("Compare them on…")}
          bundleName={applied?.bundleName ?? t("Suggested metrics")}
          bundleIDs={bundleIDs}
          value={profileAxes}
          onChange={(ids) => setProfileDraft((d) => ({ ...d, displayBaseIDs: ids }))}
          options={options}
          loading={loading}
        />
      ),
    },
    {
      id: "lifts",
      title: t("Lifts"),
      // Within the question's shape: exactly one lift, or up to its cap.
      isComplete: () =>
        liftIDs.length === 0
          ? isProfile
            ? !anyExerciseAxis
            : liftsOptional
          : singleLift
            ? liftIDs.length === 1
            : maxLifts == null || liftIDs.length <= maxLifts,
      onBeforeNext: () => {
        nameAfterGroups();
        return true;
      },
      render: () => (
        <LiftsStep
          prompt={
            type === "ranking"
              ? t("Rank on which lift?")
              : isProfile
                ? t("Which lifts, for both groups?")
                : singleLift
                  ? t("On which lift?")
                  : oneAthlete
                    ? t("Which of their lifts?")
                    : t("Which lifts?")
          }
          athleteIDs={liftAthletes}
          timeframe={timeframe}
          onTimeframeChange={setTimeframe}
          value={liftIDs}
          onChange={setLifts}
          single={singleLift}
          max={maxLifts}
          prepick={prepickLifts}
          optional={isProfile ? !anyExerciseAxis : liftsOptional}
          minAthletes={type === "ranking" ? RANKING_MIN_ATHLETES : 0}
          pickerTitle={t("Exercises")}
        />
      ),
    },
    {
      id: "check",
      title: t("Check"),
      render: () =>
        isProfile ? (
          <ProfileCheck
            request={gaps.length === 0 ? profileChartRequest(profileDraft, profileAxes) : null}
            draft={profileDraft}
            setDraft={setProfileDraft}
            roster={roster}
            complete={gaps.length === 0}
            metricNames={profileAxes
              .map((id) => options.find((o) => o.displayBaseID === id)?.label)
              .filter((n): n is string => !!n)}
          />
        ) : (
          <FlatCheck
            type={type as Exclude<ChartType, "profile">}
            request={
              gaps.length === 0
                ? flatChartRequest(type as Exclude<ChartType, "profile">, flatDraft, { displayBaseID, displayBaseID2 })
                : null
            }
            draft={flatDraft}
            setDraft={setFlatDraft}
            members={scopeMembers(flatWho, roster)}
            nameOf={(id) => {
              const user = roster.find((u) => u.id === id);
              return user?.name ?? user?.email ?? "";
            }}
            liftsOptional={liftsOptional}
            metricLabel={options.find((o) => o.displayBaseID === displayBaseID)?.label ?? null}
            metricYLabel={options.find((o) => o.displayBaseID === displayBaseID2)?.label ?? null}
            metricLevel={levelOf(displayBaseID)}
            metricYLevel={levelOf(displayBaseID2)}
            complete={gaps.length === 0}
          />
        ),
    },
  ];

  const wizard = useWizard({ steps, onFinish: save });

  // Picking a question applies it and moves on. Re-picking the one in use just
  // moves on; a different one with athletes/exercises chosen confirms first.
  function pick(template: ChartTemplate) {
    if (applied?.id === template.id) {
      wizard.next();
      return;
    }
    if (hasWhoOrWhich) {
      setConfirmSwitch(template);
      return;
    }
    onApply(template);
    wizard.next();
  }

  const onQuestion = wizard.current?.id === "question";

  return (
    <DrawerView bare>
      <WizardDrawerView
        controller={wizard}
        title={t("New chart")}
        onClose={onClose}
        finishLabel={t("Save chart")}
        trailing={
          applied && !demo ? (
            <button
              type="button"
              onClick={onAdjust}
              className="clickable-small shrink-0 rounded-full bg-surface-base px-4 py-2.5 text-foreground-default shadow ring-1 ring-surface-stroke transition-colors hover:bg-surface-base-muted"
            >
              {t("Adjust everything")}
            </button>
          ) : undefined
        }
        banner={
          applied && !onQuestion ? (
            <div className="space-y-3">
              <div className="rounded-xl bg-foreground-red-orange/10 px-3 py-2">
                <p className="body-small font-medium text-foreground-default">{applied.question}</p>
                <p className="body-small text-foreground-muted">{applied.decide}</p>
              </div>
              {wizard.isLast && showValidation && gaps[0] ? (
                <ErrorAlert>{gaps[0].message}</ErrorAlert>
              ) : null}
            </div>
          ) : undefined
        }
      />
      <ConfirmDialog
        open={confirmSwitch != null}
        title={t("Switch to “{question}”?", { question: confirmSwitch?.question ?? "" })}
        message={t(
          "Your athletes and exercises stay. Chart type, metric, timeframe, title and note are replaced.",
        )}
        confirmLabel={t("Switch")}
        cancelLabel={t("Keep current")}
        onConfirm={() => {
          if (confirmSwitch) {
            onApply(confirmSwitch);
            wizard.next();
          }
          setConfirmSwitch(null);
        }}
        onCancel={() => setConfirmSwitch(null)}
      />
    </DrawerView>
  );
}

// ─── Check step ──────────────────────────────────────────────────────────────

/** "Lena, Tom and 3 more" — at most three names, then a count. */
function namesList(names: string[], t: TranslateFn): string {
  const shown = names.filter((n) => n.length > 0);
  if (shown.length <= 3) return shown.join(", ");
  return t("{names} and {count} more", { names: shown.slice(0, 3).join(", "), count: shown.length - 3 });
}

/** A coverage note: green when everyone has data, amber with one-tap fixes when not. */
function CoverageNote({
  tone,
  children,
  actions,
}: {
  tone: "ok" | "warn";
  children: ReactNode;
  actions?: ReactNode;
}) {
  return (
    <div
      role="status"
      className={`space-y-2 rounded-2xl px-4 py-3 ${
        tone === "ok"
          ? "bg-foreground-platinum-green-bright/12"
          : "bg-feedback-warning/12"
      }`}
    >
      <p className="body-small text-foreground-default">{children}</p>
      {actions ? <div className="flex flex-wrap gap-2">{actions}</div> : null}
    </div>
  );
}

function FixChip({ label, onClick }: { label: string; onClick: () => void }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className="body-small rounded-full border border-surface-stroke bg-surface-base px-3 py-1 text-foreground-default transition-colors hover:bg-surface-base-muted"
    >
      {label}
    </button>
  );
}

/** The timeframe chips, the title and the note — the last touches before saving. */
function FinishingFields({
  timeframe,
  onTimeframeChange,
  title,
  onTitleChange,
  note,
  onNoteChange,
}: {
  timeframe: Timeframe;
  onTimeframeChange: (next: Timeframe) => void;
  title: string;
  onTitleChange: (value: string) => void;
  note: string;
  onNoteChange: (value: string) => void;
}) {
  const { t } = useTranslation();
  return (
    <>
      <section>
        <h3 className="body-small mb-2 text-foreground-muted">{t("Timeframe")}</h3>
        <TimeframePresets value={timeframe} onChange={onTimeframeChange} />
      </section>
      <section>
        <h3 className="body-small mb-2 text-foreground-muted">{t("Title")}</h3>
        <TextField
          className="w-full"
          value={title}
          onChange={(e) => onTitleChange(e.target.value)}
          placeholder={t("Name this chart")}
          aria-label={t("Title")}
        />
      </section>
      <section>
        <h3 className="body-small mb-2 text-foreground-muted">{t("How to read it")}</h3>
        <TextArea
          value={note}
          onChange={(e) => onNoteChange(e.target.value)}
          rows={3}
          placeholder={t("Optional context, shown in the chart's header.")}
          aria-label={t("How to read it")}
          className="block w-full resize-y"
        />
        <p className="body-small mt-1 text-foreground-more-muted">{t("Shown on the chart.")}</p>
      </section>
    </>
  );
}

/** One tap to a timeframe reaching further back, when there is one. */
function useLongerFix(timeframe: Timeframe, onChange: (next: Timeframe) => void) {
  const { t } = useTranslation();
  const now = today();
  const longer = longerPreset(timeframe, now);
  if (!longer) return null;
  return (
    <FixChip
      label={t("Use {period}", { period: presetLabel(longer, t).toLowerCase() })}
      onClick={() => onChange(upToTodayFrom(longer, now))}
    />
  );
}

function FlatCheck({
  type,
  request,
  draft,
  setDraft,
  members,
  nameOf,
  liftsOptional,
  metricLabel,
  metricYLabel,
  metricLevel,
  metricYLevel,
  complete,
}: {
  type: Exclude<ChartType, "profile">;
  /** The chart as Save would send it, once nothing blocks Save; null until then. */
  request: ChartRequest | null;
  draft: FlatDraft;
  setDraft: Dispatch<SetStateAction<FlatDraft>>;
  /** The athletes the chart covers today (fixed picks + tag members). */
  members: string[];
  nameOf: (id: string) => string;
  liftsOptional: boolean;
  metricLabel: string | null;
  metricYLabel: string | null;
  metricLevel?: string;
  metricYLevel?: string;
  /** Nothing blocks Save (the preview then says what it opens as). */
  complete: boolean;
}) {
  const { t } = useTranslation();
  const names = useRecipeNames(draft);
  const activity = useLiftActivity(draft.timeframe);
  const setTimeframe = (timeframe: Timeframe) => setDraft((d) => ({ ...d, timeframe }));
  const longerFix = useLongerFix(draft.timeframe, setTimeframe);
  const lifts = draft.exerciseDefinitionIDs;
  const checkLifts = lifts.length > 0 && draft.exerciseTagIDs.length === 0;

  let coverage: ReactNode = null;
  if (members.length === 0) {
    // A group nobody carries yet: allowed (the chart fills in as athletes get
    // the tag), but said plainly so an empty chart is no surprise.
    coverage = (
      <CoverageNote tone="warn">
        {t("Nobody has this tag yet. The chart stays empty until athletes get it.")}
      </CoverageNote>
    );
  } else if (checkLifts && activity.status === "loading") {
    coverage = <p className="body-small text-foreground-more-muted">{t("Checking who has data…")}</p>;
  } else if (checkLifts && activity.status === "error") {
    coverage = (
      <StalenessHint onRetry={activity.retry}>
        {t("Couldn’t check the data. You can still save.")}
      </StalenessHint>
    );
  } else if (checkLifts && activity.status === "ready") {
    const { withData, without } = liftCoverage(liftActivity(activity.rows, members), lifts, members);
    // Leaving athletes out only works for fixed picks — a group's members come from its tag.
    const leaveOut =
      without.length > 0 && withData.length > 0 && draft.userTagIDs.length === 0 ? (
        <FixChip
          label={t("Leave them out")}
          onClick={() => setDraft((d) => ({ ...d, userIDs: d.userIDs.filter((id) => !without.includes(id)) }))}
        />
      ) : null;
    if (type === "ranking" && withData.length < RANKING_MIN_ATHLETES) {
      coverage = (
        <CoverageNote tone="warn" actions={longerFix}>
          {t(
            "{count, plural, =0 {Nobody has sessions on this lift in this timeframe.} one {Only one athlete has sessions on this lift in this timeframe.} other {Only # athletes have sessions on this lift in this timeframe.}} A ranking needs more.",
            { count: withData.length },
          )}
        </CoverageNote>
      );
    } else if (without.length === 0) {
      coverage = (
        <CoverageNote tone="ok">
          {t(
            "{count, plural, one {The athlete has sessions on the chosen lifts in this timeframe.} other {All # athletes have sessions on the chosen lifts in this timeframe.}}",
            { count: withData.length },
          )}
        </CoverageNote>
      );
    } else {
      coverage = (
        <CoverageNote
          tone="warn"
          actions={
            longerFix || leaveOut ? (
              <>
                {longerFix}
                {leaveOut}
              </>
            ) : undefined
          }
        >
          {withData.length === 0
            ? t("None of these athletes has sessions on the chosen lifts in this timeframe.")
            : t("{withData} of {total} athletes have sessions on the chosen lifts in this timeframe. No sessions: {names}.", {
                withData: withData.length,
                total: members.length,
                names: namesList(without.map(nameOf), t),
              })}
        </CoverageNote>
      );
    }
  }

  return (
    <div className="space-y-6">
      <div>
        <p className="heading-normal text-foreground-default">{t("Check and save")}</p>
        <div className="mt-2 rounded-2xl border border-surface-stroke px-4 py-3">
          <FlatRecipe
            type={type}
            metricLabel={metricLabel}
            metricYLabel={metricYLabel}
            athleteNames={names.athleteNames}
            athleteCount={draft.userIDs.length}
            exerciseNames={names.exerciseNames}
            exerciseCount={draft.exerciseDefinitionIDs.length}
            athleteTagNames={names.athleteTagNames}
            exerciseTagNames={names.exerciseTagNames}
            exercisesOptional={liftsOptional}
          />
        </div>
      </div>
      {coverage}
      <FixedListTagHint
        scope={{ ids: draft.userIDs, tagIDs: draft.userTagIDs }}
        onChange={(scope) => setDraft((d) => ({ ...d, userIDs: scope.ids, userTagIDs: scope.tagIDs }))}
      />
      {type === "correlation" ? (
        <p className="body-small text-foreground-muted">
          {t("Each athlete needs about 10 sessions with both measured before the chart can say whether the two move together.")}
        </p>
      ) : null}
      <FinishingFields
        timeframe={draft.timeframe}
        onTimeframeChange={setTimeframe}
        title={draft.title}
        onTitleChange={(title) => setDraft((d) => ({ ...d, title }))}
        note={draft.note}
        onNoteChange={(note) => setDraft((d) => ({ ...d, note }))}
      />
      <FlatPreviewCard
        type={type}
        request={request}
        draft={draft}
        title={draft.title.trim() || metricLabel}
        metricLabel={metricLabel}
        metricYLabel={metricYLabel}
        metricLevel={metricLevel}
        metricYLevel={metricYLevel}
        exercisesOptional={liftsOptional}
        complete={complete}
      />
    </div>
  );
}

function ProfileCheck({
  request,
  draft,
  setDraft,
  roster,
  metricNames,
  complete,
}: {
  /** The chart as Save would send it, once nothing blocks Save; null until then. */
  request: ChartRequest | null;
  draft: ProfileDraft;
  setDraft: Dispatch<SetStateAction<ProfileDraft>>;
  roster: Parameters<typeof scopeMembers>[1];
  metricNames: string[];
  /** Nothing blocks Save (the preview then says what it opens as). */
  complete: boolean;
}) {
  const { t } = useTranslation();
  const groupText = useProfileGroupText(draft, {
    profile: t("Profile group"),
    reference: t("Reference group"),
  });
  const activity = useLiftActivity(draft.timeframe);
  const setTimeframe = (timeframe: Timeframe) => setDraft((d) => ({ ...d, timeframe }));
  const longerFix = useLongerFix(draft.timeframe, setTimeframe);
  const lifts = draft.profile.exerciseDefinitionIDs;
  const groups = (["profile", "reference"] as const).map((key) => {
    const group = draft[key];
    const members = scopeMembers({ ids: group.userIDs, tagIDs: group.userTagIDs }, roster);
    return { key, label: group.label.trim() || (key === "profile" ? t("Profile group") : t("Reference group")), members };
  });

  let coverage: ReactNode = null;
  if (lifts.length > 0 && activity.status === "loading") {
    coverage = <p className="body-small text-foreground-more-muted">{t("Checking who has data…")}</p>;
  } else if (lifts.length > 0 && activity.status === "error") {
    coverage = (
      <StalenessHint onRetry={activity.retry}>
        {t("Couldn’t check the data. You can still save.")}
      </StalenessHint>
    );
  } else if (lifts.length > 0 && activity.status === "ready") {
    const lines = groups.map((group) => ({
      ...group,
      withData: liftCoverage(liftActivity(activity.rows, group.members), lifts, group.members).withData.length,
    }));
    const complete = lines.every((line) => line.members.length > 0 && line.withData === line.members.length);
    coverage = (
      <CoverageNote tone={complete ? "ok" : "warn"} actions={complete ? undefined : longerFix}>
        {lines
          .map((line) =>
            line.members.length === 0
              ? t("{group}: nobody has this tag yet.", { group: line.label })
              : t("{group}: {withData} of {total} athletes have sessions on these lifts.", {
                  group: line.label,
                  withData: line.withData,
                  total: line.members.length,
                }),
          )
          .join(" ")}
      </CoverageNote>
    );
  }

  return (
    <div className="space-y-6">
      <div>
        <p className="heading-normal text-foreground-default">{t("Check and save")}</p>
        <div className="mt-2 rounded-2xl border border-surface-stroke px-4 py-3">
          <ProfileRecipe
            metricNames={metricNames}
            metricCount={draft.displayBaseIDs.length}
            referenceLabel={draft.reference.label.trim() || null}
            profileLabel={draft.profile.label.trim() || null}
            referenceFilled={groupHasSelection(draft.reference)}
            profileFilled={groupHasSelection(draft.profile)}
          />
        </div>
      </div>
      {coverage}
      <p className="body-small text-foreground-muted">
        {t("Opens as two shapes on one radar: each side's athletes and lifts are pooled into its values.")}
      </p>
      {groups.map((group) => (
        <FixedListTagHint
          key={group.key}
          name={group.label}
          scope={{ ids: draft[group.key].userIDs, tagIDs: draft[group.key].userTagIDs }}
          onChange={(scope) =>
            setDraft((d) => ({
              ...d,
              [group.key]: { ...d[group.key], userIDs: scope.ids, userTagIDs: scope.tagIDs },
            }))
          }
        />
      ))}
      <FinishingFields
        timeframe={draft.timeframe}
        onTimeframeChange={setTimeframe}
        title={draft.title}
        onTitleChange={(title) => setDraft((d) => ({ ...d, title }))}
        note={draft.note}
        onNoteChange={(note) => setDraft((d) => ({ ...d, note }))}
      />
      <ProfilePreviewCard
        request={request}
        draft={draft}
        title={draft.title.trim() || (metricNames[0] ? `${t("Profile")} · ${metricNames[0]}` : null)}
        metricNames={metricNames}
        groupNames={groupText.name}
        axesSummary={axesSummaryText(metricNames, metricNames.length, t)}
        warning={profileAxesHint(metricNames.length, t)}
        complete={complete}
      />
    </div>
  );
}
