import { Component, type ErrorInfo, Fragment, type ReactNode } from 'react';

import { SpinnerIcon } from './icons';

interface Props {
  children: ReactNode;
  /**
   * Optional scope label shown in logs / devtools. Defaults to `root` — set
   * per boundary (e.g. `route`, `conversation`) so multiple boundaries can be
   * distinguished at a glance.
   */
  scope?: string;
  /**
   * If the retry count exceeds this, the boundary stops auto-retrying and
   * shows a more helpful message (still without a button — Discord-style,
   * the app keeps trying but hints the user to hold on or check network).
   */
  maxAutoRetries?: number;
}

interface State {
  error: Error | null;
  retryKey: number;
  attempt: number;
}

const RETRY_DELAYS_MS = [2000, 4000, 8000, 15000, 30000];

// Discord-style error boundary.
//   - Catches render-time errors in its subtree.
//   - Shows a centred spinner + status text. Never renders a manual "Reload"
//     button; the boundary remounts its children on an exponential-backoff
//     schedule so the UI self-heals once the underlying issue clears (typical
//     causes: a realtime reconnect, a transient network blip, or a race that
//     only fires once).
//   - Escalates the label after each failed retry so the user sees that the
//     app is trying, rather than silent infinite spinning.
export class ErrorBoundary extends Component<Props, State> {
  state: State = { error: null, retryKey: 0, attempt: 0 };

  private retryTimer: number | null = null;

  static getDerivedStateFromError(error: Error): Partial<State> {
    return { error };
  }

  override componentDidCatch(error: Error, info: ErrorInfo): void {
    const scope = this.props.scope ?? 'root';
    // We explicitly log here — the boundary itself swallows the error from
    // React, so without this the failure would be invisible in production.
    console.error('[ErrorBoundary:' + scope + '] caught render error', error, info);
  }

  override componentDidUpdate(_prev: Props, prevState: State): void {
    if (this.state.error && !prevState.error) {
      this.scheduleRetry();
    }
  }

  override componentWillUnmount(): void {
    if (this.retryTimer !== null) {
      window.clearTimeout(this.retryTimer);
      this.retryTimer = null;
    }
  }

  private scheduleRetry(): void {
    if (this.retryTimer !== null) return;
    const attempt = this.state.attempt;
    const delay =
      RETRY_DELAYS_MS[Math.min(attempt, RETRY_DELAYS_MS.length - 1)] ??
      RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1] ??
      30000;
    this.retryTimer = window.setTimeout(() => {
      this.retryTimer = null;
      this.setState((prev) => ({
        error: null,
        retryKey: prev.retryKey + 1,
        attempt: prev.attempt + 1,
      }));
    }, delay);
  }

  override render(): ReactNode {
    if (this.state.error) {
      const max = this.props.maxAutoRetries ?? RETRY_DELAYS_MS.length;
      const escalated = this.state.attempt >= max;
      return <RetryingScreen escalated={escalated} attempt={this.state.attempt} />;
    }
    // `retryKey` forces a remount of the subtree so hooks re-run cleanly after
    // an error (otherwise stale state from the crashed tree can immediately
    // re-throw). Use a keyed Fragment so the boundary doesn't inject an extra
    // wrapper div — that would break `flex h-full` chains (e.g. AppShell →
    // Outlet → page column).
    return <Fragment key={this.state.retryKey}>{this.props.children}</Fragment>;
  }
}

function RetryingScreen({ escalated, attempt }: { escalated: boolean; attempt: number }) {
  const primary = escalated
    ? 'Verbindungsprobleme…'
    : attempt === 0
      ? 'Einen Moment bitte'
      : 'Versuche erneut zu laden…';
  const secondary = escalated
    ? 'Prüfe deine Internetverbindung. Wir versuchen es weiter.'
    : 'Die App lädt sich gleich selbst neu.';
  return (
    <div
      role="status"
      aria-live="polite"
      className="flex min-h-screen w-full items-center justify-center bg-surface-3 px-6"
    >
      <div className="flex flex-col items-center gap-4 text-center">
        <div className="relative flex h-16 w-16 items-center justify-center">
          <span
            aria-hidden="true"
            className="absolute inset-0 rounded-full border-2 border-accent/20"
          />
          <span
            aria-hidden="true"
            className="absolute inset-0 rounded-full border-2 border-accent border-r-transparent border-b-transparent animate-spin"
          />
          <SpinnerIcon className="hidden" />
        </div>
        <div className="max-w-sm space-y-1.5">
          <p className="font-display text-lg font-semibold text-fg">{primary}</p>
          <p className="text-sm text-fg-muted">{secondary}</p>
        </div>
      </div>
    </div>
  );
}
