/* global React */
/* =====================================================================
 * error-boundary.jsx — last line of defense against the white screen.
 *
 * Wraps every page's <App>. When any descendant component throws during
 * render, instead of React's default white-screen blowup we show an
 * in-character fallback: a torn page from 42 号宇航员's logbook.
 *
 * Loaded BEFORE the page-specific jsx in each HTML; exposes
 *   window.ErrorBoundary
 *
 * Self-contained on purpose — if this file itself throws at top level,
 * the safety net is gone. Keep it simple, no external deps.
 * ===================================================================== */

const { Component } = React;

// 系统观测口吻：技术词汇 + 近未来叙事。
// 出错时随机抽一条，避免每次都看到同一段，叙事不至于穿帮。
// 每段中的 \n 会被渲染成换行（依赖 S.quote 的 whiteSpace: pre-line）。
const NARRATIVES = [
  {
    cn: "算力路由正在绕开本区域的拥塞节点，\n你所请求的页面暂时漂移在缓冲层之外。",
    en: "Compute routing is bypassing congested nodes in this region. Your page is briefly adrift outside the buffer layer.",
  },
  {
    cn: "2030年的某个工程师在这里留下了一个未修复的漏洞。\n我们正在派人去找他。",
    en: "Some engineer in 2030 left an unpatched bug here. We're sending someone to find them.",
  },
  {
    cn: "海底光缆在这段历史里断裂过一次。\n我们还在评估影响范围。",
    en: "An undersea cable broke once in this stretch of history. Damage radius still under assessment.",
  },
  {
    cn: "负责这个页面的AI在三分钟前提交了离职申请。\n我们正在交接。",
    en: "The AI responsible for this page filed its resignation three minutes ago. Handover in progress.",
  },
];

function pickNarrative() {
  return NARRATIVES[Math.floor(Math.random() * NARRATIVES.length)];
}

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    // Diagnostic: append `?testerror=1` to any page URL to force the
    // fallback to render — handy for previewing the design without
    // breaking real code. Remove the param from the URL to dismiss.
    const forceTest =
      typeof window !== "undefined" &&
      /[?&]testerror=1/.test(window.location.search);
    this.state = forceTest
      ? {
          hasError: true,
          error: new Error("Intentional test error — remove ?testerror=1 from the URL to dismiss."),
          narrative: pickNarrative(),
        }
      : { hasError: false, error: null, narrative: null };
  }

  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error,
      narrative: pickNarrative(),
    };
  }

  componentDidCatch(error, info) {
    // Surface the real error to console so the developer (or Claude) can
    // still diagnose. The user only sees the in-character fallback.
    if (typeof console !== "undefined" && console.error) {
      console.error("[ErrorBoundary]", error);
      if (info && info.componentStack) {
        console.error("[ErrorBoundary] component stack:", info.componentStack);
      }
    }
  }

  render() {
    if (!this.state.hasError) return this.props.children;

    const n = this.state.narrative;
    const err = this.state.error;
    const errMsg = (err && (err.message || String(err))) || "(no detail)";
    const errStack = (err && err.stack) || "";

    return (
      <div style={S.scrim}>
        <div style={S.card}>
          {/* HUD-style status row, matching the rest of the site's tone */}
          <div style={S.statusRow}>
            <span style={S.statusDot} />
            <span style={S.statusLabel}>TRANSMISSION&nbsp;LOST&nbsp;·&nbsp;信号中断</span>
          </div>

          <div style={S.divider} />

          <FallbackIcon />

          {/* Narrative — 42 号宇航员's voice */}
          <blockquote style={S.quote}>「{n.cn}」</blockquote>
          <p style={S.quoteEn}>{n.en}</p>

          <div style={S.divider} />

          {/* Actions: get back to base, or try the signal again */}
          <div style={S.actionRow}>
            <a href="index.html" style={S.btnSecondary}
               onMouseEnter={hoverSecondaryOn} onMouseLeave={hoverSecondaryOff}>
              回到主地图
            </a>
            <button type="button" style={S.btnPrimary}
                    onMouseEnter={hoverPrimaryOn} onMouseLeave={hoverPrimaryOff}
                    onClick={() => window.location.reload()}>
              重新尝试
            </button>
          </div>

          {/* Tech log — collapsed by default. For me/devs to debug. */}
          <details style={S.details}>
            <summary style={S.summary}>TECH&nbsp;LOG&nbsp;·&nbsp;技术日志</summary>
            <pre style={S.pre}>{errMsg}{errStack ? "\n\n" + errStack : ""}</pre>
          </details>
        </div>
      </div>
    );
  }
}

// Tiny SVG glyph — a tilted antenna + a fragmented signal wave + a small
// burnt-orange X. Same hand-drawn ink vocabulary as the map itself.
function FallbackIcon() {
  return (
    <svg
      width="64" height="64" viewBox="0 0 64 64"
      style={{ display: "block", margin: "0 auto 14px", opacity: 0.78 }}
      aria-hidden="true"
    >
      {/* base/ground */}
      <line x1="14" y1="52" x2="50" y2="52"
        stroke="#2C2416" strokeWidth="1.4" strokeLinecap="round" />
      {/* tilted antenna shaft */}
      <line x1="24" y1="50" x2="40" y2="14"
        stroke="#2C2416" strokeWidth="1.4" strokeLinecap="round" />
      {/* small dish at the top */}
      <path d="M 36 16 Q 40 10, 44 16" fill="none"
        stroke="#2C2416" strokeWidth="1.2" strokeLinecap="round" />
      {/* one stable signal wave */}
      <path d="M 46 12 Q 52 6, 58 14" fill="none"
        stroke="#C17A3A" strokeWidth="1.2" strokeLinecap="round" />
      {/* one fragmented signal wave (lost) */}
      <path d="M 44 16 Q 54 4, 62 16" fill="none"
        stroke="#C17A3A" strokeWidth="1.0" strokeLinecap="round"
        strokeDasharray="2 3" opacity="0.6" />
      {/* X mark — "lost" */}
      <line x1="50" y1="22" x2="58" y2="30"
        stroke="#C17A3A" strokeWidth="1.5" strokeLinecap="round" />
      <line x1="58" y1="22" x2="50" y2="30"
        stroke="#C17A3A" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}

// ---------------- styles ----------------
// Inline objects keep the boundary self-contained. Falls back to literal
// hex if the page hasn't defined the CSS variables (extremely defensive —
// if even that fails we still render readable text).
const ink     = "var(--ink, #2C2416)";
const inkMid  = "var(--ink-mid, #8C8074)";
const paper   = "var(--paper, #F2EDE4)";
const paper2  = "var(--paper-2, #EAE4D8)";
const burn    = "var(--burn, #C17A3A)";
const serif   = "'Noto Serif SC', Georgia, serif";
const serifEn = "'Cormorant Garamond', Georgia, serif";
const mono    = "'JetBrains Mono', ui-monospace, monospace";
const sans    = "'Inter', 'Noto Sans SC', system-ui, sans-serif";

const S = {
  scrim: {
    position: "fixed", inset: 0,
    display: "flex", alignItems: "center", justifyContent: "center",
    padding: "24px",
    background: paper,
    backgroundImage:
      "radial-gradient(at 12% 8%, rgba(193,122,58,0.06), transparent 35%)," +
      "radial-gradient(at 88% 92%, rgba(140,128,116,0.08), transparent 40%)",
    color: ink,
    zIndex: 9999,
    overflow: "auto",
  },
  card: {
    width: "100%",
    maxWidth: 540,
    padding: "32px 32px 26px",
    background: paper2,
    border: "1px solid rgba(44,36,22,0.20)",
    boxShadow:
      "0 24px 50px -28px rgba(44,36,22,0.42)," +
      "0 4px 12px -6px rgba(44,36,22,0.18)," +
      "inset 0 0 0 1px rgba(242,237,228,0.6)",
    fontFamily: sans,
  },
  statusRow: {
    display: "flex", alignItems: "center", gap: 10,
    fontFamily: mono, fontSize: 11, letterSpacing: "0.2em",
  },
  statusDot: {
    width: 7, height: 7, borderRadius: "50%",
    background: burn,
    boxShadow: "0 0 0 3px rgba(193,122,58,0.18)",
    animation: "ebPulse 2.2s ease-in-out infinite",
    flex: "0 0 auto",
  },
  statusLabel: {
    color: ink,
    whiteSpace: "nowrap",
  },
  divider: {
    margin: "16px 0",
    height: 1,
    background:
      "repeating-linear-gradient(90deg, rgba(44,36,22,0.32) 0 4px, transparent 4px 9px)",
  },
  quote: {
    fontFamily: serif,
    fontSize: 18,
    lineHeight: 1.78,
    color: ink,
    margin: "8px 0 12px",
    whiteSpace: "pre-line", // preserve the \n line break inside each narrative
  },
  quoteEn: {
    fontFamily: serifEn,
    fontStyle: "italic",
    fontSize: 14,
    lineHeight: 1.55,
    color: inkMid,
    margin: "0 0 14px",
  },
  actionRow: {
    display: "flex", gap: 10, justifyContent: "flex-end",
    flexWrap: "wrap",
    marginTop: 4,
  },
  btnPrimary: {
    appearance: "none",
    cursor: "pointer",
    padding: "9px 18px",
    fontFamily: mono,
    fontSize: 11,
    letterSpacing: "0.22em",
    color: paper,
    background: ink,
    border: "1px solid " + ink,
    transition: "background 180ms, transform 180ms",
  },
  btnSecondary: {
    display: "inline-block",
    cursor: "pointer",
    padding: "9px 18px",
    fontFamily: mono,
    fontSize: 11,
    letterSpacing: "0.22em",
    color: ink,
    background: "transparent",
    border: "1px solid rgba(44,36,22,0.42)",
    textDecoration: "none",
    transition: "border-color 180ms, color 180ms",
  },
  details: {
    marginTop: 22,
    fontFamily: mono,
    fontSize: 10.5,
    color: inkMid,
  },
  summary: {
    cursor: "pointer",
    letterSpacing: "0.2em",
    userSelect: "none",
    padding: "4px 0",
  },
  pre: {
    marginTop: 8,
    padding: "10px 12px",
    background: "rgba(44,36,22,0.05)",
    border: "1px solid rgba(44,36,22,0.12)",
    fontSize: 10.5,
    lineHeight: 1.5,
    color: ink,
    whiteSpace: "pre-wrap",
    wordBreak: "break-word",
    maxHeight: 200,
    overflow: "auto",
    fontFamily: mono,
  },
};

function hoverPrimaryOn(e)  { e.currentTarget.style.background = "#3a3022"; }
function hoverPrimaryOff(e) { e.currentTarget.style.background = ink; }
function hoverSecondaryOn(e) {
  e.currentTarget.style.borderColor = burn;
  e.currentTarget.style.color = burn;
}
function hoverSecondaryOff(e) {
  e.currentTarget.style.borderColor = "rgba(44,36,22,0.42)";
  e.currentTarget.style.color = "var(--ink, #2C2416)";
}

// Inject the pulse keyframe once. Idempotent: re-loading the file (which
// shouldn't happen) won't add a second copy.
if (typeof document !== "undefined" && !document.getElementById("__eb-style__")) {
  const styleEl = document.createElement("style");
  styleEl.id = "__eb-style__";
  styleEl.textContent =
    "@keyframes ebPulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }";
  document.head.appendChild(styleEl);
}

window.ErrorBoundary = ErrorBoundary;
