// app.jsx — root, router, tweaks

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "severity": "moderate",
  "heroVariant": "split",
  "fontPair": "geist",
  "accent": "#10A171"
}/*EDITMODE-END*/;

const FONT_PAIRS = {
  geist:  { sans: "Geist", mono: "Geist Mono", label: "Geist · Geist Mono" },
  manrope:{ sans: "Manrope", mono: "JetBrains Mono", label: "Manrope · JetBrains" },
  jakarta:{ sans: "Plus Jakarta Sans", mono: "IBM Plex Mono", label: "Jakarta · Plex" },
  dm:     { sans: "DM Sans", mono: "DM Mono", label: "DM Sans · DM Mono" },
};

// ── URL routing ───────────────────────────────────────────────────────────────
// Lightweight History-API router. Only landing + partner pages get real URLs;
// transient views (results, legal, activate) stay state-only with no URL.
// The referral status tab rides in the hash: /referrals#new|reviewed|offersent|expired|declined.
const REF_FILTERS = ["new", "reviewed", "offersent", "expired", "declined"];

// Indexable views → their own canonical path. Every other view (partner portal,
// inspection results) is noindex. The SPA serves one index.html for all paths, so
// without per-route canonical sync each route inherits the homepage's hardcoded
// canonical and self-reports to Google as a duplicate of "/".
const CANONICAL_ORIGIN = "https://ottoklar.com";
const INDEXABLE_PATHS = { landing: "/", terms: "/terms", privacy: "/privacy", imprint: "/imprint" };

function parseLocation() {
  // Drop a trailing ".html" so the files backing the legal routes (/terms → terms.html)
  // resolve to the same view when reached by their file path, matching the canonical
  // each one declares. "/index.html" collapses to "/".
  const path = (window.location.pathname || "/")
    .replace(/\/+$/, "")
    .replace(/(?:\/index)?\.html$/, "") || "/";
  const hash = (window.location.hash || "").replace(/^#/, "");
  // Referral detail: /referral/<id> — a sub-state of the referrals tab.
  const refMatch = path.match(/^\/referral\/(.+)$/);
  if (refMatch) return { view: "partner-dash", tab: "referrals", referralId: decodeURIComponent(refMatch[1]) };
  // Inspection result: /inspection/<id> — public, deep-linkable results page.
  const insMatch = path.match(/^\/inspection\/(.+)$/);
  if (insMatch) return { view: "results", inspectionId: decodeURIComponent(insMatch[1]) };
  switch (path) {
    case "/login":     return { view: "partner-login" };
    case "/signup":    return { view: "partner-signup" };
    case "/reset-password": return { view: "partner-reset", resetToken: new URLSearchParams(window.location.search).get("token") || "" };
    case "/referrals": return { view: "partner-dash", tab: "referrals", filter: REF_FILTERS.includes(hash) ? hash : "all" };
    case "/payments":  return { view: "partner-dash", tab: "payments" };
    case "/settings":  return { view: "partner-dash", tab: "settings" };
    case "/terms":     return { view: "terms" };
    case "/privacy":   return { view: "privacy" };
    case "/imprint":   return { view: "imprint" };
    default:           return { view: "landing" };
  }
}

// Canonical path for a routed view; null → not URL-routed (leave URL unchanged).
function pathForView(view, tab) {
  switch (view) {
    case "landing":        return "/";
    case "partner-login":  return "/login";
    case "partner-signup": return "/signup";
    case "partner-reset":  return "/reset-password";
    case "partner-dash":   return tab === "payments" ? "/payments" : tab === "settings" ? "/settings" : "/referrals";
    case "terms":          return "/terms";
    case "privacy":        return "/privacy";
    case "imprint":        return "/imprint";
    default:               return null;
  }
}

// Dashboard routes require auth; redirect to /login when signed out.
function gateRoute(route) {
  if (route.view === "partner-dash" && !(window.OK_API && OK_API.isSignedIn())) {
    window.history.replaceState(null, "", "/login");
    return { view: "partner-login" };
  }
  return route;
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [route, setRoute] = React.useState(() => gateRoute(parseLocation()));
  const view = route.view;
  const [uploadedPhoto, setUploadedPhoto] = React.useState(null);
  const [activationEmail, setActivationEmail] = React.useState("");

  // i18n: keep <html lang> in sync and re-render on language change (toggle dormant).
  const [, __forceI18n] = React.useReducer((n) => n + 1, 0);
  React.useEffect(() => {
    document.documentElement.lang = getLang();
    onLangChange(() => __forceI18n());
    return () => onLangChange(null);
  }, []);

  // Apply font + accent live via CSS vars
  React.useEffect(() => {
    const pair = FONT_PAIRS[t.fontPair] || FONT_PAIRS.geist;
    document.documentElement.style.setProperty("--sans", `"${pair.sans}", ui-sans-serif, system-ui, sans-serif`);
    document.documentElement.style.setProperty("--mono", `"${pair.mono}", ui-monospace, "SF Mono", Menlo, monospace`);
    document.documentElement.style.setProperty("--accent", t.accent);
    // Derive ink + tint colors
    const tint = hexToTint(t.accent, 0.9);
    const ink  = darken(t.accent, 0.32);
    document.documentElement.style.setProperty("--accent-tint", tint);
    document.documentElement.style.setProperty("--accent-ink", ink);
  }, [t.fontPair, t.accent]);

  // Navigate to a routed URL: push history, then re-derive the view (with auth gate).
  const navigate = (path) => {
    if (window.location.pathname + window.location.hash !== path) {
      window.history.pushState(null, "", path);
    }
    setRoute(gateRoute(parseLocation()));
  };
  // Show a transient, non-URL view (results, legal, activate) without touching history.
  const showView = (v, extra) => setRoute({ view: v, ...(extra || {}) });

  // Keep the view in sync with browser back/forward and hash changes.
  React.useEffect(() => {
    const sync = () => setRoute(gateRoute(parseLocation()));
    window.addEventListener("popstate", sync);
    window.addEventListener("hashchange", sync);
    return () => { window.removeEventListener("popstate", sync); window.removeEventListener("hashchange", sync); };
  }, []);

  // SEO: sync <link rel=canonical>, og:url and the robots meta with the route so each
  // indexable page declares its own canonical (not the homepage's) and noindex views
  // aren't advertised as index,follow. index.html ships static homepage values as the default.
  // Non-production hosts (dev.ottoklar.com, *.vercel.app, localhost) are forced noindex and
  // canonical'd to the production origin so preview/staging never competes with ottoklar.com.
  React.useEffect(() => {
    const isProdHost = window.location.hostname === "ottoklar.com";
    const canonicalPath = INDEXABLE_PATHS[view];               // undefined → not an indexable view
    const indexable = isProdHost && canonicalPath !== undefined;
    const url = CANONICAL_ORIGIN + (canonicalPath !== undefined ? canonicalPath : ((window.location.pathname || "/").replace(/\/+$/, "") || "/"));
    const set = (sel, attr, val) => { const el = document.head.querySelector(sel); if (el) el.setAttribute(attr, val); };
    set('link[rel="canonical"]', "href", url);
    set('meta[property="og:url"]', "content", url);
    set('meta[name="robots"]', "content", indexable ? "index, follow" : "noindex, follow");
  }, [view]);

  const launch = (id, photo) => {
    setUploadedPhoto(photo || null);
    const realId = typeof id === "string" ? id.trim() : "";
    // Real inspection → deep-linkable /inspection/<id> URL. Without a real id there's
    // nothing to show (the no-inspection demo report was retired), so stay on landing.
    if (realId) navigate("/inspection/" + encodeURIComponent(realId));
    else navigate("/");
  };

  // Global escape hatch: route if the view has a URL, else show it transiently.
  React.useEffect(() => { window.__nav = (v) => { const p = pathForView(v); p ? navigate(p) : showView(v); }; }, []);

  const home = () => navigate("/");
  const goPartnerLogin  = () => navigate("/login");
  const goPartnerSignUp = () => navigate("/signup");
  // A fresh login/activation sets this one-shot flag so the dashboard pops the
  // onboarding modal once. SPA navigation keeps it in memory; a browser refresh
  // reloads the app (clearing it), so a reload shows only the referral-list
  // reminder box instead of reopening the modal.
  const goPartnerDash   = () => { window.__okJustAuthed = true; navigate("/referrals"); };
  const goPartnerActivate = (email) => { setActivationEmail(email || ""); showView("partner-activate"); };
  const goPartnerForgot = () => showView("partner-forgot");

  return (
    <>
      {view === "landing"        && <Landing onLaunch={launch} heroVariant={t.heroVariant} onHome={home} onPartnerLogin={goPartnerLogin} />}
      {view === "results"        && <ResultsPage severity={t.severity} onHome={home} inspectionId={route.inspectionId} uploadedPhoto={uploadedPhoto} />}
      {view === "terms"          && <TermsPage onHome={home} />}
      {view === "privacy"        && <PrivacyPage onHome={home} />}
      {view === "imprint"        && <ImprintPage onHome={home} />}
      {view === "partner-login"  && <PartnerLoginPage onHome={home} onLoginSuccess={goPartnerDash} onSignUp={goPartnerSignUp} onNeedsActivation={goPartnerActivate} onForgot={goPartnerForgot} />}
      {view === "partner-signup" && <PartnerSignUpPage onHome={home} onSignUpSuccess={goPartnerActivate} onLogin={goPartnerLogin} />}
      {view === "partner-activate" && <PartnerActivatePage onHome={home} email={activationEmail} onActivated={goPartnerDash} onLogin={goPartnerLogin} />}
      {view === "partner-forgot" && <PartnerForgotPasswordPage onHome={home} onLogin={goPartnerLogin} />}
      {view === "partner-reset"  && <PartnerResetPasswordPage onHome={home} token={route.resetToken} onLogin={goPartnerLogin} onResetSuccess={goPartnerLogin} />}
      {view === "partner-dash"   && <PartnerDashPage onHome={home} tab={route.tab || "referrals"} onTab={(tab) => navigate(pathForView("partner-dash", tab))} filter={route.filter || "all"} onFilter={(f) => navigate("/referrals" + (f && f !== "all" ? "#" + f : ""))} referralId={route.referralId} onSelectReferral={(id) => navigate("/referral/" + encodeURIComponent(id))} onBackToList={() => navigate("/referrals")} />}

      <CookieBanner />
      <TweaksPanel>
        <TweakSection label="Demo damage" />
        <TweakRadio
          label="Severity"
          value={t.severity}
          options={["minor", "moderate", "severe"]}
          onChange={(v) => setTweak("severity", v)}
        />
        <div style={{ fontSize: 11, color: "rgba(41,38,27,.55)", marginTop: -2, marginBottom: 6, lineHeight: 1.4 }}>
          Changes the damage annotations on the demo car illustration.
        </div>

        <TweakSection label="Landing hero" />
        <TweakSelect
          label="Layout"
          value={t.heroVariant}
          options={[
            { value: "split",    label: "Split — text + report card" },
            { value: "centered", label: "Centered — bold headline" },
            { value: "report",   label: "Report-first — proof up top" },
          ]}
          onChange={(v) => setTweak("heroVariant", v)}
        />

        <TweakSection label="Theme" />
        <TweakColor
          label="Accent"
          value={t.accent}
          options={["#10A171", "#0E7C8A", "#1F4FD9", "#E25A1C", "#0A0A0B"]}
          onChange={(v) => setTweak("accent", v)}
        />
        <TweakSelect
          label="Font pair"
          value={t.fontPair}
          options={Object.entries(FONT_PAIRS).map(([k, v]) => ({ value: k, label: v.label }))}
          onChange={(v) => setTweak("fontPair", v)}
        />
      </TweaksPanel>
    </>
  );
}

// Color helpers ──────────────────────────────────────────────────────────────
function hexToRgb(hex){
  const h = hex.replace("#","");
  const s = h.length === 3 ? h.split("").map(c => c+c).join("") : h;
  return [parseInt(s.slice(0,2),16), parseInt(s.slice(2,4),16), parseInt(s.slice(4,6),16)];
}
function rgbToHex(r,g,b){
  const c = v => Math.max(0,Math.min(255,Math.round(v))).toString(16).padStart(2,"0");
  return "#" + c(r) + c(g) + c(b);
}
function hexToTint(hex, amt){
  const [r,g,b] = hexToRgb(hex);
  return rgbToHex(r + (255-r)*amt, g + (255-g)*amt, b + (255-b)*amt);
}
function darken(hex, amt){
  const [r,g,b] = hexToRgb(hex);
  return rgbToHex(r * (1-amt), g * (1-amt), b * (1-amt));
}

// App-wide error boundary. Without one, any render-time throw (e.g. in the email/OTP gate)
// unmounts the whole tree and leaves a blank white page. This catches it, logs the real
// error for diagnosis, and shows a recoverable fallback instead of a blank screen.
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null }; }
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(error, info) { console.error("[OttoKlar] UI crash:", error, info && info.componentStack); }
  render() {
    if (!this.state.error) return this.props.children;
    return (
      <div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24, fontFamily: "var(--sans)", color: "var(--ink)" }}>
        <div className="card" style={{ maxWidth: 440, padding: "32px 28px", textAlign: "center" }}>
          <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 8 }}>Etwas ist schiefgelaufen.</div>
          <p style={{ fontSize: 14, color: "var(--mute)", lineHeight: 1.6, margin: "0 0 18px" }}>
            Bitte laden Sie die Seite neu. Bleibt das Problem bestehen, versuchen Sie es später erneut.
          </p>
          <button className="btn btn-primary" onClick={() => window.location.reload()} style={{ justifyContent: "center" }}>
            Seite neu laden
          </button>
          <div className="mono" style={{ fontSize: 11, color: "var(--mute)", marginTop: 16, wordBreak: "break-word", textAlign: "left", opacity: .8 }}>
            {String(this.state.error && (this.state.error.stack || this.state.error.message || this.state.error))}
          </div>
        </div>
      </div>
    );
  }
}

ReactDOM.createRoot(document.getElementById("root")).render(<ErrorBoundary><App /></ErrorBoundary>);
