"use client";

import { useState } from "react";

import type { MetricReturnDto } from "@enode/core/api/dtos/metrics";
import { findMeasurementDisplayValue } from "@enode/core/training/measurements";
import type {
  RepPhase,
  WorkoutRep,
  WorkoutSet,
} from "@enode/core/training/models";
import {
  compareRepsByCreated,
  pairByPhase,
  phaseOrderOf,
} from "@enode/core/training/rep-order";
import { countedRepCount } from "@enode/core/training/rep-scope";
import { useTranslation } from "@enode/core/translations/hooks";
import { useUnits } from "@enode/core/use-units";
import { InvalidRepBadge } from "@enode/ui/invalid-rep-badge";

import { RepSelectActions } from "./complete-set-rep-select-actions";
import { ChevronDownIcon, ChevronUpIcon } from "./icons";
import { formatMeasurementValue } from "@enode/core/format/measurement";

// Collapsible per-rep card. Summary line shows the tracked count and
// (when any rep is flagged invalid) the invalid count. Expanded body
// lists every rep — tap-to-select toggles the rep into a multi-select;
// the floating action pill below the card surfaces Toggle valid /
// Delete / Done.
export function RepsCard({
  set,
  focusMetrics,
  onRepsChange,
}: {
  set: WorkoutSet;
  focusMetrics: MetricReturnDto[];
  onRepsChange: (reps: WorkoutRep[]) => void;
}) {
  const { t } = useTranslation();
  const [expanded, setExpanded] = useState(false);
  const [selectedIDs, setSelectedIDs] = useState<Set<string>>(new Set());

  // Snap selection back to the empty set whenever the underlying set
  // changes (the parent reseats the editor on a new set), so a stale
  // id list doesn't leak across edits.
  const [prevSetId, setPrevSetId] = useState(set.id);
  if (prevSetId !== set.id) {
    setPrevSetId(set.id);
    setSelectedIDs(new Set());
    setExpanded(false);
  }

  // Time order, NOT `order`: the backend numbers a pair's concentric before
  // its eccentric even though the eccentric happened first, so sorting by
  // `order` time-reverses every pair. `pairByPhase` requires time-ordered
  // input — same sort as the set-review timeline and the technique charts.
  const reps = [...set.reps].sort(compareRepsByCreated);
  // Pair concentric + eccentric reps so the expanded list groups them
  // into bracketed pair-blocks; per-rep selection still works inside.
  const pairs = pairByPhase(reps, (r) => r.phase);
  // Tracked-rep count follows the shared `isCounted` rule (valid concentrics)
  // so it always equals the set's repCount measurement — toggling a rep
  // invalid drops the headline immediately. The invalid count next to it stays
  // across BOTH phases, so the coach still sees everything flagged.
  const summary = t(
    "{count, plural, one {# Tracked rep} other {# Tracked reps}}",
    { count: countedRepCount(reps) },
  );
  const invalidCount = reps.filter((r) => !r.valid).length;

  function toggleSelected(id: string): void {
    setSelectedIDs((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  function clearSelection(): void {
    setSelectedIDs(new Set());
  }

  // Flip `valid` on every selected rep — each rep toggles independently,
  // so a mixed selection ends up still mixed but inverted. Selection
  // stays so the user can chain further actions.
  function handleToggleValid(): void {
    onRepsChange(
      set.reps.map((r) =>
        selectedIDs.has(r.id) ? { ...r, valid: !r.valid } : r,
      ),
    );
  }

  function handleDeleteSelected(): void {
    onRepsChange(set.reps.filter((r) => !selectedIDs.has(r.id)));
    clearSelection();
  }

  return (
    <>
      <div className="overflow-hidden rounded-2xl border border-surface-stroke bg-surface-base">
        <button
          type="button"
          onClick={() => setExpanded((v) => !v)}
          aria-expanded={expanded}
          className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-surface-base-muted"
        >
          <span className="body-prominent">{summary}</span>
          <span className="flex-1" />
          {invalidCount > 0 && (
            <span className="stat-small uppercase text-feedback-error">
              {t("{count} invalid", { count: invalidCount })}
            </span>
          )}
          {expanded ? (
            <ChevronUpIcon className="size-4 text-foreground-muted" />
          ) : (
            <ChevronDownIcon className="size-4 text-foreground-muted" />
          )}
        </button>
        {expanded && (
          <>
            <div className="border-t border-surface-stroke" />
            {/* One shared scroll container for every row — horizontal AND
                vertical — so the fixed-width columns stay aligned across
                rows while they scroll together. The inner list is
                max-content wide (never narrower than the card) so row
                backgrounds and separators span the full scroll width. */}
            <div className="max-h-72 overflow-auto">
              <ul className="w-max min-w-full divide-y divide-surface-stroke">
                {pairs.map((pair) => {
                  const first = pair.concentricFirst
                    ? pair.concentric
                    : pair.eccentric;
                  const second = pair.concentricFirst
                    ? pair.eccentric
                    : pair.concentric;
                  // Pair grouping is conveyed by the shared `#position`
                  // number, the CON / ECC chips and the eccentric row's
                  // diagonal-stripe background — the outer `<ul>`'s
                  // divide-y keeps inter-pair separators while the two
                  // rows of a pair sit flush.
                  return (
                    // Labels come from `phaseOrderOf` (the iOS
                    // concentricOrderValue / eccentricOrderValue
                    // computed-property analogues), so "#1 CON" is
                    // the 1st concentric and "#1 ECC" is the 1st
                    // eccentric — they only coincide when the pair
                    // is a real ecc-leads pair, and they diverge for
                    // con-led / standalone sets.
                    <li key={pair.position}>
                      {first && (
                        <RepRow
                          rep={first}
                          set={set}
                          focusMetrics={focusMetrics}
                          orderValue={phaseOrderOf(reps, first)}
                          selected={selectedIDs.has(first.id)}
                          onTap={() => toggleSelected(first.id)}
                        />
                      )}
                      {second && (
                        <RepRow
                          rep={second}
                          set={set}
                          focusMetrics={focusMetrics}
                          orderValue={phaseOrderOf(reps, second)}
                          selected={selectedIDs.has(second.id)}
                          onTap={() => toggleSelected(second.id)}
                        />
                      )}
                    </li>
                  );
                })}
              </ul>
            </div>
          </>
        )}
      </div>
      {selectedIDs.size > 0 && (
        <div className="flex justify-center pt-1">
          <RepSelectActions
            onToggleValid={handleToggleValid}
            onDelete={handleDeleteSelected}
            onDone={clearSelection}
          />
        </div>
      )}
    </>
  );
}

// One rep in the expanded list. Layout: time + #order + phase chip in
// front, then one fixed-width column per focus metric so values line up
// across rows — overflow is handled by the card's shared horizontal
// scroll container, not by shrinking the cells. The "Invalid" badge is
// a zero-width sticky overlay that floats over the row, pinned to the
// visible right edge while the columns scroll underneath it. Valid reps
// carry no badge per spec.
//
// `orderValue` is the 1-based position within the rep's same-phase
// sort (iOS `concentricOrderValue` / `eccentricOrderValue` parity).
function RepRow({
  rep,
  set,
  focusMetrics,
  orderValue,
  selected,
  onTap,
}: {
  rep: WorkoutRep;
  set: WorkoutSet;
  focusMetrics: MetricReturnDto[];
  orderValue: number;
  selected: boolean;
  onTap: () => void;
}) {
  // Subtle diagonal-stripe background for eccentric rows — same
  // geometry as the chart bars (2 px / 4 px @ -45°), but in
  // `currentColor` at ~8 % so the pattern reads against the row's
  // surface without competing with selection / hover tints (those
  // use background-color and show through the gradient's transparent
  // bands).
  const eccentricRowStyle =
    rep.phase === "eccentric"
      ? {
          backgroundImage:
            "repeating-linear-gradient(-45deg, color-mix(in srgb, currentColor 8%, transparent) 0, color-mix(in srgb, currentColor 8%, transparent) 2px, transparent 2px, transparent 6px)",
        }
      : undefined;
  return (
    <button
      type="button"
      onClick={onTap}
      aria-pressed={selected}
      className={`flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors ${
        selected ? "bg-foreground-red-orange/10" : "hover:bg-surface-base-muted"
      } ${
        // Invalid rows mute their whole content (order number + values), so the
        // row reads as excluded even where the badge scrolls out of view.
        rep.valid ? "" : "text-foreground-muted"
      }`}
      style={eccentricRowStyle}
    >
      <div className="flex shrink-0 items-center gap-3">
        <span className="body-small tabular-nums text-foreground-muted">
          {formatRepTime(rep.created)}
        </span>
        <span className="body-prominent min-w-10 tabular-nums">
          #{orderValue}
        </span>
        <span className="stat-small uppercase text-foreground-muted">
          {phaseLabel(rep.phase)}
        </span>
      </div>
      <div className="flex flex-1 items-center gap-2">
        {focusMetrics.map((metric) => (
          <RepFocusValue
            key={metric.id ?? metric.key}
            rep={rep}
            set={set}
            metric={metric}
          />
        ))}
      </div>
      {!rep.valid && (
        // Zero-width sticky anchor: takes no column space, sticks 16 px
        // (the row padding) off the scrollport's right edge, and the
        // badge hangs off it leftwards as a floating chip above the
        // scrolling columns.
        <span className="sticky right-4 z-10 w-0 shrink-0 self-stretch">
          <InvalidRepBadge className="absolute right-0 top-1/2 -translate-y-1/2 shadow-sm" />
        </span>
      )}
    </button>
  );
}

// One focus-metric cell in a rep row — the rep's load-factor-scaled
// value plus the metric's unit. Renders an EMPTY cell when the rep
// carries no measurement for the metric (e.g. force/power on an
// eccentric rep); the equal-width column keeps alignment either way.
function RepFocusValue({
  rep,
  set,
  metric,
}: {
  rep: WorkoutRep;
  set: WorkoutSet;
  metric: MetricReturnDto;
}) {
  const { formatMetric } = useUnits();
  const value = metric.id
    ? findMeasurementDisplayValue(rep.measurements, metric.id, set)
    : null;
  const fmt = value != null ? formatMetric(value, metric) : null;
  // Fixed-width column so values line up across rows; a row that would
  // overflow scrolls horizontally with the whole list instead of
  // shrinking or overlapping its neighbours. A missing measurement
  // renders an empty column (no dash) that still holds its slot, so an
  // ECC row's blank force/power keeps the columns after it aligned.
  return (
    <span className="body-prominent w-24 shrink-0 tabular-nums whitespace-nowrap">
      {fmt?.text ?? (value != null ? formatMeasurementValue(value) : "")}
      {fmt?.unit && (
        <span className="ml-1 stat-small uppercase text-foreground-muted">
          {fmt.unit}
        </span>
      )}
    </span>
  );
}

// Short phase-chip label rendered next to the rep order number. Three
// letters fit the tabular layout without forcing the focus-metric band
// narrower on tight screens.
function phaseLabel(phase: RepPhase): string {
  return phase === "eccentric" ? "ECC" : "CON";
}

function formatRepTime(ms: number): string {
  const d = new Date(ms);
  if (Number.isNaN(d.getTime())) return "—";
  // Local time, second precision — these are recent local timestamps
  // from the in-session sensor stream, not workout-schedule UTC times.
  return d.toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false,
  });
}
