import type {
  StepTechStepTemperatureView,
  StepTechStepView,
  SubmitTechStepCorrectionResult,
} from "@batch-cooking/shared";
import { Fragment, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "../../../components/ui/Tooltip";
import { splitDescriptionSegments } from "./highlight-tech-steps";
import { TechStepCorrectionPopover } from "./TechStepCorrectionPopover";
import { type TextSelectionRange, useTextSelection } from "./use-text-selection";

/**
 * Human label for a detected temperature — the °C value, else the gas-mark
 * number, else the qualitative level, each via i18n. Shown in the
 * highlight's Tooltip (`raw` is what stays inline as the highlighted text).
 * `t` is passed in (see `ingredient-label.ts` for the same "plain function,
 * unit-testable" split).
 */
function temperatureLabel(
  temperature: StepTechStepTemperatureView,
  t: (key: string, options?: Record<string, unknown>) => string,
): string {
  if (temperature.celsius !== null) {
    return t("recipes.temperature.celsius", { value: temperature.celsius });
  }
  if (temperature.gasMark !== null) {
    return t("recipes.temperature.gasMark", { value: temperature.gasMark });
  }
  if (temperature.qualitative !== null) {
    return t(`recipes.temperature.qualitative.${temperature.qualitative}`);
  }
  return temperature.raw;
}

/**
 * A recipe step's description, with every detected technique's exact
 * matched words highlighted and given a {@link Tooltip} naming the
 * technique (e.g. hovering/focusing "hacher" in "Hacher les oignons" shows
 * "Hacher") — `RecipeDetailPanel`'s replacement for a bare `<p>{description}</p>`.
 *
 * A match's wider `contextStart`/`contextEnd` clause (see
 * `StepTechStepView`) is deliberately *not* visualized here — only the
 * tight keyword span is highlighted. The backend still computes and
 * persists it (`tech-step-matcher.ts`/`StepTechStep`), and
 * `splitDescriptionByTechSteps` still splits the description around it
 * (`isKeyword: false` context segments), but this component now renders
 * those non-keyword segments as plain text, same as a segment with no
 * technique at all — the visual "wider clause, subtler highlight"
 * treatment (`.step-tech-step-context`) turned out to be more visual noise
 * than useful signal in practice and was turned back off.
 *
 * `techStep.key` resolves its tooltip label through `catalog.techSteps.<key>`
 * i18n, the same pattern every other reference catalog (diets, units, …)
 * uses for its display text. A keyword's `source` (`"auto"` — the
 * classifier — vs `"manual"` — a viewer's correction, applied immediately)
 * gets its own modifier class (`.step-tech-step--manual`), a different
 * color, so the two are visually distinguishable at a glance rather than
 * only via the tooltip text.
 *
 * `editable` (off by default) additionally lets the viewer select text or
 * click an existing highlight to open a {@link TechStepCorrectionPopover} —
 * see `use-text-selection.ts` for how a browser selection is translated
 * into an absolute `[start, end)` span. When `!editable`, every segment
 * renders exactly as before (no extra wrapping elements, no `data-offset`,
 * no click handlers) — this mode is purely additive, not a rewrite of the
 * read-only rendering.
 *
 * Maintains its own local copy of `techSteps` (seeded from the prop, then
 * replaced with whatever `POST .../corrections` returns on a successful
 * submit — see `SubmitTechStepCorrectionResult`'s doc comment,
 * `packages/shared`) so a correction's effect (a new/relabeled/removed
 * highlight) appears immediately, without needing the parent to re-fetch
 * the whole recipe. Resynced whenever the `techSteps` prop itself changes
 * (e.g. the parent reloaded the recipe for an unrelated reason) so this
 * never keeps showing stale local state past that.
 */
export function StepDescription({
  description,
  techSteps,
  editable = false,
  recipeId,
  stepId,
}: {
  description: string;
  techSteps: StepTechStepView[];
  /** Requires `recipeId`/`stepId` when `true` — omit (or leave `false`) for a read-only view with nothing real to correct against yet (e.g. `RecipeDetailPanel`'s `"loaded-draft"` unsaved-preview branch). */
  editable?: boolean;
  recipeId?: number;
  stepId?: number;
}) {
  const { t } = useTranslation();
  const [liveTechSteps, setLiveTechSteps] = useState(techSteps);
  useEffect(() => setLiveTechSteps(techSteps), [techSteps]);

  // Temperatures are attached per technique-clause server-side; for the
  // read-only highlight we only care *where* in the description they are,
  // so flatten them across every entry (deduped by span). `?? []` guards a
  // pre-`temperatures` payload the same way this component already tolerates
  // a match with no `contextStart`/`contextEnd`.
  const temperatures = Array.from(
    new Map(
      liveTechSteps
        .flatMap((techStep) => techStep.temperatures ?? [])
        .map((temperature) => [`${temperature.start}:${temperature.end}`, temperature]),
    ).values(),
  );
  const segments = splitDescriptionSegments(description, liveTechSteps, temperatures);
  const containerRef = useRef<HTMLParagraphElement>(null);
  const { getSelectionRange } = useTextSelection(containerRef);

  const [activeCorrection, setActiveCorrection] = useState<{
    range: TextSelectionRange;
    selectedText: string;
    previousTechStepId: number | null;
  } | null>(null);

  // Routes the *next* text selection to the open `TechStepCorrectionPopover`
  // (as an ingredient/utensil mention span) instead of opening a brand-new
  // correction — set when that popover calls `onRequestSpan`, cleared once
  // `handleMouseUp` resolves the selection below. See
  // `TechStepCorrectionPopover.tsx`'s own doc comment for why this can live
  // entirely alongside the still-visible, still-selectable description
  // rather than needing the popover itself to move/hide.
  const [pendingSpanRequest, setPendingSpanRequest] = useState<"ingredient" | "utensil" | null>(
    null,
  );
  const [resolvedMetadataSpan, setResolvedMetadataSpan] = useState<{
    nonce: number;
    kind: "ingredient" | "utensil";
    range: TextSelectionRange;
    text: string;
  } | null>(null);
  const nextMetadataSpanNonce = useRef(0);

  function closeActiveCorrection() {
    setActiveCorrection(null);
    setPendingSpanRequest(null);
    setResolvedMetadataSpan(null);
  }

  function handleMouseUp() {
    if (!editable) return;
    const range = getSelectionRange();
    if (!range) return;
    if (pendingSpanRequest !== null) {
      nextMetadataSpanNonce.current += 1;
      setResolvedMetadataSpan({
        nonce: nextMetadataSpanNonce.current,
        kind: pendingSpanRequest,
        range,
        text: description.slice(range.start, range.end),
      });
      setPendingSpanRequest(null);
      return;
    }
    setActiveCorrection({
      range,
      selectedText: description.slice(range.start, range.end),
      previousTechStepId: null,
    });
  }

  function handleSubmitted(result: SubmitTechStepCorrectionResult) {
    setLiveTechSteps(result.techSteps);
  }

  // The occurrence `activeCorrection` is currently open for, matched by its
  // exact `[start, end)` (not just `techStep.id` — the same technique can
  // legitimately occur more than once in one description) — whatever
  // ingredients/utensils it already carries seed
  // `TechStepCorrectionPopover`'s own pending lists. `undefined` (not an
  // empty array) for a brand-new selection, same as "nothing to look up
  // yet".
  const activeStepTechStep = activeCorrection
    ? liveTechSteps.find(
        (techStep) =>
          techStep.start === activeCorrection.range.start &&
          techStep.end === activeCorrection.range.end,
      )
    : undefined;

  // Tracks each segment's own absolute start offset into `description` as
  // the map below walks them in order — segments are contiguous and cover
  // the whole description (see `splitDescriptionByTechSteps`'s doc
  // comment), so a running total is exact, no re-derivation needed.
  let offset = 0;

  return (
    <>
      <p ref={containerRef} onMouseUp={handleMouseUp}>
        {segments.map((segment, index) => {
          const start = offset;
          offset += segment.text.length;
          // Captured now, not read as `offset` later inside a click
          // handler below — `offset` keeps mutating for every subsequent
          // segment this same `.map()` pass renders, so a closure
          // referencing it directly would see its *final* value (the end
          // of the whole description) whenever it actually fires, long
          // after render — found via a real correction submitted with
          // `end` far past this segment's own text.
          const end = offset;
          // A segment's own text/techStep don't uniquely identify it (the
          // same word can appear twice in one description) — index is the
          // only thing that does, but this list is fully regenerated from
          // `description`/`liveTechSteps` on every render (never reordered
          // or spliced in place), so using it as part of the key is safe
          // here.
          const key = `${index}-${segment.text}`;

          if (segment.temperature) {
            // A detected temperature mention ("180°C", "feu doux") — its
            // own subtle highlight + a Tooltip with the normalized value.
            // Not correctable (no popover), same as utensils; still gets a
            // `data-offset` in editable mode so a text selection spanning
            // it resolves correctly.
            const label = temperatureLabel(segment.temperature, t);
            const modifier = segment.temperature.qualitative ?? "value";
            return (
              <Tooltip key={key} content={label}>
                <span
                  className={`step-temperature step-temperature--${modifier}`}
                  data-offset={editable ? start : undefined}
                >
                  {segment.text}
                </span>
              </Tooltip>
            );
          }

          if (!segment.techStep || !segment.isKeyword) {
            // Context-only or plain run — rendered as plain text in
            // read-only mode, same as before this component supported
            // `editable` at all (see this component's doc comment for why
            // the wider-clause highlight itself was turned back off).
            // Editable mode still wraps it in a `data-offset` span so a
            // selection starting/ending in plain text resolves correctly.
            if (!editable) return <Fragment key={key}>{segment.text}</Fragment>;
            return (
              <span key={key} data-offset={start}>
                {segment.text}
              </span>
            );
          }

          const techStep = segment.techStep;
          const isManual = segment.source === "manual";
          const tooltipLabel = isManual
            ? t("recipes.techStepCorrection.manualTooltip", {
                technique: t(`catalog.techSteps.${techStep.key}`),
              })
            : t(`catalog.techSteps.${techStep.key}`);
          return (
            <Tooltip key={key} content={tooltipLabel}>
              {/* A real <button>, not a <mark>, so it's natively focusable
                  (keyboard/screen-reader users can reach the tooltip) without
                  fighting the "non-interactive element" a11y lint a bare
                  tabIndex on <mark> would trip — styled to read as inline
                  highlighted text, not as a button (see .step-tech-step). */}
              <button
                type="button"
                className={isManual ? "step-tech-step step-tech-step--manual" : "step-tech-step"}
                data-offset={editable ? start : undefined}
                onClick={
                  editable
                    ? () => {
                        // Clears any in-progress ingredient/utensil
                        // span-selection from whatever correction was open
                        // before — opening a *different* one has nothing
                        // left to resolve that selection into.
                        setPendingSpanRequest(null);
                        setResolvedMetadataSpan(null);
                        setActiveCorrection({
                          range: { start, end },
                          selectedText: segment.text,
                          previousTechStepId: techStep.id,
                        });
                      }
                    : undefined
                }
              >
                {segment.text}
              </button>
            </Tooltip>
          );
        })}
      </p>
      {editable && activeCorrection && recipeId !== undefined && stepId !== undefined && (
        <TechStepCorrectionPopover
          recipeId={recipeId}
          stepId={stepId}
          range={activeCorrection.range}
          selectedText={activeCorrection.selectedText}
          previousTechStepId={activeCorrection.previousTechStepId}
          existingIngredients={activeStepTechStep?.ingredients ?? []}
          existingUtensils={activeStepTechStep?.utensils ?? []}
          resolvedMetadataSpan={resolvedMetadataSpan}
          onRequestSpan={setPendingSpanRequest}
          onClose={closeActiveCorrection}
          onSubmitted={handleSubmitted}
        />
      )}
    </>
  );
}
