/**
 * GIF Enhanced Renderer
 *
 * Draws visual affordances (click indicators, drag paths, labels) onto a canvas
 * before encoding frames. This keeps the offscreen document focused on encoding
 * while the background capture pipeline handles compositing.
 *
 * Coordinates are expected to be in viewport CSS pixels. If a caller provides
 * screenshot-space coordinates, it should convert them to viewport space first.
 */

// ============================================================================
// Types
// ============================================================================

export type ActionType =
  | 'click'
  | 'double_click'
  | 'triple_click'
  | 'right_click'
  | 'drag'
  | 'scroll'
  | 'type'
  | 'key'
  | 'navigate'
  | 'hover'
  | 'fill'
  | 'annotation'
  | 'other';

export type CoordinateSpace = 'viewport' | 'screenshot';

export interface Point {
  x: number;
  y: number;
}

export interface ActionMetadata {
  type: ActionType;
  coordinates?: Point;
  startCoordinates?: Point;
  endCoordinates?: Point;
  text?: string;
  url?: string;
  ref?: string;

  // Enhanced rendering hints
  label?: string;
  coordinateSpace?: CoordinateSpace;
  timestampMs?: number;
}

export interface GifEnhancedRenderingConfig {
  enabled?: boolean;

  clickIndicators?: {
    enabled?: boolean;
    color?: string;
    fillColor?: string;
    radiusPx?: number;
    lineWidthPx?: number;
    durationMs?: number;
    // Capture-side animation hints (auto-capture mode only)
    animationFrames?: number;
    animationIntervalMs?: number;
    animationFrameDelayCs?: number;
  };

  dragPaths?: {
    enabled?: boolean;
    color?: string;
    lineWidthPx?: number;
    durationMs?: number;
    arrowSizePx?: number;
    dash?: number[];
    startDotRadiusPx?: number;
    endDotRadiusPx?: number;
  };

  labels?: {
    enabled?: boolean;
    mode?: 'action' | 'annotation' | 'both';
    showForClicks?: boolean;
    font?: string;
    maxLength?: number;
    durationMs?: number;
    backgroundColor?: string;
    borderColor?: string;
    textColor?: string;
    paddingX?: number;
    paddingY?: number;
    radiusPx?: number;
    offsetPx?: number;
  };
}

// ============================================================================
// Resolved Config Types
// ============================================================================

export interface ResolvedClickIndicatorConfig {
  enabled: boolean;
  color: string;
  fillColor: string;
  radiusPx: number;
  lineWidthPx: number;
  durationMs: number;
  animationFrames: number;
  animationIntervalMs: number;
  animationFrameDelayCs: number;
}

export interface ResolvedDragPathConfig {
  enabled: boolean;
  color: string;
  lineWidthPx: number;
  durationMs: number;
  arrowSizePx: number;
  dash: number[];
  startDotRadiusPx: number;
  endDotRadiusPx: number;
}

export interface ResolvedLabelsConfig {
  enabled: boolean;
  mode: 'action' | 'annotation' | 'both';
  showForClicks: boolean;
  font: string;
  maxLength: number;
  durationMs: number;
  backgroundColor: string;
  borderColor: string;
  textColor: string;
  paddingX: number;
  paddingY: number;
  radiusPx: number;
  offsetPx: number;
}

export interface ResolvedGifEnhancedRenderingConfig {
  enabled: boolean;
  clickIndicators: ResolvedClickIndicatorConfig;
  dragPaths: ResolvedDragPathConfig;
  labels: ResolvedLabelsConfig;
}

export interface ActionEvent {
  action: ActionMetadata;
  atMs: number;
}

export interface CapturePlan {
  frames: number;
  intervalMs: number;
  delayCs: number;
}

export interface RenderGifEnhancedOverlaysParams {
  ctx: OffscreenCanvasRenderingContext2D;
  outputWidth: number;
  outputHeight: number;
  viewportWidth: number;
  viewportHeight: number;
  nowMs: number;
  events: readonly ActionEvent[];
  config: ResolvedGifEnhancedRenderingConfig;
}

// ============================================================================
// Constants
// ============================================================================

const CLICK_ACTIONS: readonly ActionType[] = [
  'click',
  'double_click',
  'triple_click',
  'right_click',
];

// ============================================================================
// Utility Functions
// ============================================================================

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

function normalizePositiveNumber(
  value: unknown,
  fallback: number,
  min: number,
  max: number,
): number {
  if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
  return clamp(value, min, max);
}

function normalizePositiveInt(value: unknown, fallback: number, min: number, max: number): number {
  if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
  return clamp(Math.floor(value), min, max);
}

function normalizeDash(value: unknown, fallback: number[]): number[] {
  if (!Array.isArray(value)) return fallback;
  const nums = value.filter((n) => typeof n === 'number' && Number.isFinite(n) && n > 0);
  return nums.length >= 2 ? (nums as number[]) : fallback;
}

function easeOutCubic(t: number): number {
  const x = clamp(t, 0, 1);
  return 1 - Math.pow(1 - x, 3);
}

function projectPoint(
  point: Point,
  viewportWidth: number,
  viewportHeight: number,
  outputWidth: number,
  outputHeight: number,
): Point | null {
  if (
    typeof point.x !== 'number' ||
    typeof point.y !== 'number' ||
    !Number.isFinite(point.x) ||
    !Number.isFinite(point.y)
  ) {
    return null;
  }

  const vw = viewportWidth > 0 ? viewportWidth : outputWidth;
  const vh = viewportHeight > 0 ? viewportHeight : outputHeight;

  return {
    x: (point.x / vw) * outputWidth,
    y: (point.y / vh) * outputHeight,
  };
}

function buildRoundedRectPath(
  ctx: OffscreenCanvasRenderingContext2D,
  x: number,
  y: number,
  width: number,
  height: number,
  radius: number,
): void {
  const r = Math.max(0, Math.min(radius, Math.min(width, height) / 2));
  const x2 = x + width;
  const y2 = y + height;

  ctx.moveTo(x + r, y);
  ctx.arcTo(x2, y, x2, y2, r);
  ctx.arcTo(x2, y2, x, y2, r);
  ctx.arcTo(x, y2, x, y, r);
  ctx.arcTo(x, y, x2, y, r);
}

function truncate(text: string, maxLength: number): string {
  const trimmed = text.trim();
  if (trimmed.length <= maxLength) return trimmed;
  return `${trimmed.slice(0, Math.max(0, maxLength - 1))}…`;
}

// ============================================================================
// Label Resolution
// ============================================================================

function resolveActionLabel(action: ActionMetadata, cfg: ResolvedLabelsConfig): string | null {
  const explicit = typeof action.label === 'string' ? action.label.trim() : '';
  const isExplicit = explicit.length > 0;

  const mode = cfg.mode;
  const canShowAction = mode === 'action' || mode === 'both';
  const canShowAnnotation = mode === 'annotation' || mode === 'both';

  if ((action.type === 'annotation' || isExplicit) && canShowAnnotation) {
    const labelText = explicit || (typeof action.text === 'string' ? action.text.trim() : '');
    return labelText.length > 0 ? truncate(labelText, cfg.maxLength) : null;
  }

  if (!canShowAction) return null;

  switch (action.type) {
    case 'click':
    case 'double_click':
    case 'triple_click':
    case 'right_click':
      if (!cfg.showForClicks) return null;
      return action.type.replace('_', ' ').toUpperCase();
    case 'drag':
      return 'DRAG';
    case 'scroll':
      return 'SCROLL';
    case 'hover':
      return 'HOVER';
    case 'navigate': {
      if (!action.url) return 'NAVIGATE';
      try {
        const host = new URL(action.url).hostname;
        return host ? `→ ${host}` : 'NAVIGATE';
      } catch {
        return 'NAVIGATE';
      }
    }
    case 'type': {
      const content = typeof action.text === 'string' ? action.text : '';
      return content.trim().length > 0 ? `TYPE "${truncate(content, cfg.maxLength)}"` : 'TYPE';
    }
    case 'key': {
      const content = typeof action.text === 'string' ? action.text : '';
      return content.trim().length > 0 ? `KEY [${truncate(content, cfg.maxLength)}]` : 'KEY';
    }
    case 'fill': {
      const content = typeof action.text === 'string' ? action.text : '';
      return content.trim().length > 0 ? `FILL "${truncate(content, cfg.maxLength)}"` : 'FILL';
    }
    default:
      return null;
  }
}

function resolveAnchorPoint(action: ActionMetadata): Point | null {
  if (action.type === 'drag') {
    return action.endCoordinates || action.coordinates || action.startCoordinates || null;
  }
  return action.coordinates || action.endCoordinates || action.startCoordinates || null;
}

// ============================================================================
// Drawing Functions
// ============================================================================

function drawClickIndicator(
  ctx: OffscreenCanvasRenderingContext2D,
  x: number,
  y: number,
  progress: number,
  type: ActionType,
  cfg: ResolvedClickIndicatorConfig,
): void {
  const t = clamp(progress, 0, 1);
  const eased = easeOutCubic(t);

  const base = cfg.radiusPx;
  const radius = base * (0.35 + 0.95 * eased);
  const alpha = 1 - eased;

  ctx.save();
  ctx.globalAlpha = alpha;

  ctx.lineWidth = cfg.lineWidthPx;
  ctx.strokeStyle = cfg.color;
  ctx.fillStyle = cfg.fillColor;

  ctx.shadowColor = 'rgba(0, 0, 0, 0.25)';
  ctx.shadowBlur = 8;

  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2);
  ctx.stroke();

  ctx.shadowBlur = 0;

  if (type === 'double_click' || type === 'triple_click') {
    ctx.globalAlpha = 1;
    ctx.fillStyle = cfg.color;
    ctx.font = `700 ${Math.max(10, Math.round(base * 0.6))}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(type === 'double_click' ? '2×' : '3×', x, y);
  } else {
    ctx.beginPath();
    ctx.arc(x, y, Math.max(2, base * 0.16), 0, Math.PI * 2);
    ctx.fill();
  }

  ctx.restore();
}

function drawArrowHead(
  ctx: OffscreenCanvasRenderingContext2D,
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  size: number,
): void {
  const dx = x2 - x1;
  const dy = y2 - y1;
  const len = Math.hypot(dx, dy);
  if (!Number.isFinite(len) || len < 1) return;

  const ux = dx / len;
  const uy = dy / len;
  const px = -uy;
  const py = ux;

  const headLen = size;
  const headWidth = size * 0.65;

  const backX = x2 - ux * headLen;
  const backY = y2 - uy * headLen;

  ctx.beginPath();
  ctx.moveTo(x2, y2);
  ctx.lineTo(backX + px * headWidth, backY + py * headWidth);
  ctx.lineTo(backX - px * headWidth, backY - py * headWidth);
  ctx.closePath();
  ctx.fill();
}

function drawDragPath(
  ctx: OffscreenCanvasRenderingContext2D,
  start: Point,
  end: Point,
  progress: number,
  cfg: ResolvedDragPathConfig,
): void {
  const t = clamp(progress, 0, 1);
  const alpha = 1 - easeOutCubic(t);

  ctx.save();
  ctx.globalAlpha = alpha;

  ctx.strokeStyle = cfg.color;
  ctx.fillStyle = cfg.color;
  ctx.lineWidth = cfg.lineWidthPx;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  ctx.setLineDash(cfg.dash);

  ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
  ctx.shadowBlur = 6;

  ctx.beginPath();
  ctx.moveTo(start.x, start.y);
  ctx.lineTo(end.x, end.y);
  ctx.stroke();

  ctx.setLineDash([]);
  ctx.shadowBlur = 0;

  ctx.beginPath();
  ctx.arc(start.x, start.y, cfg.startDotRadiusPx, 0, Math.PI * 2);
  ctx.fill();

  ctx.beginPath();
  ctx.arc(end.x, end.y, cfg.endDotRadiusPx, 0, Math.PI * 2);
  ctx.fill();

  drawArrowHead(ctx, start.x, start.y, end.x, end.y, cfg.arrowSizePx);

  ctx.restore();
}

function drawLabelPill(
  ctx: OffscreenCanvasRenderingContext2D,
  text: string,
  anchor: Point | null,
  alpha: number,
  cfg: ResolvedLabelsConfig,
  outputWidth: number,
  outputHeight: number,
): void {
  ctx.save();
  ctx.globalAlpha = clamp(alpha, 0, 1);

  ctx.font = cfg.font;
  ctx.textAlign = 'left';
  ctx.textBaseline = 'middle';

  const metrics = ctx.measureText(text);
  const ascent = Number.isFinite(metrics.actualBoundingBoxAscent)
    ? metrics.actualBoundingBoxAscent
    : 10;
  const descent = Number.isFinite(metrics.actualBoundingBoxDescent)
    ? metrics.actualBoundingBoxDescent
    : 4;
  const textHeight = ascent + descent;
  const pillWidth = Math.ceil(metrics.width + cfg.paddingX * 2);
  const pillHeight = Math.ceil(textHeight + cfg.paddingY * 2);

  const margin = 4;
  const ax = anchor?.x ?? margin;
  const ay = anchor?.y ?? margin;

  let x = ax + cfg.offsetPx;
  let y = ay - pillHeight / 2;

  if (x + pillWidth > outputWidth - margin) x = ax - cfg.offsetPx - pillWidth;
  if (y < margin) y = ay + cfg.offsetPx;
  if (y + pillHeight > outputHeight - margin) y = outputHeight - margin - pillHeight;

  x = clamp(x, margin, Math.max(margin, outputWidth - margin - pillWidth));
  y = clamp(y, margin, Math.max(margin, outputHeight - margin - pillHeight));

  ctx.fillStyle = cfg.backgroundColor;
  ctx.strokeStyle = cfg.borderColor;
  ctx.lineWidth = 1;

  ctx.beginPath();
  buildRoundedRectPath(ctx, x, y, pillWidth, pillHeight, cfg.radiusPx);
  ctx.fill();
  ctx.stroke();

  ctx.fillStyle = cfg.textColor;
  ctx.fillText(text, x + cfg.paddingX, y + pillHeight / 2);

  ctx.restore();
}

// ============================================================================
// Schema Input Normalization
// ============================================================================

/**
 * External schema input type that supports both shorthand (boolean) and full config.
 * This maps to what users pass via the MCP tool schema.
 */
interface SchemaEnhancedRenderingInput {
  // Global toggle (Schema allows `true` to enable all defaults)
  enabled?: boolean;

  // Sub-configs can be boolean (enable/disable) or object (custom config)
  clickIndicators?:
    | boolean
    | {
        enabled?: boolean;
        // Schema aliases (from tools.ts)
        color?: string;
        radius?: number; // alias for radiusPx
        animationDurationMs?: number; // alias for durationMs
        animationFrames?: number;
        animationIntervalMs?: number;
      };

  dragPaths?:
    | boolean
    | {
        enabled?: boolean;
        color?: string;
        lineWidth?: number; // alias for lineWidthPx
        lineDash?: number[]; // alias for dash
        arrowSize?: number; // alias for arrowSizePx
      };

  labels?:
    | boolean
    | {
        enabled?: boolean;
        font?: string;
        textColor?: string;
        bgColor?: string; // alias for backgroundColor
        padding?: number; // alias for paddingX/paddingY
        borderRadius?: number; // alias for radiusPx
        offset?: { x?: number; y?: number } | number; // alias for offsetPx
      };

  durationMs?: number; // global fallback duration for all overlays
}

function normalizeSchemaInput(raw: unknown): GifEnhancedRenderingConfig | undefined {
  // Handle `true` shorthand - enable with all defaults
  if (raw === true) {
    return { enabled: true };
  }

  // Handle `false` or falsy
  if (!raw || typeof raw !== 'object') {
    return undefined;
  }

  const input = raw as SchemaEnhancedRenderingInput;
  const result: GifEnhancedRenderingConfig = {};

  // Global enabled
  result.enabled = input.enabled ?? true; // If object passed, default to enabled

  // Global duration fallback
  const globalDuration = typeof input.durationMs === 'number' ? input.durationMs : undefined;

  // Normalize clickIndicators
  if (input.clickIndicators === false) {
    result.clickIndicators = { enabled: false };
  } else if (input.clickIndicators === true) {
    result.clickIndicators = { enabled: true };
  } else if (typeof input.clickIndicators === 'object') {
    const ci = input.clickIndicators;
    result.clickIndicators = {
      enabled: ci.enabled ?? true,
      color: ci.color,
      radiusPx: ci.radius,
      durationMs: ci.animationDurationMs ?? globalDuration,
      animationFrames: ci.animationFrames,
      animationIntervalMs: ci.animationIntervalMs,
    };
  }

  // Normalize dragPaths
  if (input.dragPaths === false) {
    result.dragPaths = { enabled: false };
  } else if (input.dragPaths === true) {
    result.dragPaths = { enabled: true };
  } else if (typeof input.dragPaths === 'object') {
    const dp = input.dragPaths;
    result.dragPaths = {
      enabled: dp.enabled ?? true,
      color: dp.color,
      lineWidthPx: dp.lineWidth,
      dash: dp.lineDash,
      arrowSizePx: dp.arrowSize,
      durationMs: globalDuration,
    };
  }

  // Normalize labels
  if (input.labels === false) {
    result.labels = { enabled: false };
  } else if (input.labels === true) {
    result.labels = { enabled: true };
  } else if (typeof input.labels === 'object') {
    const lb = input.labels;
    const offset = lb.offset;
    const offsetPx =
      typeof offset === 'number' ? offset : typeof offset === 'object' ? offset.x : undefined;
    result.labels = {
      enabled: lb.enabled ?? true,
      font: lb.font,
      textColor: lb.textColor,
      backgroundColor: lb.bgColor,
      paddingX: typeof lb.padding === 'number' ? lb.padding : undefined,
      paddingY: typeof lb.padding === 'number' ? lb.padding : undefined,
      radiusPx: lb.borderRadius,
      offsetPx,
      durationMs: globalDuration,
    };
  }

  return result;
}

// ============================================================================
// Config Resolution
// ============================================================================

export function resolveGifEnhancedRenderingConfig(
  input?: GifEnhancedRenderingConfig | unknown,
): ResolvedGifEnhancedRenderingConfig {
  // Normalize schema input (handles `true`, boolean sub-configs, field aliases)
  const normalized = normalizeSchemaInput(input) ?? (input as GifEnhancedRenderingConfig);
  const enabled = normalized?.enabled ?? false;

  const clickIntervalMs = normalizePositiveInt(
    normalized?.clickIndicators?.animationIntervalMs,
    80,
    20,
    500,
  );
  const clickDelayCsFallback = Math.max(1, Math.round(clickIntervalMs / 10));

  return {
    enabled,
    clickIndicators: {
      enabled: normalized?.clickIndicators?.enabled ?? true,
      color: normalized?.clickIndicators?.color ?? '#FF6A00',
      fillColor: normalized?.clickIndicators?.fillColor ?? 'rgba(255, 106, 0, 0.18)',
      radiusPx: normalizePositiveNumber(normalized?.clickIndicators?.radiusPx, 18, 4, 96),
      lineWidthPx: normalizePositiveNumber(normalized?.clickIndicators?.lineWidthPx, 3, 1, 16),
      durationMs: normalizePositiveInt(normalized?.clickIndicators?.durationMs, 520, 120, 5000),
      animationFrames: normalizePositiveInt(normalized?.clickIndicators?.animationFrames, 3, 1, 8),
      animationIntervalMs: clickIntervalMs,
      animationFrameDelayCs: normalizePositiveInt(
        normalized?.clickIndicators?.animationFrameDelayCs,
        clickDelayCsFallback,
        1,
        100,
      ),
    },
    dragPaths: {
      enabled: normalized?.dragPaths?.enabled ?? true,
      color: normalized?.dragPaths?.color ?? '#FF2D55',
      lineWidthPx: normalizePositiveNumber(normalized?.dragPaths?.lineWidthPx, 4, 1, 20),
      durationMs: normalizePositiveInt(normalized?.dragPaths?.durationMs, 1000, 120, 8000),
      arrowSizePx: normalizePositiveNumber(normalized?.dragPaths?.arrowSizePx, 10, 4, 40),
      dash: normalizeDash(normalized?.dragPaths?.dash, [10, 8]),
      startDotRadiusPx: normalizePositiveNumber(normalized?.dragPaths?.startDotRadiusPx, 4, 2, 24),
      endDotRadiusPx: normalizePositiveNumber(normalized?.dragPaths?.endDotRadiusPx, 5, 2, 24),
    },
    labels: {
      enabled: normalized?.labels?.enabled ?? false,
      mode: normalized?.labels?.mode ?? 'both',
      showForClicks: normalized?.labels?.showForClicks ?? false,
      font:
        normalized?.labels?.font ??
        '600 13px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',
      maxLength: normalizePositiveInt(normalized?.labels?.maxLength, 48, 8, 200),
      durationMs: normalizePositiveInt(normalized?.labels?.durationMs, 1200, 120, 12000),
      backgroundColor: normalized?.labels?.backgroundColor ?? 'rgba(0, 0, 0, 0.72)',
      borderColor: normalized?.labels?.borderColor ?? 'rgba(255, 255, 255, 0.14)',
      textColor: normalized?.labels?.textColor ?? '#FFFFFF',
      paddingX: normalizePositiveNumber(normalized?.labels?.paddingX, 10, 2, 40),
      paddingY: normalizePositiveNumber(normalized?.labels?.paddingY, 6, 2, 30),
      radiusPx: normalizePositiveNumber(normalized?.labels?.radiusPx, 10, 0, 30),
      offsetPx: normalizePositiveNumber(normalized?.labels?.offsetPx, 12, 0, 80),
    },
  };
}

// ============================================================================
// Capture Plan
// ============================================================================

export function resolveCapturePlanForAction(
  config: ResolvedGifEnhancedRenderingConfig,
  action: ActionMetadata | undefined,
  defaultFrameDelayCs: number,
): CapturePlan {
  const base: CapturePlan = { frames: 1, intervalMs: 0, delayCs: defaultFrameDelayCs };
  if (!config.enabled || !action) return base;

  if (config.clickIndicators.enabled && CLICK_ACTIONS.includes(action.type)) {
    const frames = config.clickIndicators.animationFrames;
    if (frames > 1) {
      return {
        frames,
        intervalMs: config.clickIndicators.animationIntervalMs,
        delayCs: config.clickIndicators.animationFrameDelayCs,
      };
    }
  }

  return base;
}

// ============================================================================
// Main Render Function
// ============================================================================

export function renderGifEnhancedOverlays(params: RenderGifEnhancedOverlaysParams): void {
  const { ctx, outputWidth, outputHeight, viewportWidth, viewportHeight, nowMs, events, config } =
    params;

  if (!config.enabled || events.length === 0) return;

  const clickCfg = config.clickIndicators;
  const dragCfg = config.dragPaths;
  const labelCfg = config.labels;

  for (const event of events) {
    const ageMs = nowMs - event.atMs;
    if (!Number.isFinite(ageMs) || ageMs < 0) continue;

    const action = event.action;

    if (clickCfg.enabled && CLICK_ACTIONS.includes(action.type)) {
      const anchor = resolveAnchorPoint(action);
      if (anchor) {
        const p = projectPoint(anchor, viewportWidth, viewportHeight, outputWidth, outputHeight);
        if (p)
          drawClickIndicator(ctx, p.x, p.y, ageMs / clickCfg.durationMs, action.type, clickCfg);
      }
    }

    if (dragCfg.enabled && action.type === 'drag') {
      const start = action.startCoordinates || null;
      const end = action.endCoordinates || action.coordinates || null;
      if (start && end) {
        const p1 = projectPoint(start, viewportWidth, viewportHeight, outputWidth, outputHeight);
        const p2 = projectPoint(end, viewportWidth, viewportHeight, outputWidth, outputHeight);
        if (p1 && p2) drawDragPath(ctx, p1, p2, ageMs / dragCfg.durationMs, dragCfg);
      }
    }

    // Render labels: always show annotation actions, respect labelCfg.enabled for other actions
    const isAnnotation = action.type === 'annotation' || typeof action.label === 'string';
    const shouldRenderLabel = labelCfg.enabled || isAnnotation;

    if (shouldRenderLabel) {
      const text = resolveActionLabel(action, labelCfg);
      if (text) {
        const anchor = resolveAnchorPoint(action);
        const p = anchor
          ? projectPoint(anchor, viewportWidth, viewportHeight, outputWidth, outputHeight)
          : null;

        const t = clamp(ageMs / labelCfg.durationMs, 0, 1);
        const alpha = 1 - clamp((t - 0.75) / 0.25, 0, 1);

        drawLabelPill(ctx, text, p, alpha, labelCfg, outputWidth, outputHeight);
      }
    }
  }
}

// ============================================================================
// Event Pruning
// ============================================================================

export function pruneActionEventsInPlace(
  events: ActionEvent[],
  nowMs: number,
  config: ResolvedGifEnhancedRenderingConfig,
): void {
  if (events.length === 0) return;

  // Check if any events have annotations (which are always rendered)
  const hasAnnotations = events.some(
    (e) => e.action.type === 'annotation' || typeof e.action.label === 'string',
  );

  let maxLifetimeMs = 0;
  if (config.enabled) {
    if (config.clickIndicators.enabled)
      maxLifetimeMs = Math.max(maxLifetimeMs, config.clickIndicators.durationMs);
    if (config.dragPaths.enabled)
      maxLifetimeMs = Math.max(maxLifetimeMs, config.dragPaths.durationMs);
    if (config.labels.enabled) maxLifetimeMs = Math.max(maxLifetimeMs, config.labels.durationMs);
  }

  // Always account for label duration if there are annotations (they're always rendered)
  if (hasAnnotations) {
    maxLifetimeMs = Math.max(maxLifetimeMs, config.labels.durationMs);
  }

  if (maxLifetimeMs <= 0) {
    events.length = 0;
    return;
  }

  const cutoff = nowMs - maxLifetimeMs - 250;
  let dropCount = 0;
  while (dropCount < events.length && events[dropCount].atMs < cutoff) dropCount++;
  if (dropCount > 0) events.splice(0, dropCount);
}
