"use client";

import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import type {
  EntityTagCreateDto,
  EntityTagReturnDto,
  EntityTagReturnWithRelationsDto,
  SharingVisibility,
  TagColorDto,
} from "@enode/core/api/dtos/tags";
import {
  createEntityTag,
  deleteEntityTag,
  updateEntityTag,
} from "@enode/core/api/tags";
import { getCurrentUserID } from "@enode/core/api/storage";
import {
  sharedAuthorName,
  sharedEntityScope,
} from "@enode/core/sharing/scope";
import {
  sharingLockState,
  useCanShare,
} from "@enode/core/sharing/share-privileges";
import { useFreshTag } from "@enode/core/tags/hooks";
import {
  loadEntityTagColors,
  removeTag,
  upsertTag,
} from "@enode/core/tags/store";
import { tagDotStyle, tagPillStyle } from "@enode/core/tags/tag-style";
import { useDisplayName } from "@enode/core/text-content/useDisplayName";
import { useTextContent } from "@enode/core/text-content/useTextContent";
import { useTranslation } from "@enode/core/translations/hooks";
import type { TranslateFn } from "@enode/core/translations";
import { Button } from "@enode/ui/button";
import { ConfirmDialog } from "@enode/ui/confirm-dialog";
import { DrawerStack, DrawerView, useDrawerStack } from "@enode/ui/drawer-stack";
import { InlineAlert } from "@enode/ui/feedback/inline-alert";
import { StalenessHint } from "@enode/ui/feedback/staleness-hint";
import { presentError } from "@enode/ui/feedback/use-error-presenter";
import { Card, Row, RowDivider, SectionLabel } from "@enode/ui/list-rows";
import { ProvenanceBadge } from "@enode/ui/sharing/provenance-badge";
import { SegmentedPicker } from "@enode/ui/segmented-picker";
import {
  useVisibilityLabel,
  visibilityIcon,
} from "@enode/ui/sharing/visibility";
import { Toggle } from "@enode/ui/settings/settings-rows";
import { TextField } from "@enode/ui/text-field";
import { useDrawerHost } from "../../../components/use-drawer-host";
import { isTagReadOnly } from "./tag-access";
import { TourAutoStart } from "@enode/ui/tour/tour-provider";
import { TOUR_TARGET } from "../../../tours/tour-targets";
import { useTagCreateTour } from "../../../tours/use-tag-create-tour";
import {
  TAG_KINDS,
  type TagKindFlags,
  tagKindFlags,
  tagKindLabel,
} from "./tag-kinds";

// The palette is served by `GET /entity_tags/colors`; this fixed set (mirroring
// the old portal's palette) is only a fallback so the editor still offers
// swatches when that fetch fails or the app is offline.
const FALLBACK_TAG_COLORS: TagColorDto[] = [
  "#22c55e",
  "#84cc16",
  "#3b82f6",
  "#14b8a6",
  "#a855f7",
  "#ef4444",
  "#f97316",
  "#eab308",
].map((hex) => ({ name: hex, hex }));

// Why a read-only tag can't be edited, stated as the reason the reader can act
// on. A system-provided public tag has no author to name, so it says what it is
// instead. Literal t("…") per branch, like every other hint here.
function readOnlyHint(
  visibility: string | null | undefined,
  t: TranslateFn,
): string {
  if (visibility === "sharedPublic") {
    return t(
      "Provided by the system and shared with everyone — view only, can’t be edited or deleted.",
    );
  }
  return t(
    "Only its author can change this tag. You can use it as it is — attach it, filter by it — but not edit or delete it.",
  );
}

// One-line explanation of the selected access level, shown under the picker so
// the user learns what each option means as they pick it. The selected value
// already shows in the control, so the caption states the scope, not the name.
// Literal t("…") per branch so the extractor registers each string. `null` for
// anything unmapped rather than a wrong default.
function visibilityHint(
  visibility: SharingVisibility,
  t: TranslateFn,
): string | null {
  switch (visibility) {
    case "sharedPrivate":
      return t("Only you, the tag’s creator, can see this tag.");
    case "sharedInternal":
      return t("Available to everyone in your organisation.");
    default:
      return null;
  }
}

// Create / edit a tag (same chrome + list-row vocabulary as UserDrawer). Pass
// `tag` to edit it (prefilled, with a delete affordance); omit it to create.
export function TagDrawer({
  target,
  onClose,
  onSaved,
  onDeleted,
}: {
  // `initialKinds` presets the "Use for" kinds of a NEW tag (ignored when editing).
  target: {
    tag?: EntityTagReturnWithRelationsDto;
    initialKinds?: Partial<TagKindFlags>;
  } | null;
  onClose: () => void;
  // The created/updated tag, so the caller can upsert its list row in place.
  onSaved: (saved: EntityTagReturnDto) => void;
  onDeleted: (id: string) => void;
}) {
  const { t } = useTranslation();
  const { open, rendered, sessionKey } = useDrawerHost(target);
  const tagCreateTour = useTagCreateTour();
  if (!rendered) return null;
  // Someone else's tag — and every public one — is viewable but not editable;
  // the drawer opens read-only. See `isTagReadOnly`.
  const readOnly = isTagReadOnly(rendered.tag, getCurrentUserID());
  return (
    <>
    {/* First-creation tour over the open drawer — the chain target of the
        Tags tour's click-to-advance step. Create mode only: opening an
        existing tag never triggers it. Gated on `open` so it can't fire
        during the slide-out. */}
    {open && !rendered.tag && <TourAutoStart tour={tagCreateTour} />}
    <DrawerStack
      key={sessionKey}
      open={open}
      onClose={onClose}
      ariaLabel={
        readOnly ? t("Tag") : rendered.tag ? t("Edit tag") : t("Create tag")
      }
      dismissable={false}
      root={
        <TagForm
          tag={rendered.tag}
          initialKinds={rendered.initialKinds}
          onClose={onClose}
          onSaved={onSaved}
          onDeleted={onDeleted}
        />
      }
    />
    </>
  );
}

/**
 * The tag form as a view PUSHED onto another drawer's stack, to create a tag
 * where it's needed (e.g. "Tag these athletes…" in the chart builder) instead
 * of on the Tags page. Same form, header and Create action as the TagDrawer;
 * its back arrow and a successful Create pop it off again.
 *
 * @param initialKinds Presets the "Use for" kinds (e.g. `{ user: true }` when
 *   the tag is created for athletes); the coach can still change them.
 * @param intro A short line above the form saying what happens with the tag.
 * @param onCreated The created tag, after the view has been popped.
 */
export function TagCreateView({
  initialKinds,
  intro,
  onCreated,
}: {
  initialKinds?: Partial<TagKindFlags>;
  intro?: ReactNode;
  onCreated: (tag: EntityTagReturnDto) => void;
}) {
  const stack = useDrawerStack();
  return (
    <TagForm
      initialKinds={initialKinds}
      intro={intro}
      onClose={() => stack.pop()}
      onSaved={onCreated}
      onDeleted={() => {}}
    />
  );
}

function TagForm({
  tag,
  initialKinds,
  intro,
  onClose,
  onSaved,
  onDeleted,
}: {
  tag?: EntityTagReturnWithRelationsDto;
  initialKinds?: Partial<TagKindFlags>;
  intro?: ReactNode;
  onClose: () => void;
  onSaved: (saved: EntityTagReturnDto) => void;
  onDeleted: (id: string) => void;
}) {
  const { t } = useTranslation();
  const visibilityLabel = useVisibilityLabel();
  const isEdit = tag != null;
  // Someone else's tag is read-only: the drawer presents the data but every
  // control is inert, and Save / Delete are hidden.
  //
  // OWNERSHIP decides, NOT the access level and NOT `sharingPrivilege`. A
  // `write` grant deliberately does not unlock editing in place — the product
  // rule is edit-copies-first, and `canEditSharedDefinition` (the 313 dialog)
  // is the one predicate in the codebase that honours `write`. Gating on
  // `sharedPublic` alone, as this did, left a colleague's INTERNAL tag fully
  // editable and bulk-deletable.
  const myUserID = useMemo(() => getCurrentUserID(), []);
  const readOnly = isEdit && isTagReadOnly(tag, myUserID);
  const scope = sharedEntityScope(tag, myUserID);
  const foreignScope = readOnly && scope !== "mine" ? scope : null;
  // Whether this account may raise a tag off Private at all. `readOnly` is
  // somebody else's tag — inbound, so the grant has no say there.
  const canShareTags = useCanShare("tags");

  // Seeded from the tag when editing (the drawer mounts fresh per open, so the
  // initializers run once with the right values).
  const [name, setName] = useState(tag?.name ?? "");
  const [detail, setDetail] = useState(tag?.detail ?? "");
  const [colorCode, setColorCode] = useState<string>(
    tag?.colorCode ?? FALLBACK_TAG_COLORS[0].hex,
  );
  // The swatch palette from `GET /entity_tags/colors`. `null` while it loads (the
  // picker shows a skeleton) so the real colours appear in one shot, instead of a
  // reduced fallback set popping over to the server list. The static fallback is
  // used only if the fetch fails or returns nothing.
  const [palette, setPalette] = useState<TagColorDto[] | null>(null);
  useEffect(() => {
    let cancelled = false;
    // Session-cached + deduped in the store: repeat opens (and the drawer's
    // StrictMode/portal re-mount) reuse one fetch instead of re-hitting the
    // static palette endpoint each time.
    loadEntityTagColors()
      .then((colors) => {
        if (cancelled) return;
        setPalette(colors.length > 0 ? colors : FALLBACK_TAG_COLORS);
      })
      .catch((err) => {
        if (cancelled) return;
        // Non-blocking: fall back to the static palette so the user can still pick.
        console.error("Failed to load tag colours:", err);
        setPalette(FALLBACK_TAG_COLORS);
      });
    return () => {
      cancelled = true;
    };
  }, []);
  const [visibility, setVisibility] = useState<SharingVisibility>(
    (tag?.sharingVisibility as SharingVisibility) ?? "sharedPrivate",
  );
  const [kinds, setKinds] = useState<TagKindFlags>(() =>
    tag ? tagKindFlags(tag) : { ...tagKindFlags(null), ...initialKinds },
  );
  const [submitting, setSubmitting] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  // Driven by the CURRENT pick, so "Make it private" collapses the section the
  // moment it lands rather than lingering on a share already revoked.
  const lock = sharingLockState(canShareTags, visibility);
  const lockedVisibility =
    lock.kind === "lockedShared" ? lock.visibility : visibility;

  // Fetch-before-edit: force-revalidate the (small) catalogue on open — the
  // per-tag equivalent of a by-id fetch — and gate Save until it confirms.
  // `deleted` = refresh succeeded but the tag is gone (deleted in another app).
  const fresh = useFreshTag(tag?.id ?? null);
  // Whether any field was touched — guards the fresh reseed below.
  const dirtyRef = useRef(false);
  const markDirty = () => {
    dirtyRef.current = true;
  };
  // Reseed from the fresh catalogue entry while PRISTINE (it usually lands
  // before the first keystroke); after that the admin's input wins.
  // Render-time state adjustment (cf. exercise-detail-drawer's per-exercise
  // reseed) rather than an effect.
  const freshTag = fresh.status === "ready" ? fresh.tag : null;
  const [seededFrom, setSeededFrom] = useState<EntityTagReturnDto | null>(null);
  if (freshTag && freshTag !== seededFrom && !dirtyRef.current) {
    setSeededFrom(freshTag);
    setName(freshTag.name ?? "");
    setDetail(freshTag.detail ?? "");
    if (freshTag.colorCode) setColorCode(freshTag.colorCode);
    setVisibility(
      (freshTag.sharingVisibility as SharingVisibility) ?? "sharedPrivate",
    );
    setKinds(tagKindFlags(freshTag));
  }

  const freshBlocked =
    isEdit && (fresh.status !== "ready" || fresh.deleted);
  const canSave = name.trim().length > 0;

  // Public/system tags usually carry no literal `name`/`detail` — only
  // localized text-content ids. The read-only view resolves through the
  // display-name rule (own value, else text content); the editors keep
  // editing the literal fields, which user-created tags always have.
  const resolvedName = useDisplayName(freshTag ?? tag, "");
  const resolvedDetail = useTextContent((freshTag ?? tag)?.detailTextContentID);

  async function handleSave() {
    if (!canSave || freshBlocked) return;
    setSubmitting(true);
    try {
      const dto: EntityTagCreateDto = {
        id: tag?.id ?? crypto.randomUUID(),
        name: name.trim(),
        detail: detail.trim() || null,
        colorCode,
        sharingVisibility: visibility,
        isExerciseTag: kinds.exercise,
        isWorkoutTag: kinds.workout,
        isUserTag: kinds.user,
        isSessionTag: kinds.session,
        isSetTag: kinds.set,
        isRepTag: kinds.rep,
      };
      // The plain PUT updates only intrinsic fields — assignments are untouched.
      const saved = tag
        ? await updateEntityTag(tag.id, dto)
        : await createEntityTag(dto);
      upsertTag(saved); // so existing tag pickers reflect it without a reload
      onSaved(saved);
      onClose();
    } catch (err) {
      presentError(err, t, { onRetry: handleSave });
    } finally {
      setSubmitting(false);
    }
  }

  async function handleDelete() {
    if (!tag) return;
    setSubmitting(true);
    try {
      await deleteEntityTag(tag.id);
      removeTag(tag.id);
      onDeleted(tag.id);
      onClose();
    } catch (err) {
      setConfirmDelete(false); // surface the error behind the prompt
      presentError(err, t, { onRetry: handleDelete });
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <DrawerView
      title={readOnly ? t("Tag") : isEdit ? t("Edit tag") : t("Create tag")}
      trailing={
        readOnly ? undefined : (
          <Button
            variant="primary"
            size="rounded"
            onClick={handleSave}
            disabled={!canSave || submitting || freshBlocked}
            data-tour={TOUR_TARGET.tagSave}
          >
            {submitting ? t("Saving…") : isEdit ? t("Save") : t("Create")}
          </Button>
        )
      }
    >
      <div className="space-y-6 py-2">
        {!isEdit && intro ? (
          <p className="body-small rounded-xl bg-surface-base-muted px-3 py-2 text-foreground-muted">
            {intro}
          </p>
        ) : null}
        {/* Deleted in another app: refresh succeeded but the tag is gone —
            surface the state instead of allowing a doomed save. The list
            catches up on its next revalidation. */}
        {isEdit && fresh.deleted && (
          <InlineAlert title={t("This tag no longer exists")}>
            {t("It was deleted in another app. Close this drawer — the list will update itself.")}
          </InlineAlert>
        )}
        {/* The refresh failed — saving stays paused; viewing works. Not shown
            for read-only public tags, where there's nothing to save. */}
        {isEdit && !readOnly && fresh.status === "error" && (
          <StalenessHint onRetry={fresh.retry}>
            {t("Couldn’t refresh this tag — saving is paused until it can be reloaded.")}
          </StalenessHint>
        )}
        {/* Appearance first — a live preview of the pill the tag will BE in
            every list, picker and chip, with the palette right under it. The
            colour used to be a swatch cloud crammed into a row's trailing slot,
            on the one page that never showed the result of picking one. The
            tour's "name it" step spotlights this and General together. */}
        <div data-tour={TOUR_TARGET.tagGeneral} className="space-y-6">
          <section>
            <SectionLabel>{t("Appearance")}</SectionLabel>
            <Card>
              <div className="flex items-center justify-center px-4 py-6">
                <TagPreview
                  name={readOnly ? resolvedName : name}
                  colorCode={colorCode}
                />
              </div>
              {!readOnly && (
                <>
                  <RowDivider />
                  <div className="px-4 py-3">
                    <ColorPicker
                      colors={palette}
                      value={colorCode}
                      onChange={(next) => {
                        markDirty();
                        setColorCode(next);
                      }}
                    />
                  </div>
                </>
              )}
            </Card>
          </section>

          <section>
            <SectionLabel>{t("General")}</SectionLabel>
            <Card>
              <Row
                label={t("Name")}
                trailing={
                  readOnly ? (
                    <ReadOnlyValue>{resolvedName || "—"}</ReadOnlyValue>
                  ) : (
                    <TextField
                      className="w-56 text-right"
                      value={name}
                      onChange={(e) => {
                        markDirty();
                        setName(e.target.value);
                      }}
                      placeholder={t("Tag name")}
                    />
                  )
                }
              />
              <RowDivider />
              {/* Detail: a full-width, borderless auto-growing textarea that
                  fills its own row (no label) — mirrors the exercise drawer's
                  Details field. */}
              <div className="px-4 py-3">
                {readOnly ? (
                  <p className="body-prominent whitespace-pre-wrap text-foreground-default">
                    {detail.trim() || resolvedDetail?.trim() || "—"}
                  </p>
                ) : (
                  <textarea
                    value={detail}
                    onChange={(e) => {
                      markDirty();
                      setDetail(e.target.value);
                    }}
                    placeholder={t("Add details")}
                    rows={1}
                    className="body-prominent field-sizing-content w-full resize-none border-0 bg-transparent p-0 text-foreground-default outline-none placeholder:text-foreground-muted"
                  />
                )}
              </div>
            </Card>
          </section>
        </div>

        {/* No sharing grant and nothing shared ⇒ the whole section is absent
            and the tag stays Private. A disabled picker would advertise a
            capability the account does not have. */}
        {(readOnly || lock.kind !== "locked") && (
        <section data-tour={TOUR_TARGET.tagAccess}>
          <SectionLabel>{t("Access level")}</SectionLabel>
          <Card>
            {/* One decision, so no row label repeating the section heading: the
                picker IS the row, and the hint below it tracks the live
                selection and teaches each level as it's picked — quiet enough
                to live with on every open, not an alert. */}
            <div className="space-y-2.5 px-4 py-3">
              {readOnly || lock.kind === "lockedShared" ? (
                <ReadOnlyValue>
                  {visibilityLabel(
                    readOnly ? tag?.sharingVisibility : lockedVisibility,
                  ) || "—"}
                </ReadOnlyValue>
              ) : (
                // Shared pill picker (same as the set editor's "Set type" row),
                // not a native <select>. Only Private/Organisation are creatable
                // — "Public" tags are system-provided and stay read-only (see
                // page.tsx), so they aren't offered here.
                <SegmentedPicker<SharingVisibility>
                  fullWidth
                  ariaLabel={t("Visibility")}
                  value={visibility}
                  onChange={(next) => {
                    markDirty();
                    setVisibility(next);
                  }}
                  // Labels come from the shared resolver, not local literals:
                  // one word per access level across both apps.
                  options={[
                    {
                      value: "sharedPrivate",
                      label: visibilityLabel("sharedPrivate"),
                    },
                    {
                      value: "sharedInternal",
                      label: visibilityLabel("sharedInternal"),
                    },
                  ]}
                />
              )}
              {/* The grantless author's way DOWN. Their tag is out there; the
                  missing grant stops them raising it again, never taking it
                  back. */}
              {!readOnly && lock.kind === "lockedShared" && (
                <Button
                  variant="neutral"
                  size="small"
                  onClick={() => {
                    markDirty();
                    setVisibility("sharedPrivate");
                  }}
                >
                  {t("Make it private")}
                </Button>
              )}
              <p className="flex items-start gap-1.5 text-foreground-muted">
                <span className="mt-px shrink-0" aria-hidden>
                  {visibilityIcon(readOnly ? tag?.sharingVisibility : visibility)}
                </span>
                <span className="body-small">
                  {readOnly
                    ? readOnlyHint(tag?.sharingVisibility, t)
                    : lock.kind === "lockedShared"
                      ? t(
                          "This tag is shared, but your account can’t share tags, so its access level can’t be raised again. You can still make it private.",
                        )
                      : visibilityHint(visibility, t)}
                </span>
              </p>
              {/* Where it came from — the same attribution shape the workout
                  template drawer uses for someone else's template. */}
              {foreignScope && (
                <ProvenanceBadge
                  scope={foreignScope}
                  author={sharedAuthorName(tag)}
                  labelled={!sharedAuthorName(tag)}
                />
              )}
            </div>
          </Card>
        </section>
        )}

        <section data-tour={TOUR_TARGET.tagAppliesTo}>
          <SectionLabel>{t("Applies to")}</SectionLabel>
          <Card>
            {TAG_KINDS.map((kind, i) => (
              <div key={kind}>
                {i > 0 && <RowDivider />}
                <Row
                  label={tagKindLabel(kind, t)}
                  trailing={
                    <Toggle
                      ariaLabel={tagKindLabel(kind, t)}
                      checked={kinds[kind]}
                      disabled={readOnly}
                      onChange={(next) => {
                        markDirty();
                        setKinds((prev) => ({ ...prev, [kind]: next }));
                      }}
                    />
                  }
                />
              </div>
            ))}
          </Card>
        </section>

        {isEdit && !readOnly && (
          <Button
            variant="destructive"
            size="small"
            onClick={() => setConfirmDelete(true)}
            disabled={submitting}
          >
            {t("Delete tag")}
          </Button>
        )}
      </div>

      <ConfirmDialog
        open={confirmDelete}
        destructive
        title={t("Delete tag?")}
        message={t(
          "This permanently deletes this tag and detaches it from everything it’s on. This can’t be undone.",
        )}
        confirmLabel={t("Delete")}
        busy={submitting}
        onConfirm={handleDelete}
        onCancel={() => setConfirmDelete(false)}
      />
    </DrawerView>
  );
}

// A static, right-aligned value for a read-only (public) tag's Row — the
// immutable counterpart to the TextField / SegmentedPicker editors above.
function ReadOnlyValue({ children }: { children: ReactNode }) {
  return (
    <span className="body-normal text-right text-foreground-default">
      {children}
    </span>
  );
}

// The live preview: the tag as the PILL it will be everywhere else — the same
// soft tint `tagPillStyle` gives `<TagPill>` in tables, pickers and chips.
//
// It builds its own pill instead of rendering `<TagPill>` because it shows an
// unsaved DRAFT: a name being typed (or none yet), not an entity with an id and
// a text-content to resolve. The shared style helper is the part that must not
// drift, and that is what it reuses.
function TagPreview({
  name,
  colorCode,
}: {
  name: string;
  colorCode: string;
}) {
  const { t } = useTranslation();
  const label = name.trim();
  return (
    <span
      className={`body-prominent inline-flex max-w-full items-center overflow-hidden rounded-full border px-4 py-1.5 capitalize ${
        label ? "" : "italic opacity-60"
      }`}
      style={tagPillStyle(colorCode)}
    >
      {/* Same trick as TagPill: `truncate` clips at the padding edge, so the
          vertical padding keeps descenders from being sheared. */}
      <span className="min-w-0 truncate py-1 -my-1">
        {label || t("Tag name")}
      </span>
    </span>
  );
}

// A swatch-grid colour picker; each swatch uses the same tinted `tagDotStyle` as
// the tag dots elsewhere (fill + full-colour ring), so the picker previews how
// the tag will read. The selected swatch gets an extra focus ring. `colors` is
// `null` while the server palette loads — a skeleton row stands in so the real
// swatches appear in one shot rather than swapping in over a fallback set.
//
// Left-aligned and full width: it is its own block under the preview, not a
// value crammed into a row's trailing slot, so the swatches wrap into an even
// grid instead of a ragged right-hand cloud.
function ColorPicker({
  colors,
  value,
  onChange,
}: {
  colors: TagColorDto[] | null;
  value: string;
  onChange: (next: string) => void;
}) {
  if (colors === null) {
    return (
      <div className="flex flex-wrap items-center gap-2.5" aria-hidden>
        {Array.from({ length: 8 }).map((_, i) => (
          <span
            key={i}
            className="size-8 animate-pulse rounded-full bg-surface-base-muted"
          />
        ))}
      </div>
    );
  }
  return (
    <div className="flex flex-wrap items-center gap-2.5">
      {colors.map((c) => (
        <button
          key={c.hex}
          type="button"
          onClick={() => onChange(c.hex)}
          aria-label={c.name}
          aria-pressed={value === c.hex}
          className={`size-8 rounded-full border transition ${
            value === c.hex
              ? "ring-2 ring-foreground-default ring-offset-2 ring-offset-surface-drawer"
              : "hover:ring-1 hover:ring-surface-stroke-muted"
          }`}
          style={tagDotStyle(c.hex)}
        />
      ))}
    </div>
  );
}
