// ActivityBar — the ONE progress bar for the authoring app.
//
// ★ WHY IT EXISTS. Omar, 2026-09-11: "Show a progress bar when uploading a video
//   from the layout editor [and] generating AI questions in Assessments … If a
//   progress-bar component already exists, reuse it. Use the same component,
//   styling and behaviour across all sections. Do not create separate designs
//   for these activities."
//
//   Nothing existed to reuse. There were THREE hand-rolled bars, each private to
//   the file that drew it — `IngestProgress` (surface-sources.jsx, and its data
//   is mock), `PreviewProgress` (preview-modal.jsx, hard-wired to Preview's own
//   four stages and painted for a black backdrop) and `BuildStepper`'s inner bar
//   (surface-export.jsx). Adding a fourth would have been the thing he asked us
//   not to do, so this is the shared one, and its look is the look those already
//   share: 4px, `--accent` on `--surface-inset`, 400ms ease
//   (`feedback_one_door_per_field_reuse_existing_style`).
//
// ── ★ THE HONESTY RULE THIS COMPONENT ENFORCES ──────────────────────────────
//
// A percentage is a CLAIM. `value` is accepted ONLY as a real measurement — bytes
// transferred out of bytes total, units finished out of units total. When nothing
// can be measured, callers pass no `value` at all and get the indeterminate
// sheen, which says "working" without saying how far.
//
// This is not decoration. `surface-export.jsx`'s stepper already animates a
// percentage off a wall-clock timer against a hard-coded 1200ms, and
// `fe-preview-progress.test.ts` exists because a fabricated position is worse
// than no position: it reads as measured and is not
// (`feedback_a_plausible_placeholder_is_worse_than_an_obvious_one`). So this
// component has no "fake it smoothly" mode and must never grow one — there is a
// test named after that.
//
// ── Tones ───────────────────────────────────────────────────────────────────
// `tone="dark"` is for the video preview box, which paints on near-black and
// where the surrounding text is already white. It is the only reason a literal
// colour appears below; every other colour is a design token.

/**
 * @param {object}  props
 * @param {number} [props.value]    0..1, a MEASURED fraction. Omit when unknown.
 * @param {string}  props.label     What is happening, in the author's words.
 * @param {string} [props.detail]   Secondary line — "3 of 7", "12.4 MB", "8s".
 * @param {'running'|'done'|'error'} [props.state]
 * @param {string} [props.error]    Shown when state === 'error'.
 * @param {'light'|'dark'} [props.tone]
 * @param {boolean} [props.compact] Bar + one line only; for tight slots.
 */
function ActivityBar({
  value,
  label,
  detail,
  state = 'running',
  error,
  tone = 'light',
  compact = false,
}) {
  const dark = tone === 'dark';
  // A measurement or nothing. `Number.isFinite` rather than a truthiness test,
  // because 0 is a real and meaningful measurement — "nothing has transferred
  // yet" — and `value && ...` would silently turn it into "unknown".
  const measured = typeof value === 'number' && Number.isFinite(value);
  const pct = measured ? Math.max(0, Math.min(1, value)) * 100 : 100;
  const done = state === 'done';
  const failed = state === 'error';

  const track = dark ? 'rgba(255,255,255,0.14)' : 'var(--surface-inset)';
  const fill = failed ? 'var(--error)' : done ? 'var(--accent)' : 'var(--accent)';
  const labelColor = dark ? 'rgba(255,255,255,0.88)' : 'var(--text)';
  const detailColor = dark ? 'rgba(255,255,255,0.6)' : 'var(--text-muted)';

  return (
    <div
      data-activity-bar={state}
      data-activity-measured={measured ? 'true' : 'false'}
      // Announced to a screen reader as it changes, rather than only drawn.
      role="status"
      aria-live="polite"
      style={{ width: '100%' }}
    >
      <style>{`@keyframes dz-activity-sheen {
        0% { transform: translateX(-60%); } 100% { transform: translateX(160%); }
      }`}</style>

      {failed ? (
        <div
          style={{
            fontSize: 12,
            color: 'var(--error)',
            display: 'flex',
            alignItems: 'center',
            gap: 6,
            lineHeight: 1.45,
          }}
        >
          <I.AlertTriangle size={13} />
          <span>{error || 'It did not finish — try again'}</span>
        </div>
      ) : (
        <>
          <div
            style={{
              display: 'flex',
              alignItems: 'baseline',
              justifyContent: 'space-between',
              gap: 10,
              marginBottom: 6,
            }}
          >
            <span
              style={{
                fontSize: compact ? 11.5 : 12.5,
                fontWeight: 600,
                color: labelColor,
                display: 'flex',
                alignItems: 'center',
                gap: 6,
              }}
            >
              {done ? (
                <I.Check size={13} style={{ color: 'var(--accent)' }} />
              ) : (
                <I.Loader
                  size={13}
                  className="spin"
                  style={{ color: dark ? 'rgba(255,255,255,0.8)' : 'var(--accent)' }}
                />
              )}
              {label}
            </span>
            {/* The measured figure sits beside the label, never inside the bar —
                a number drawn on a moving fill is unreadable at 4px. */}
            {detail || measured ? (
              <span
                style={{
                  fontSize: 11.5,
                  color: detailColor,
                  fontVariantNumeric: 'tabular-nums',
                  whiteSpace: 'nowrap',
                }}
              >
                {detail}
                {detail && measured ? ' · ' : ''}
                {measured ? `${Math.round(pct)}%` : ''}
              </span>
            ) : null}
          </div>

          <div
            style={{
              height: 4,
              background: track,
              borderRadius: 2,
              overflow: 'hidden',
            }}
          >
            <div
              style={{
                height: '100%',
                width: `${pct}%`,
                background: fill,
                transition: 'width 400ms ease',
                position: 'relative',
                overflow: 'hidden',
              }}
            >
              {/* The sheen means "still working". It runs on an unmeasured bar —
                  which is full-width, so the motion IS the whole signal — and on a
                  measured one that has not finished. It stops on `done`, because a
                  finished bar that keeps moving reads as still running. */}
              {done ? null : (
                <div
                  style={{
                    position: 'absolute',
                    inset: 0,
                    background:
                      'linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent)',
                    width: '40%',
                    animation: 'dz-activity-sheen 1.4s ease-in-out infinite',
                  }}
                />
              )}
            </div>
          </div>
        </>
      )}
    </div>
  );
}

/**
 * Seconds elapsed since `active` last became true, ticking while it stays true.
 *
 * A MEASURED number, and deliberately not an estimate or a countdown: nothing
 * here knows how long an upload or a model call will take, and inventing a
 * remaining time is the same lie as inventing a percentage. Lifted from the
 * preview modal's own timer so both count the same way.
 */
function useElapsedSeconds(active) {
  const [elapsed, setElapsed] = React.useState(0);
  React.useEffect(() => {
    if (!active) {
      setElapsed(0);
      return undefined;
    }
    const startedAt = Date.now();
    const id = setInterval(
      () => setElapsed(Math.floor((Date.now() - startedAt) / 1000)),
      250,
    );
    return () => clearInterval(id);
  }, [active]);
  return elapsed;
}

/** "4.2 MB" / "812 KB" — for an upload's secondary line. */
function formatBytes(n) {
  if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) return '';
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}

Object.assign(window, { ActivityBar, useElapsedSeconds, formatBytes });
