import { Component, Fragment, type ReactNode } from 'react';
import * as Sentry from '@sentry/react';

import i18n from '../../i18n';
import { MapFallback } from './map-page/Fallbacks';

/**
 * Scoped error boundary for the WebGL map.
 *
 * deck.gl runs in interleaved mode, sharing MapLibre's WebGL context. When that
 * shared context gets into a bad state (lost context, a buffer/texture finalized
 * while still referenced — the "bufferSubData: no buffer" / "bindTexture: deleted
 * object" storm), the next interleaved draw can throw. deck.gl's own animation
 * loop swallows render errors via its `onError` prop, but interleaved draws fire
 * from MapLibre's synchronous `render` event, which can run inside a React commit
 * — so the throw bubbles to the nearest React error boundary.
 *
 * Without this, that throw reaches the single top-level boundary and blanks the
 * entire app ("Something went wrong / Refresh the page"). This boundary contains
 * the blast radius to the map and auto-recovers by remounting it, which builds a
 * fresh MapLibre + deck.gl context. Filters, selection and viewport live above
 * this boundary (in MapPage / the URL), so a remount preserves them.
 */

const MAX_AUTO_RECOVERIES = 2;
// Remount after a short delay so a transient GL state can settle first.
const RECOVERY_DELAY_MS = 400;
// Crashes spaced further apart than this are treated as independent, so the
// auto-recovery budget resets — only a tight crash loop escalates to manual retry.
const STABILITY_WINDOW_MS = 30_000;

interface MapErrorBoundaryProps {
  children: ReactNode;
}

interface MapErrorBoundaryState {
  errored: boolean;
  failedPermanently: boolean;
  renderKey: number;
  recoveries: number;
}

export class MapErrorBoundary extends Component<MapErrorBoundaryProps, MapErrorBoundaryState> {
  state: MapErrorBoundaryState = {
    errored: false,
    failedPermanently: false,
    renderKey: 0,
    recoveries: 0,
  };

  private recoveryTimer: ReturnType<typeof setTimeout> | null = null;
  private lastErrorAt = 0;

  static getDerivedStateFromError(): Partial<MapErrorBoundaryState> {
    return { errored: true };
  }

  componentDidCatch(error: Error, info: { componentStack?: string | null }) {
    const now = Date.now();
    const withinWindow = now - this.lastErrorAt < STABILITY_WINDOW_MS;
    this.lastErrorAt = now;
    const recoveries = withinWindow ? this.state.recoveries + 1 : 1;

    Sentry.captureException(error, {
      tags: { scope: 'map' },
      extra: { componentStack: info.componentStack, recoveries },
    });

    if (recoveries <= MAX_AUTO_RECOVERIES) {
      this.scheduleRecovery(recoveries);
    } else {
      this.setState({ failedPermanently: true, recoveries });
    }
  }

  componentWillUnmount() {
    this.clearRecoveryTimer();
  }

  private clearRecoveryTimer() {
    if (this.recoveryTimer) {
      clearTimeout(this.recoveryTimer);
      this.recoveryTimer = null;
    }
  }

  private scheduleRecovery(recoveries: number) {
    this.clearRecoveryTimer();
    this.recoveryTimer = setTimeout(() => {
      this.recoveryTimer = null;
      this.setState((prev) => ({
        errored: false,
        failedPermanently: false,
        renderKey: prev.renderKey + 1,
        recoveries,
      }));
    }, RECOVERY_DELAY_MS);
  }

  private handleManualRetry = () => {
    this.clearRecoveryTimer();
    this.lastErrorAt = 0;
    this.setState((prev) => ({
      errored: false,
      failedPermanently: false,
      renderKey: prev.renderKey + 1,
      recoveries: 0,
    }));
  };

  render() {
    if (this.state.failedPermanently) {
      return (
        <div className="flex h-full w-full items-center justify-center bg-warm-100 px-6 text-center dark:bg-navy-950">
          <div>
            <h2 className="text-lg font-semibold text-warm-900 dark:text-warm-100">
              {i18n.t('map.error.heading', { defaultValue: 'The map ran into a problem' })}
            </h2>
            <p className="mt-2 text-sm text-warm-600 dark:text-warm-300">
              {i18n.t('map.error.body', {
                defaultValue: 'This can happen when your browser’s graphics context is interrupted.',
              })}
            </p>
            <button
              type="button"
              onClick={this.handleManualRetry}
              className="mt-4 rounded-lg bg-teal-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-teal-700 dark:bg-teal-500 dark:hover:bg-teal-400"
            >
              {i18n.t('map.error.reload', { defaultValue: 'Reload the map' })}
            </button>
          </div>
        </div>
      );
    }

    // While auto-recovering, show the same spinner used for the lazy-load
    // fallback so the remount reads as a brief reload rather than a crash.
    if (this.state.errored) {
      return <MapFallback />;
    }

    return <Fragment key={this.state.renderKey}>{this.props.children}</Fragment>;
  }
}
