import React, { useEffect, useReducer, useRef, useState } from 'react';
import styled, { css, keyframes } from 'styled-components';
import type { ActivityHistoryEntry } from '../../utils/wsFrames';

/** Minimum time a single stage stays visible before advancing to a queued update (#96). */
const MIN_DISPLAY_MS = 400;
const ELAPSED_THRESHOLD_MS = 10_000;
const SILENCE_THRESHOLD_MS = 30_000;

function usePrefersReducedMotion(): boolean {
  const [reduced, setReduced] = useState<boolean>(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
    return window.matchMedia('(prefers-reduced-motion: reduce)')?.matches ?? false;
  });

  useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return undefined;
    const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
    if (!mql) return undefined;
    const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
    if (typeof mql.addEventListener === 'function') {
      mql.addEventListener('change', handler);
      return () => mql.removeEventListener('change', handler);
    }
    // Safari < 14 fallback
    mql.addListener(handler);
    return () => mql.removeListener(handler);
  }, []);

  return reduced;
}

type QueueItem = { stage: string; label: string | null; detail: string | null };

type IndicatorState = {
  current: QueueItem | null;
  queue: QueueItem[];
  lastIncomingStage: string | null;
};

type IndicatorAction =
  | { type: 'INCOMING'; item: QueueItem | null }
  | { type: 'ADVANCE' }
  | { type: 'RESET' };

const initialIndicatorState: IndicatorState = {
  current: null,
  queue: [],
  lastIncomingStage: null,
};

function indicatorReducer(state: IndicatorState, action: IndicatorAction): IndicatorState {
  switch (action.type) {
    case 'RESET':
      return initialIndicatorState;
    case 'INCOMING': {
      if (!action.item) return initialIndicatorState;
      // Same stage as the last update seen — refresh content in place (label/detail tweak),
      // no need to enforce the min-display queue since it isn't a stage transition.
      if (action.item.stage === state.lastIncomingStage) {
        if (state.queue.length > 0) {
          const queue = [...state.queue];
          queue[queue.length - 1] = action.item;
          return { ...state, queue };
        }
        return { ...state, current: action.item };
      }
      if (!state.current) {
        return { current: action.item, queue: [], lastIncomingStage: action.item.stage };
      }
      return {
        ...state,
        queue: [...state.queue, action.item],
        lastIncomingStage: action.item.stage,
      };
    }
    case 'ADVANCE': {
      if (state.queue.length === 0) return state;
      const [next, ...rest] = state.queue;
      return { ...state, current: next, queue: rest };
    }
    default:
      return state;
  }
}

const spin = keyframes`
  to { transform: rotate(360deg); }
`;

const bounce = keyframes`
  0%, 80%, 100% { transform: scale(0); }
  40% { transform: scale(1); }
`;

const Root = styled.div`
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  max-width: 100%;
  min-width: 0;
  width: 100%;
  color: ${({ theme }) => theme.colors.text};
  font-size: 0.9rem;

  @media (max-width: 360px) {
    font-size: 0.85rem;
  }
`;

const HistoryList = styled.ul`
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  opacity: 0.6;
  min-width: 0;
`;

const HistoryItem = styled.li`
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  min-width: 0;

  span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }
`;

const CheckMark = styled.span`
  flex-shrink: 0;
  color: ${({ theme }) => (theme.darkMode ? '#8ee6a0' : '#2e7d32')};
  font-weight: 700;
`;

const CurrentRow = styled.div`
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
`;

const Spinner = styled.span<{ $reducedMotion: boolean }>`
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  opacity: 0.7;
  ${({ $reducedMotion }) =>
    $reducedMotion
      ? css`
          animation: none;
        `
      : css`
          animation: ${spin} 0.8s linear infinite;
        `}
`;

const TextColumn = styled.div`
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
`;

const LabelText = styled.span`
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
`;

const DetailText = styled.span`
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.78rem;
  opacity: 0.65;
  margin-top: 0.1rem;
`;

const MetaText = styled.span`
  flex-shrink: 0;
  font-size: 0.75rem;
  opacity: 0.55;
  white-space: nowrap;
`;

const DotsRow = styled.div`
  display: flex;
  align-items: center;
  padding: 0.15rem 0;
`;

const Dot = styled.span<{ $delay: string; $reducedMotion: boolean }>`
  width: 8px;
  height: 8px;
  margin: 0 4px;
  border-radius: 50%;
  background: currentColor;
  ${({ $reducedMotion, $delay }) =>
    $reducedMotion
      ? css`
          opacity: 0.6;
        `
      : css`
          animation: ${bounce} 1.4s infinite ease-in-out both;
          animation-delay: ${$delay};
        `}
`;

export type ActivityIndicatorProps = {
  /** Current stage key from the latest "status" frame, or null before the first update. */
  stage: string | null;
  label?: string | null;
  detail?: string | null;
  /** Previously completed stages for this turn, oldest first. */
  history?: ActivityHistoryEntry[];
  /** Hides the indicator entirely once a stream is interrupted (#96). */
  interrupted?: boolean;
};

const ActivityIndicator = ({
  stage,
  label = null,
  detail = null,
  history = [],
  interrupted = false,
}: ActivityIndicatorProps): JSX.Element | null => {
  const [state, dispatch] = useReducer(indicatorReducer, initialIndicatorState);
  const reducedMotion = usePrefersReducedMotion();

  const overallStartRef = useRef<number | null>(null);
  const lastUpdateRef = useRef<number>(Date.now());
  const currentSinceRef = useRef<number>(Date.now());
  const advanceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [, forceTick] = useState(0);

  useEffect(() => {
    if (interrupted || stage == null) {
      overallStartRef.current = null;
      dispatch({ type: 'RESET' });
      return;
    }
    if (overallStartRef.current == null) {
      overallStartRef.current = Date.now();
    }
    lastUpdateRef.current = Date.now();
    dispatch({ type: 'INCOMING', item: { stage, label, detail } });
  }, [stage, label, detail, interrupted]);

  // Reset the per-stage clock whenever the displayed stage changes.
  useEffect(() => {
    currentSinceRef.current = Date.now();
  }, [state.current?.stage]);

  // Enforce the minimum display time per stage before advancing the queue.
  useEffect(() => {
    if (advanceTimerRef.current) {
      clearTimeout(advanceTimerRef.current);
      advanceTimerRef.current = null;
    }
    if (state.queue.length === 0) return undefined;
    const elapsed = Date.now() - currentSinceRef.current;
    const remaining = Math.max(MIN_DISPLAY_MS - elapsed, 0);
    advanceTimerRef.current = setTimeout(() => {
      dispatch({ type: 'ADVANCE' });
    }, remaining);
    return () => {
      if (advanceTimerRef.current) clearTimeout(advanceTimerRef.current);
    };
  }, [state.queue.length]);

  // Tick once a second while active so elapsed/"still working" copy stays fresh.
  useEffect(() => {
    if (!state.current) return undefined;
    const id = setInterval(() => forceTick((n) => n + 1), 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [Boolean(state.current)]);

  if (interrupted) return null;

  const now = Date.now();
  const elapsedMs = overallStartRef.current != null ? now - overallStartRef.current : 0;
  const silentMs = now - lastUpdateRef.current;
  const showElapsed = state.current != null && elapsedMs >= ELAPSED_THRESHOLD_MS;
  const stillWorking = state.current != null && silentMs >= SILENCE_THRESHOLD_MS;
  const elapsedLabel = showElapsed ? `${Math.floor(elapsedMs / 1000)}s` : null;

  return (
    <Root role="status" aria-live="polite">
      {history.length > 0 && (
        <HistoryList>
          {history.map((entry, i) => (
            <HistoryItem key={`${entry.stage}-${entry.startedAt}-${i}`}>
              <CheckMark aria-hidden="true">✓</CheckMark>
              <span>{entry.label}</span>
            </HistoryItem>
          ))}
        </HistoryList>
      )}

      {state.current ? (
        <CurrentRow>
          <Spinner
            aria-hidden="true"
            $reducedMotion={reducedMotion}
            data-testid="activity-spinner"
            data-reduced-motion={reducedMotion}
          />
          <TextColumn>
            <LabelText>
              {stillWorking ? 'Still working…' : state.current.label || 'Working…'}
            </LabelText>
            {state.current.detail && (
              <DetailText title={state.current.detail}>{state.current.detail}</DetailText>
            )}
          </TextColumn>
          {elapsedLabel && !stillWorking && <MetaText>{elapsedLabel}</MetaText>}
        </CurrentRow>
      ) : (
        <DotsRow data-testid="activity-dots-fallback">
          <Dot $delay="-0.32s" $reducedMotion={reducedMotion} />
          <Dot $delay="-0.16s" $reducedMotion={reducedMotion} />
          <Dot $delay="0s" $reducedMotion={reducedMotion} />
        </DotsRow>
      )}
    </Root>
  );
};

export default ActivityIndicator;
