// ---------------------------------------------------------------------------
// shell / ErrorBoundary.tsx — WAVE 30, owner item 2 (ruling R5).
//
// ⛔ WHY THIS FILE EXISTS, AND THE MEASUREMENT BEHIND IT. Until today,
// `ErrorBoundary|componentDidCatch|getDerivedStateFromError` had ZERO hits in the
// whole of `web/src`. React's contract when a render throws and nothing catches it
// is to unmount the ENTIRE tree — so one unguarded property access anywhere in the
// product produced a blank white page. That is exactly what a user calls "it
// crashes", and it is why owner item 2 arrived as a report with no diagnosis: there
// was nothing left on screen to diagnose.
//
// A boundary is therefore not a workaround for the hide-fields panel. It is the
// difference between "one surface failed and said so" and "the product vanished".
//
// ⚠ WHAT NO GATE IN THIS REPO CAN PROVE, measured 2026-08-12 rather than assumed —
// written here so nobody reads the gate as stronger than it is:
//   · there is NO DOM. `jsdom`, `happy-dom`, `linkedom`, `domino` and
//     `react-test-renderer` are all absent from `web/node_modules` (160 packages),
//     and there is no vitest. So the React RECONCILER cannot be run under node.
//   · React 18.3.1's server renderers do NOT invoke error boundaries. Both
//     `renderToString` and `renderToStaticMarkup` were fed one throwing child under
//     a boundary declaring `getDerivedStateFromError`; both PROPAGATED the throw and
//     `componentDidCatch` never fired.
// ⇒ the fallback is a SEPARATE exported component precisely so it can be rendered
//   for real, and the class's protocol is driven by hand in `verify_grid_ux.py`
//   (throw → catch → `getDerivedStateFromError` → `render()`), which is React's own
//   documented sequence. The reconciler's guarantee is React's, not ours; what IS
//   ours — that every surface is actually wrapped — is a source scan, and that is
//   the check with teeth ([[reachable-is-not-the-same-as-built]]: a boundary that
//   exists and wraps nothing is this repo's sixth whole-correct-unreachable feature).
// ---------------------------------------------------------------------------

import { Component } from "react";
import type { ErrorInfo, ReactNode } from "react";
import "./errorBoundary.css";

export interface FailurePanelProps {
  /** The surface that failed, named the way the user got to it ("Odoo orders",
   *  "Automation", "Loopable"). A panel that will not say WHAT broke sends the
   *  reader back to us with the same sentence item 2 arrived with. */
  surface: string;
  /** The thrown message. Shown, not hidden: a user who can paste one line saves a
   *  scouting session. ⚠ It is the message ONLY — never a stack, and never a value
   *  from a row, because this panel is on screen wherever the failure happened. */
  message?: string;
  onReload: () => void;
}

/**
 * The contained failure card.
 *
 * It borrows `.shell-placeholder` and `.shell-retry` on purpose — the shell already
 * has two failure panels (the data-error card and the nav-failed card) and a third
 * visual language for the same idea would read as a different kind of problem. The
 * only new paint is the tone strip in `errorBoundary.css`, which is what separates
 * "this did not load" from "this broke".
 *
 * Exported as its own component so it can be rendered for real under node; see the
 * measurement in this file's header for why the class cannot be.
 */
export function FailurePanel({ surface, message, onReload }: FailurePanelProps) {
  return (
    <div className="shell-placeholder shell-crash" role="alert">
      <h1>{surface} could not be shown</h1>
      <p>
        Something here failed while drawing. The rest of the app still works — pick
        another database from the sidebar, or reload to try this one again.
      </p>
      {message ? <p className="shell-crash-detail">{message}</p> : null}
      <button
        className="login-submit shell-retry"
        type="button"
        onClick={onReload}
      >
        Reload
      </button>
    </div>
  );
}

export interface ErrorBoundaryProps {
  surface: string;
  children: ReactNode;
}

export interface ErrorBoundaryState {
  error: Error | null;
}

/**
 * Catch a render throw and paint `FailurePanel` in its place, leaving every sibling
 * mounted.
 *
 * ⚠ A BOUNDARY LATCHES, AND THAT IS THE TRAP TO KNOW ABOUT. Once it holds an error
 * it renders the panel until it is REMOUNTED — there is no "it will sort itself out
 * on the next render". So every mount below passes a `key` that changes when the
 * user moves (the route, the surface), which turns "navigate away and back" into the
 * recovery path a person will actually try. `Reload` is the second door, and it is
 * the same control the shell's two existing failure cards offer.
 */
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { error: null };
  }

  static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
    // Anything can be thrown in JavaScript, including a string or `undefined`.
    // Normalising here means `render` has one shape to reason about and the panel
    // can never itself throw reading `.message` off a non-Error.
    return { error: error instanceof Error ? error : new Error(String(error)) };
  }

  componentDidCatch(error: unknown, info: ErrorInfo): void {
    // The console is the only sink this client has today. It is enough to turn the
    // next report from "it crashes" into a stack — which was the whole cost of item
    // 2 arriving undiagnosed. Forwarding this to the server is booked as pending
    // work rather than smuggled in here: it needs a route, and it must be proven to
    // carry no row values before it ships.
    console.error(`[${this.props.surface}] render failed`, error, info.componentStack);
  }

  render(): ReactNode {
    const { error } = this.state;
    if (error) {
      return (
        <FailurePanel
          surface={this.props.surface}
          message={error.message}
          onReload={() => window.location.reload()}
        />
      );
    }
    return this.props.children;
  }
}
