"use client";

import { useState, type ReactNode } from "react";
import { assignEntityTag } from "@enode/core/api/tags";
import type { EntityTagReturnDto } from "@enode/core/api/dtos/tags";
import { useHasPrivilege } from "@enode/core/auth/use-privilege";
import { useExercises } from "@enode/core/exercises/hooks";
import { tagsFromUsers } from "@enode/core/tags/from-users";
import { tagDotStyle } from "@enode/core/tags/tag-style";
import { useTagsVersion } from "@enode/core/tags/hooks";
import { upsertTagRelation } from "@enode/core/tags/relations-store";
import { getTag } from "@enode/core/tags/store";
import { displayName } from "@enode/core/text-content/display-name";
import { useTranslation } from "@enode/core/translations/hooks";
import { useUsers } from "@enode/core/users/hooks";
import { loadUsers } from "@enode/core/users/store";
import { isEffectivelyTrainable } from "@enode/core/users/trainable";
import { useDrawerStack } from "@enode/ui/drawer-stack";
import { useExerciseNames } from "@enode/ui/exercise-picker";
import { toast } from "@enode/ui/feedback/toast-store";
import { presentError } from "@enode/ui/feedback/use-error-presenter";
import { TagsCard } from "@enode/ui/tags-card";
import { AthleteSelection } from "../../../../components/selection/athlete-selection";
import { ExerciseSelection } from "../../../../components/selection/exercise-selection";
import { exercisesWithTagHref, usersWithTagHref } from "../../../../components/tag-links";
import { TagCreateView } from "../../tags/tag-drawer";
import { hasScope, type ScopeIDs } from "./recent-scope";
import {
  scopeCoverage,
  tagMemberIDs,
  type TaggedEntity,
} from "../../../../components/selection/tag-members";

// "Who" and "what" a chart covers, as two rows used TOGETHER: fixed picks
// ("always exactly these") plus tags ("everyone with the tag, now and later").
// A tag's members are changed where the tag is assigned — the Users or
// Exercises page — never in the chart, so each tag shows who it covers today
// and links there. A summary line counts the result (see
// components/selection/tag-members.ts).

/**
 * A change to one or both lists, applied in ONE update — e.g. "use this tag
 * instead" swaps fixed picks for a tag at once, which two separate updates
 * against a stale snapshot would lose.
 */
export type ScopeChange = { ids?: string[]; tagIDs?: string[] };

// ─── Athletes ────────────────────────────────────────────────────────────────

export function AthleteScope({
  ids,
  tagIDs,
  onChange,
  pickerTitle,
  recent,
}: {
  ids: string[];
  tagIDs: string[];
  onChange: (change: ScopeChange) => void;
  pickerTitle: string;
  /** The last saved chart's athletes, offered as "Same as last chart". */
  recent?: ScopeIDs | null;
}) {
  const { t } = useTranslation();
  const canWriteTag = useHasPrivilege("writeEntityTag");
  // The roster the chart can cover (trainable, not archived) — with each user's
  // embedded tags, so tag membership is known on the device.
  const { users } = useUsers();
  const roster = (users ?? []).filter(isEffectivelyTrainable);
  const nameOf = (id: string) => {
    const user = users?.find((u) => u.id === id);
    return user?.name ?? user?.email ?? "";
  };
  const tagTheseAthletes = useTagTheseAthletes();
  // Quick picks for an empty selection: the athlete tags with the most
  // members today, and the last saved chart's athletes (still on the roster).
  const tagSuggestions = rankTagsByMembers(tagsFromUsers(roster), roster);
  const rosterIDs = new Set(roster.map((user) => user.id));
  const recentPick = hasScope(recent)
    ? { ids: recent.ids.filter((id) => rosterIDs.has(id)), tagIDs: recent.tagIDs }
    : null;

  return (
    <div className="space-y-3">
      {ids.length === 0 && tagIDs.length === 0 ? (
        <QuickPicks
          tags={tagSuggestions}
          recentLabel={
            hasScope(recentPick)
              ? scopeLabel(recentPick, (id) => nameOf(id), t)
              : null
          }
          onPickTag={(tagID) => onChange({ tagIDs: [tagID] })}
          onPickRecent={() => recentPick && onChange(recentPick)}
        />
      ) : null}

      <ScopeRow
        kind="fixed"
        title={t("Specific athletes")}
        caption={t("Always exactly these athletes.")}
        action={
          canWriteTag && ids.length >= 1 ? (
            <TagTheseAthletesButton
              count={ids.length}
              // The chart now follows the new tag instead of the fixed list.
              onClick={() => tagTheseAthletes(ids, (tag) => onChange({ ids: [], tagIDs: [...tagIDs, tag.id] }))}
            />
          ) : null
        }
      >
        <AthleteSelection
          pickerTitle={pickerTitle}
          ids={ids}
          onChange={(next) => onChange({ ids: next })}
          emptyHint=""
          hideLabel
          // Picking exactly one tag's members offers the tag itself; picks
          // outside it stay fixed.
          onUseTag={(tagID, otherIDs) =>
            onChange({
              ids: otherIDs,
              tagIDs: tagIDs.includes(tagID) ? tagIDs : [...tagIDs, tagID],
            })
          }
        />
      </ScopeRow>

      <PlusDivider />

      <ScopeRow
        kind="live"
        title={t("Tags")}
        caption={t("Everyone with the tag, now and later.")}
      >
        <TagRow
          kind="user"
          tagIDs={tagIDs}
          onChange={(next) => onChange({ tagIDs: next })}
          members={(tagID) => tagMemberIDs(roster, tagID).map(nameOf)}
          countLabel={(count) =>
            t("{count, plural, one {# athlete now} other {# athletes now}}", { count })
          }
          manageHref={usersWithTagHref}
          manageLabel={t("Change who has it")}
          emptyNote={t("Nobody has this tag yet, so it adds no athletes until someone gets it.")}
        />
      </ScopeRow>

      <CoverageSummary
        show={tagIDs.length > 0}
        coverage={scopeCoverage(ids, tagIDs, roster)}
        nameOf={nameOf}
        totalLabel={(count) =>
          t("{count, plural, one {Covers # athlete today.} other {Covers # athletes today.}}", {
            count,
          })
        }
      />

    </div>
  );
}

// ─── Exercises ───────────────────────────────────────────────────────────────

export function ExerciseScope({
  ids,
  tagIDs,
  onChange,
  pickerTitle,
  athleteIDs,
  emptyHint,
  recent,
}: {
  ids: string[];
  tagIDs: string[];
  onChange: (change: ScopeChange) => void;
  pickerTitle: string;
  /** Fixed athletes whose own exercises join the picker. */
  athleteIDs: string[];
  emptyHint?: string;
  /** The last saved chart's exercises, offered as "Same as last chart". */
  recent?: ScopeIDs | null;
}) {
  const { t } = useTranslation();
  const { exercises } = useExercises();
  const exerciseName = useExerciseNames();
  // Charts filter exercises by DEFINITION, so membership is keyed by it too.
  const catalogue: TaggedEntity[] = (exercises ?? []).flatMap((exercise) =>
    exercise.exerciseDefinitionID
      ? [{ id: exercise.exerciseDefinitionID, assignedTags: exercise.assignedTags }]
      : [],
  );

  // Quick picks for an empty selection: the exercise tags covering the most
  // exercises, and the last saved chart's exercises.
  const exerciseTags = uniqueTags(
    (exercises ?? []).flatMap((exercise) => exercise.assignedTags ?? []),
  );
  const tagSuggestions = rankTagsByMembers(exerciseTags, catalogue);

  return (
    <div className="space-y-3">
      {ids.length === 0 && tagIDs.length === 0 ? (
        <QuickPicks
          tags={tagSuggestions}
          recentLabel={hasScope(recent) ? scopeLabel(recent, exerciseName, t) : null}
          onPickTag={(tagID) => onChange({ tagIDs: [tagID] })}
          onPickRecent={() => recent && onChange(recent)}
        />
      ) : null}

      <ScopeRow
        kind="fixed"
        title={t("Specific exercises")}
        caption={t("Always exactly these exercises.")}
      >
        <ExerciseSelection
          pickerTitle={pickerTitle}
          ids={ids}
          athleteIDs={athleteIDs}
          onChange={(next) => onChange({ ids: next })}
          emptyHint={emptyHint ?? ""}
          hideLabel
        />
      </ScopeRow>

      <PlusDivider />

      <ScopeRow
        kind="live"
        title={t("Tags")}
        caption={t("Every exercise with the tag, now and later.")}
      >
        <TagRow
          kind="exercise"
          tagIDs={tagIDs}
          onChange={(next) => onChange({ tagIDs: next })}
          members={(tagID) => tagMemberIDs(catalogue, tagID).map(exerciseName)}
          countLabel={(count) =>
            t("{count, plural, one {# exercise now} other {# exercises now}}", { count })
          }
          manageHref={exercisesWithTagHref}
          manageLabel={t("Change which exercises have it")}
          emptyNote={t("No exercise has this tag yet, so it adds none until one gets it.")}
        />
      </ScopeRow>

      <CoverageSummary
        show={tagIDs.length > 0}
        coverage={scopeCoverage(ids, tagIDs, catalogue)}
        nameOf={exerciseName}
        totalLabel={(count) =>
          t(
            "{count, plural, one {Covers # exercise today.} other {Covers # exercises today.}}",
            { count },
          )
        }
      />
    </div>
  );
}

// ─── Building blocks ─────────────────────────────────────────────────────────

/** How many tags a quick-pick row offers. */
const MAX_TAG_SUGGESTIONS = 4;

/** Tags with members today, most members first (at most a handful). */
function rankTagsByMembers(
  tags: readonly EntityTagReturnDto[],
  entities: readonly TaggedEntity[],
): { tag: EntityTagReturnDto; count: number }[] {
  return tags
    .map((tag) => ({ tag, count: tagMemberIDs(entities, tag.id).length }))
    .filter((entry) => entry.count > 0)
    .sort((a, b) => b.count - a.count)
    .slice(0, MAX_TAG_SUGGESTIONS);
}

function uniqueTags(tags: readonly EntityTagReturnDto[]): EntityTagReturnDto[] {
  const byID = new Map<string, EntityTagReturnDto>();
  for (const tag of tags) if (tag.id && !byID.has(tag.id)) byID.set(tag.id, tag);
  return [...byID.values()];
}

/** "Forwards, Lena +2" — the tags first, then fixed picks, kept short. */
function scopeLabel(
  scope: ScopeIDs,
  nameOf: (id: string) => string,
  t: ReturnType<typeof useTranslation>["t"],
): string {
  const names = [
    ...scope.tagIDs.map((id) => displayName(getTag(id)).trim()),
    ...scope.ids.map(nameOf),
  ].filter((name) => name.length > 0);
  const shown = names.slice(0, 2).join(", ");
  const rest = names.length - 2;
  return rest > 0 ? t("{names} +{count}", { names: shown, count: rest }) : shown;
}

// One-tap starts for an empty selection: the last saved chart's picks and the
// busiest tags (with today's counts). Picking one fills the rows below, where
// it stays fully editable.
function QuickPicks({
  tags,
  recentLabel,
  onPickTag,
  onPickRecent,
}: {
  tags: { tag: EntityTagReturnDto; count: number }[];
  recentLabel: string | null;
  onPickTag: (tagID: string) => void;
  onPickRecent: () => void;
}) {
  const { t } = useTranslation();
  if (tags.length === 0 && !recentLabel) return null;
  const chip =
    "body-small inline-flex max-w-full items-center gap-1.5 rounded-full border border-surface-stroke px-3 py-1 text-foreground-default transition-colors hover:bg-surface-base-muted";
  return (
    <div>
      <p className="body-small mb-1.5 text-foreground-muted">{t("Quick picks")}</p>
      <div className="flex flex-wrap gap-1.5">
        {recentLabel ? (
          <button type="button" onClick={onPickRecent} className={chip}>
            <span className="shrink-0 text-foreground-muted">{t("Same as last chart:")}</span>
            <span className="truncate">{recentLabel}</span>
          </button>
        ) : null}
        {tags.map(({ tag, count }) => (
          <button
            key={tag.id}
            type="button"
            onClick={() => onPickTag(tag.id)}
            className={chip}
          >
            <span
              aria-hidden
              className="size-2 shrink-0 rounded-full"
              style={tagDotStyle(tag.colorCode)}
            />
            <span className="truncate">{displayName(tag).trim()}</span>
            <span className="shrink-0 text-foreground-muted">· {count}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

// One of the two rows: a Fixed / Live badge, a title, one line on what it
// means, then the control. `action` sits at the right of the header.
function ScopeRow({
  kind,
  title,
  caption,
  action,
  children,
}: {
  kind: "fixed" | "live";
  title: string;
  caption: string;
  action?: ReactNode;
  children: ReactNode;
}) {
  return (
    <div>
      <div className="flex items-center gap-2">
        <KindBadge kind={kind} />
        <span className="body-prominent min-w-0 flex-1 truncate text-foreground-default">
          {title}
        </span>
        {action}
      </div>
      <p className="body-small mb-2 mt-0.5 text-foreground-more-muted">{caption}</p>
      {children}
    </div>
  );
}

function KindBadge({ kind }: { kind: "fixed" | "live" }) {
  const { t } = useTranslation();
  return kind === "fixed" ? (
    <span className="stat-small shrink-0 rounded-full bg-surface-base-muted px-2 py-0.5 text-foreground-default">
      {t("Fixed")}
    </span>
  ) : (
    <span className="stat-small inline-flex shrink-0 items-center gap-1 rounded-full bg-foreground-platinum-green-bright/12 px-2 py-0.5 text-foreground-platinum-green-bright">
      <span className="size-1.5 rounded-full bg-foreground-platinum-green-bright" />
      {t("Live")}
    </span>
  );
}

// The "+" between the rows: both are used together, not one or the other.
function PlusDivider() {
  return (
    <div className="flex items-center gap-2" aria-hidden>
      <span className="h-px flex-1 bg-surface-stroke" />
      <span className="body-prominent flex size-6 items-center justify-center rounded-full border border-surface-stroke text-foreground-muted">
        +
      </span>
      <span className="h-px flex-1 bg-surface-stroke" />
    </div>
  );
}

// The picked tags, then one line per tag: who it covers today (expandable to
// names) and where its members are changed — opened in a new tab so the chart
// being built isn't lost; the lists revalidate when the coach comes back.
function TagRow({
  kind,
  tagIDs,
  onChange,
  members,
  countLabel,
  manageHref,
  manageLabel,
  emptyNote,
}: {
  kind: "user" | "exercise";
  tagIDs: string[];
  onChange: (tagIDs: string[]) => void;
  members: (tagID: string) => string[];
  countLabel: (count: number) => string;
  manageHref: (tagID: string) => string;
  manageLabel: string;
  emptyNote: string;
}) {
  useTagsVersion();
  const tags = tagIDs
    .map((id) => getTag(id))
    .filter((tag): tag is EntityTagReturnDto => tag != null);
  return (
    <div className="space-y-2">
      <TagsCard
        bare
        tags={tags}
        entityKind={kind}
        onChange={(next) => onChange(next.map((tag) => tag.id))}
      />
      {tags.map((tag) => (
        <TagMembersLine
          key={tag.id}
          name={displayName(tag).trim()}
          members={members(tag.id)}
          countLabel={countLabel}
          manageHref={manageHref(tag.id)}
          manageLabel={manageLabel}
          emptyNote={emptyNote}
        />
      ))}
    </div>
  );
}

// A tag nobody carries yet is allowed (it may fill up later) but flagged, so
// an empty chart is never a surprise.
function TagMembersLine({
  name,
  members,
  countLabel,
  manageHref,
  manageLabel,
  emptyNote,
}: {
  name: string;
  members: string[];
  countLabel: (count: number) => string;
  manageHref: string;
  manageLabel: string;
  emptyNote: string;
}) {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);
  const named = members.filter((member) => member.length > 0);
  return (
    <div className="rounded-xl bg-surface-base-muted/50 px-3 py-2">
      <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
        <span className="body-small font-medium text-foreground-default">{name}</span>
        <span className="body-small text-foreground-muted">· {countLabel(members.length)}</span>
        {named.length > 0 ? (
          <button
            type="button"
            onClick={() => setOpen((value) => !value)}
            aria-expanded={open}
            className="clickable-small body-small text-foreground-muted underline-offset-2 hover:underline"
          >
            {open ? t("Hide") : t("Show")}
          </button>
        ) : null}
        <a
          href={manageHref}
          target="_blank"
          rel="noopener noreferrer"
          className="clickable-small body-small ml-auto text-foreground-red-orange hover:opacity-80"
        >
          {manageLabel} ↗
        </a>
      </div>
      {open ? (
        <p className="body-small mt-1 text-foreground-muted">{named.join(", ")}</p>
      ) : null}
      {members.length === 0 ? (
        <p className="body-small mt-1 text-foreground-red-orange">{emptyNote}</p>
      ) : null}
    </div>
  );
}

// Who the chart covers today, counting a fixed pick that also carries a tag
// once — and saying so. Only shown once tags are in play (without them the
// fixed list already says it all).
function CoverageSummary({
  show,
  coverage,
  nameOf,
  totalLabel,
}: {
  show: boolean;
  coverage: { ids: string[]; overlapIDs: string[] };
  nameOf: (id: string) => string;
  totalLabel: (count: number) => string;
}) {
  const { t } = useTranslation();
  if (!show) return null;
  const overlapNames = coverage.overlapIDs.map(nameOf).filter((name) => name.length > 0);
  return (
    <p className="body-small rounded-xl border border-surface-stroke px-3 py-2 text-foreground-default">
      {totalLabel(coverage.ids.length)}
      {overlapNames.length > 0 ? (
        <span className="text-foreground-muted">
          {" "}
          {t("{names} {count, plural, one {is} other {are}} also tagged, counted once.", {
            names: overlapNames.join(", "),
            count: overlapNames.length,
          })}
        </span>
      ) : null}
    </p>
  );
}

/** The "Tag these athletes…" link — pair with {@link useTagTheseAthletes}. */
export function TagTheseAthletesButton({
  count,
  onClick,
}: {
  count: number;
  onClick: () => void;
}) {
  const { t } = useTranslation();
  return (
    <button
      type="button"
      onClick={onClick}
      className="clickable-small body-small shrink-0 text-foreground-red-orange transition-opacity hover:opacity-80"
    >
      {t("{count, plural, one {Tag this athlete…} other {Tag these athletes…}}", { count })}
    </button>
  );
}

/**
 * "Tag these athletes…": opens the shared tag form (the Tags page's, pushed
 * onto the surrounding drawer stack, "Use for: Users" preset). Once the tag is
 * created it's given to the athletes (additive assign — nobody else's tags
 * change) and handed back, so the chart follows the tag instead of the fixed
 * list and keeps growing as athletes get it. The tag exists from that moment
 * on, even if the chart is then discarded (it can be deleted on the Users or
 * Tags page). Must be called inside a DrawerStack.
 *
 * @returns `open(athleteIDs, onTagged)`. `onTagged` runs only once the tag is
 *   assigned; a failed assign is presented with a retry, and the chart keeps
 *   its fixed list.
 */
export function useTagTheseAthletes(): (
  athleteIDs: string[],
  onTagged: (tag: EntityTagReturnDto) => void,
) => void {
  const { t } = useTranslation();
  const stack = useDrawerStack();
  return (athleteIDs, onTagged) => {
    async function assign(tag: EntityTagReturnDto) {
      try {
        const relations = await assignEntityTag(tag.id, { userIDs: athleteIDs });
        upsertTagRelation(relations);
        // The users aren't returned — revalidate so their embedded tags (and
        // the member counts in the pickers) catch up.
        loadUsers({ force: true });
        toast.success(
          t("Tag “{name}” created. The chart now follows it.", { name: displayName(tag).trim() }),
        );
        onTagged(tag);
      } catch (err) {
        presentError(err, t, { onRetry: () => void assign(tag) });
      }
    }
    stack.push(
      <TagCreateView
        initialKinds={{ user: true }}
        intro={t(
          "{count, plural, one {The new tag goes to this athlete, and the chart follows it.} other {The new tag goes to these # athletes, and the chart follows it.}} Athletes who get the tag later join the chart.",
          { count: athleteIDs.length },
        )}
        onCreated={(tag) => void assign(tag)}
      />,
    );
  };
}
