// MostlyQR — node-flow / routing editor (ADR 0004). A rule-list rendered as a
// flow: Scan → ordered rules (first match wins) → mandatory Default, plus an
// integrations lane (fan-out, separate from routing) and a live scan simulator.
// Compiles 1:1 to link.routing { rules:[{conditions[],destination_url}], default }.
//
// LIBRARY (not a page): exposes window.MQRFlow.FlowEditor({ code, plan, locked,
// onUpgrade }) so the Builder can embed it. No standalone mount. Wrapped in an
// IIFE so its top-level consts don't collide with Builder.jsx's (both run as
// text/babel scripts on the same page).
(function () {
  const I18N = (typeof window !== 'undefined' && window.ForgeI18n) || { t: (k) => k, has: () => false, locale: 'en', dir: 'ltr', fmt: { number: (n)=>String(n??''), currency:(n)=>String(n??''), percent:(n)=>String(n??''), date:(d)=>String(d??''), time:(d)=>String(d??''), relative:(d)=>String(d??'') } };
  const tr = I18N.t, fmt = I18N.fmt;
  // Country list comes from @mostly-tiny/i18n (window.ForgeI18n.countries) —
  // localized by Intl.DisplayNames for the active locale, sorted by name. Guard
  // for the no-runtime fallback so the editor still renders.
  const countryList = () => (typeof I18N.countries === 'function' ? I18N.countries() : []);
  const countryName = (c) => (typeof I18N.countryName === 'function' ? I18N.countryName(c) : c);
  const { Icon, MQMark, Wordmark } = window.MQR;
  const F = window.ForgeDesignSystem_e40d74;
  const { Button, Badge } = F;
  const Api = window.MQRApi;

  /* ── condition + integration metadata ───────────────────── */
  const DEVICES = ["mobile", "desktop", "tablet"];
  const OSES = ["ios", "android", "windows", "macos", "other"];
  const DAYS = ["S", "M", "T", "W", "T", "F", "S"];
  const OPS = [["gte", "≥"], ["gt", ">"], ["lte", "≤"], ["lt", "<"], ["eq", "="]];

  const COND_TYPES = [
    { type: "device", label: tr('mqr.flow.cond.device'), icon: "smartphone" },
    { type: "os", label: tr('mqr.flow.cond.os'), icon: "smartphone" },
    { type: "country", label: tr('mqr.flow.cond.country'), icon: "globe" },
    { type: "region", label: tr('mqr.flow.cond.region'), icon: "globe" },
    { type: "city", label: tr('mqr.flow.cond.city'), icon: "globe" },
    { type: "geo_radius", label: tr('mqr.flow.cond.georadius'), icon: "scan" },
    { type: "language", label: tr('mqr.flow.cond.language'), icon: "globe" },
    { type: "time_of_day", label: tr('mqr.flow.cond.timeofday'), icon: "refresh" },
    { type: "weekday", label: tr('mqr.flow.cond.weekday'), icon: "refresh" },
    { type: "scan_count", label: tr('mqr.flow.cond.scancount'), icon: "chart" },
  ];
  function newCondition(type) {
    switch (type) {
      case "device": return { type, in: ["mobile"] };
      case "os": return { type, in: ["ios"] };
      case "country": return { type, in: ["US"] };
      case "region": return { type, in: [] };
      case "city": return { type, in: [] };
      // London, 1 km — a sensible non-empty default that validates; the author
      // re-centres it (the "use my location" button fills these from the browser).
      case "geo_radius": return { type, lat: 51.5074, lng: -0.1278, radius: 1000 };
      case "language": return { type, in: ["en"] };
      case "time_of_day": return { type, start: "11:00", end: "15:00" };
      case "weekday": return { type, days: [1, 2, 3, 4, 5] };
      case "scan_count": return { type, op: "gte", value: 2 };
      default: return { type: "device", in: ["mobile"] };
    }
  }

  // `soon: true` = not fully wired in the backend yet (sheets is a stub; slack
  // isn't in the delivery schema). Kept here so any legacy rows still render, but
  // filtered out of the "add" menu below.
  const INT_META = {
    webhook: { label: "Webhook", icon: "link", color: "#5b8def", sub: tr('mqr.flow.int.webhook.sub') },
    zapier: { label: "Zapier", icon: "zap", color: "#ea6c3a", sub: tr('mqr.flow.int.zapier.sub') },
    sheets: { label: "Google Sheets", icon: "grid", color: "#1f9d5b", sub: tr('mqr.flow.int.sheets.sub') },
    slack: { label: "Slack", icon: "hash", color: "#611f69", sub: tr('mqr.flow.int.slack.sub') },
    discord: { label: "Discord", icon: "chat", color: "#5865f2", sub: tr('mqr.flow.int.discord.sub') },
    // Meta Conversions API (server-side retargeting) needs pixel_id + access_token — configure via
    // the REST API/setIntegration (a 2-field form); rendered here if present. (F014)
    meta_capi: { label: "Meta Ads", icon: "globe", color: "#1877f2", sub: tr('mqr.flow.int.meta.sub'), soon: true },
  };

  /* ── recipes (templates) ────────────────────────────────── */
  function RECIPES(base) {
    const d = base || "https://your-site.com";
    return [
      { key: "stores", icon: "smartphone", title: tr('mqr.flow.recipe.stores.title'), desc: tr('mqr.flow.recipe.stores.desc'),
        rules: [
          { conditions: [{ type: "os", in: ["ios"] }], destination_url: "https://apps.apple.com/app/id000000" },
          { conditions: [{ type: "os", in: ["android"] }], destination_url: "https://play.google.com/store/apps/details?id=com.app" },
        ], default: d },
      { key: "menu", icon: "refresh", title: tr('mqr.flow.recipe.menu.title'), desc: tr('mqr.flow.recipe.menu.desc'),
        rules: [{ conditions: [{ type: "time_of_day", start: "11:00", end: "15:00" }], destination_url: d + "/lunch" }],
        default: d + "/dinner" },
      { key: "geo", icon: "globe", title: tr('mqr.flow.recipe.geo.title'), desc: tr('mqr.flow.recipe.geo.desc'),
        rules: [
          { conditions: [{ type: "country", in: ["SE"] }], destination_url: d + "/se" },
          { conditions: [{ type: "country", in: ["GB", "US"] }], destination_url: d + "/en" },
        ], default: d + "/en" },
      { key: "new", icon: "users", title: tr('mqr.flow.recipe.new.title'), desc: tr('mqr.flow.recipe.new.desc'),
        rules: [{ conditions: [{ type: "scan_count", op: "lte", value: 1 }], destination_url: d + "/welcome" }],
        default: d + "/menu" },
      // Restrict-to-a-zone: block scans from OUTSIDE a GPS radius; everyone inside
      // falls through to the default. The author re-centres the zone (London default).
      { key: "fence", icon: "lock", title: tr('mqr.flow.recipe.fence.title'), desc: tr('mqr.flow.recipe.fence.desc'),
        rules: [{ label: tr('mqr.flow.recipe.fence.zone'), conditions: [{ type: "geo_radius", lat: 51.5074, lng: -0.1278, radius: 1000, outside: true }], block: true, block_message: tr('mqr.flow.recipe.fence.msg') }],
        default: d },
    ];
  }

  /* ── pure local evaluator (mirrors @mostly-tiny/integrations evaluateRouting) ── */
  function hmToMin(hm) { const m = /^(\d{1,2}):(\d{2})$/.exec(String(hm || "").trim()); return m ? +m[1] * 60 + +m[2] : null; }
  // Haversine metres — mirrors the engine's geo_radius distance check for the sim.
  function haversineM(lat1, lng1, lat2, lng2) {
    const R = 6371000, toRad = (d) => (d * Math.PI) / 180;
    const dLat = toRad(lat2 - lat1), dLng = toRad(lng2 - lng1);
    const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
    return 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));
  }
  function condMatches(c, ctx) {
    const norm = (v) => String(v == null ? "" : v).trim().toLowerCase();
    switch (c.type) {
      case "device": return (c.in || []).map(norm).includes(norm(ctx.device));
      case "os": return (c.in || []).map(norm).includes(norm(ctx.os));
      case "country": return (c.in || []).map(norm).includes(norm(ctx.country));
      case "region": return (c.in || []).map(norm).filter(Boolean).includes(norm(ctx.region));
      case "city": return (c.in || []).map(norm).filter(Boolean).includes(norm(ctx.city));
      case "geo_radius": {
        // The sim provides the visitor's coords directly (no consent prompt here).
        if (ctx.lat === "" || ctx.lng === "" || ctx.lat == null || ctx.lng == null) return false;
        const within = haversineM(Number(ctx.lat), Number(ctx.lng), Number(c.lat), Number(c.lng)) <= Number(c.radius);
        return c.outside ? !within : within;
      }
      case "language": { const base = (v) => norm(v).split("-")[0]; return (c.in || []).map(base).includes(base(ctx.language)); }
      case "time_of_day": { const m = hmToMin(ctx.time), s = hmToMin(c.start), e = hmToMin(c.end); if (m == null || s == null || e == null) return false; return s < e ? (m >= s && m < e) : (m >= s || m < e); }
      case "weekday": return (c.days || []).includes(Number(ctx.weekday));
      case "scan_count": { const n = Number(ctx.scanCount) || 0, v = Number(c.value); switch (c.op) { case "gte": return n >= v; case "gt": return n > v; case "lte": return n <= v; case "lt": return n < v; case "eq": return n === v; default: return false; } }
      default: return false;
    }
  }
  // Keep all rule fields a saved/recipe rule may carry (block/label/etc.), not just
  // conditions+destination — so block rules round-trip and don't lose their action.
  function normalizeRule(x) {
    const r = { conditions: x.conditions || [], destination_url: x.destination_url || "" };
    if (x.block) r.block = true;
    if (x.block_message) r.block_message = x.block_message;
    if (x.label) r.label = x.label;
    return r;
  }
  function evalRouting(rules, def, ctx) {
    for (let i = 0; i < rules.length; i++) {
      const conds = rules[i].conditions || [];
      if (conds.length && conds.every((c) => condMatches(c, ctx))) {
        if (rules[i].block) return { url: null, idx: i, blocked: true };
        return { url: rules[i].destination_url, idx: i };
      }
    }
    return { url: def, idx: -1 };
  }

  /* ── country pickers — single for the simulator, multi for a routing rule.
       Both draw from the one centralized list in @mostly-tiny/i18n. ── */
  function CountrySelect({ value, onChange }) {
    return (
      <select className="fl-select" value={value || ""} onChange={(e) => onChange(e.target.value)}>
        {countryList().map((o) => <option key={o.code} value={o.code}>{o.name}</option>)}
      </select>
    );
  }
  function CountryPicker({ value, onChange }) {
    const sel = value || [];
    const add = (code) => { if (code && !sel.includes(code)) onChange([...sel, code]); };
    const remove = (code) => onChange(sel.filter((c) => c !== code));
    return (
      <span className="fl-countries">
        {sel.map((c) => (
          <span className="fl-country" key={c}>
            {countryName(c)}
            <button type="button" className="fl-country__x" title={tr('mqr.flow.int.remove')} onClick={() => remove(c)}><Icon name="x" size={11} /></button>
          </span>
        ))}
        <select className="fl-select fl-country-add" value="" onChange={(e) => add(e.target.value)}>
          <option value="">{tr('mqr.flow.cond.country.add')}</option>
          {countryList().filter((o) => !sel.includes(o.code)).map((o) => <option key={o.code} value={o.code}>{o.name}</option>)}
        </select>
      </span>
    );
  }

  /* ── geo_radius inputs (centre lat/lng + radius, with a browser-geolocation
       "use my location" helper to fill the centre) ── */
  function GeoRadiusFields({ cond, onChange }) {
    const [busy, setBusy] = React.useState(false);
    const setNum = (k) => (e) => onChange({ ...cond, [k]: e.target.value === "" ? "" : Number(e.target.value) });
    const useMyLocation = () => {
      if (!navigator.geolocation) return;
      setBusy(true);
      navigator.geolocation.getCurrentPosition(
        (p) => { onChange({ ...cond, lat: Number(p.coords.latitude.toFixed(6)), lng: Number(p.coords.longitude.toFixed(6)) }); setBusy(false); },
        () => setBusy(false),
        { enableHighAccuracy: true, timeout: 10000 },
      );
    };
    return (
      <span className="fl-geo">
        {/* inside ↔ outside: an "outside" geo rule is the restrict-to-a-zone form. */}
        <span className="fl-geo__io">
          <button type="button" data-on={!cond.outside} onClick={() => onChange({ ...cond, outside: false })}>{tr('mqr.flow.cond.geo.inside')}</button>
          <button type="button" data-on={!!cond.outside} onClick={() => onChange({ ...cond, outside: true })}>{tr('mqr.flow.cond.geo.outside')}</button>
        </span>
        <input className="fl-select fl-geo__n" type="number" step="any" value={cond.lat} onChange={setNum("lat")} aria-label={tr('mqr.flow.cond.geo.lat')} placeholder={tr('mqr.flow.cond.geo.lat')} />
        <input className="fl-select fl-geo__n" type="number" step="any" value={cond.lng} onChange={setNum("lng")} aria-label={tr('mqr.flow.cond.geo.lng')} placeholder={tr('mqr.flow.cond.geo.lng')} />
        <span className="fl-geo__rad">
          <input className="fl-select fl-geo__n" type="number" min="1" value={cond.radius} onChange={setNum("radius")} aria-label={tr('mqr.flow.cond.geo.radius')} />
          <span className="fl-geo__unit">m</span>
        </span>
        <button type="button" className="fl-geo__btn" onClick={useMyLocation} disabled={busy} title={tr('mqr.flow.cond.geo.use')}>
          <Icon name="globe" size={13} sw={2} /> {busy ? tr('mqr.flow.cond.geo.locating') : tr('mqr.flow.cond.geo.use')}
        </button>
      </span>
    );
  }

  /* ── condition row ──────────────────────────────────────── */
  function ConditionRow({ cond, onChange, onRemove }) {
    const csv = (arr) => (arr || []).join(", ");
    const parseCsv = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
    return (
      <div className="fl-cond">
        <select className="fl-select fl-cond__type" value={cond.type}
          onChange={(e) => onChange(newCondition(e.target.value))}>
          {COND_TYPES.map((c) => <option key={c.type} value={c.type}>{c.label}</option>)}
        </select>
        {cond.type === "device" && (
          <select className="fl-select fl-input" value={(cond.in || [])[0] || "mobile"} onChange={(e) => onChange({ ...cond, in: [e.target.value] })}>
            {DEVICES.map((d) => <option key={d} value={d}>{d}</option>)}
          </select>
        )}
        {cond.type === "os" && (
          <select className="fl-select fl-input" value={(cond.in || [])[0] || "ios"} onChange={(e) => onChange({ ...cond, in: [e.target.value] })}>
            {OSES.map((o) => <option key={o} value={o}>{o}</option>)}
          </select>
        )}
        {cond.type === "country" && (
          <CountryPicker value={cond.in} onChange={(v) => onChange({ ...cond, in: v })} />
        )}
        {cond.type === "region" && (
          <input className="fl-input" value={csv(cond.in)} placeholder={tr('mqr.flow.cond.region.ph')}
            onChange={(e) => onChange({ ...cond, in: parseCsv(e.target.value) })} />
        )}
        {cond.type === "city" && (
          <input className="fl-input" value={csv(cond.in)} placeholder={tr('mqr.flow.cond.city.ph')}
            onChange={(e) => onChange({ ...cond, in: parseCsv(e.target.value) })} />
        )}
        {cond.type === "geo_radius" && (
          <GeoRadiusFields cond={cond} onChange={onChange} />
        )}
        {cond.type === "language" && (
          <input className="fl-input" value={csv(cond.in)} placeholder="en, sv"
            onChange={(e) => onChange({ ...cond, in: parseCsv(e.target.value.toLowerCase()) })} />
        )}
        {cond.type === "time_of_day" && (
          <span className="fl-time">
            <input className="fl-select" type="time" value={cond.start} onChange={(e) => onChange({ ...cond, start: e.target.value })} />
            <span style={{ color: "var(--fg-subtle)", fontSize: 12 }}>{tr('mqr.flow.cond.to')}</span>
            <input className="fl-select" type="time" value={cond.end} onChange={(e) => onChange({ ...cond, end: e.target.value })} />
          </span>
        )}
        {cond.type === "weekday" && (
          <span className="fl-days">
            {DAYS.map((d, i) => (
              <button key={i} className="fl-day" data-on={(cond.days || []).includes(i)}
                onClick={() => { const set = new Set(cond.days || []); set.has(i) ? set.delete(i) : set.add(i); onChange({ ...cond, days: [...set].sort() }); }}>{d}</button>
            ))}
          </span>
        )}
        {cond.type === "scan_count" && (
          <span className="fl-time">
            <select className="fl-select" value={cond.op} onChange={(e) => onChange({ ...cond, op: e.target.value })}>
              {OPS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
            </select>
            <input className="fl-select" type="number" min="1" style={{ width: 80 }} value={cond.value} onChange={(e) => onChange({ ...cond, value: Number(e.target.value) })} />
          </span>
        )}
        <button className="fl-cond__x" title={tr('mqr.flow.cond.remove')} onClick={onRemove}><Icon name="x" size={14} /></button>
      </div>
    );
  }

  /* ── rule card ──────────────────────────────────────────── */
  function RuleCard({ rule, index, total, hit, onChange, onMove, onRemove }) {
    const setCond = (i, c) => { const conditions = rule.conditions.slice(); conditions[i] = c; onChange({ ...rule, conditions }); };
    const addCond = () => onChange({ ...rule, conditions: [...rule.conditions, newCondition("device")] });
    const rmCond = (i) => onChange({ ...rule, conditions: rule.conditions.filter((_, j) => j !== i) });
    return (
      <>
        {/* The scan→route connector (fl-wire--h) already sits above the first
            rule; only inter-rule cards need their own leading wire. */}
        {index > 0 && <div className="fl-wire" />}
        <div className={"fl-rule" + (hit ? " is-hit" : "")}>
          <div className="fl-rule__top">
            <span className="fl-rule__num">{index + 1}</span>
            <span className="fl-rule__when">{tr('mqr.flow.rule.when')}</span>
            {/* Show the "lands here" badge AND the move/delete tools together — the
                tools must stay reachable even when this rule is the current hit. */}
            <span className="fl-rule__right">
              {hit && <span className="fl-rule__hitbadge">{tr('mqr.flow.rule.hit')}</span>}
              <span className="fl-rule__tools">
                <button className="fl-iconbtn" title={tr('mqr.flow.rule.moveup')} disabled={index === 0} onClick={() => onMove(index, -1)}><Icon name="chevDown" size={14} style={{ transform: "rotate(180deg)" }} /></button>
                <button className="fl-iconbtn" title={tr('mqr.flow.rule.movedown')} disabled={index === total - 1} onClick={() => onMove(index, 1)}><Icon name="chevDown" size={14} /></button>
                <button className="fl-iconbtn danger" title={tr('mqr.flow.rule.delete')} onClick={() => onRemove(index)}><Icon name="x" size={14} /></button>
              </span>
            </span>
          </div>
          {rule.conditions.map((c, i) => (
            <ConditionRow key={i} cond={c} onChange={(nc) => setCond(i, nc)} onRemove={() => rmCond(i)} />
          ))}
          <button className="fl-addcond" onClick={addCond}><Icon name="plus" size={13} sw={2.2} /> {tr('mqr.flow.rule.addcond')}</button>
          {/* Rule action: redirect to a URL, or BLOCK access (restrict-to-a-zone). */}
          <div className="fl-action">
            <span className="fl-action__seg">
              <button type="button" data-on={!rule.block} onClick={() => onChange({ ...rule, block: false })}>{tr('mqr.flow.rule.act.redirect')}</button>
              <button type="button" data-on={!!rule.block} onClick={() => onChange({ ...rule, block: true })}>{tr('mqr.flow.rule.act.block')}</button>
            </span>
            {rule.block ? (
              <div className="fl-dest fl-dest--block">
                <Icon name="lock" size={16} className="fl-dest__arrow" />
                <input className="fl-input" value={rule.block_message || ""} maxLength={80} placeholder={tr('mqr.flow.rule.block.placeholder')}
                  onChange={(e) => onChange({ ...rule, block_message: e.target.value })} />
              </div>
            ) : (
              <div className="fl-dest">
                <Icon name="arrow" size={16} className="fl-dest__arrow" />
                <input className="fl-input" value={rule.destination_url || ""} placeholder={tr('mqr.flow.rule.dest.placeholder')}
                  onChange={(e) => onChange({ ...rule, destination_url: e.target.value })} />
              </div>
            )}
          </div>
        </div>
      </>
    );
  }

  /* ── integrations lane ──────────────────────────────────── */
  // webhook + zapier both deliver to an https URL the user supplies; clicking
  // their card opens an inline form to capture it before creating the config.
  const INT_NEEDS_URL = { webhook: true, zapier: true, sheets: true, slack: true, discord: true };

  function IntegrationsLane({ ints, onAdd, onRemove }) {
    // Surface every available integration as its own button — nothing hidden
    // behind an "Add integration" dropdown — so users see exactly which
    // integrations they get (and what they'd unlock by upgrading).
    // Show as "add" cards only the integrations NOT already configured (those render as the
    // removable chips above) — so a type never appears twice. `soon` types stay hidden.
    const configuredTypes = new Set((ints || []).map((i) => i.type));
    const available = Object.keys(INT_META).filter((t) => !INT_META[t].soon && !configuredTypes.has(t));
    // When a URL-based integration is being added, `pending` holds its type and
    // we show the URL form instead of firing onAdd immediately.
    const [pending, setPending] = React.useState(null);
    const [editingId, setEditingId] = React.useState(null); // non-null → editing an existing one
    const [url, setUrl] = React.useState("");
    const [err, setErr] = React.useState("");
    const [busy, setBusy] = React.useState(false);
    // Opt-in: forward the visitor's PRECISE GPS to this webhook (not stored by us).
    const [fwdRaw, setFwdRaw] = React.useState(false);

    // Open the URL form to ADD a new integration of type `t`, or EDIT an existing one (pass it).
    const start = (t, existing) => {
      if (!existing && !INT_NEEDS_URL[t]) { onAdd(t); return; }
      setErr("");
      setUrl(existing ? (existing.url || existing.target || "") : "");
      setFwdRaw(existing ? existing.forward_location === "raw" : false);
      setEditingId(existing ? existing.id : null);
      setPending(t);
    };
    const cancel = () => { setPending(null); setEditingId(null); setUrl(""); setErr(""); setFwdRaw(false); };
    const confirm = async () => {
      const u = url.trim();
      // Match the server: these need a valid https URL.
      if (!/^https:\/\/.+/i.test(u)) { setErr(tr('mqr.flow.int.url.invalid')); return; }
      setBusy(true); setErr("");
      try { await onAdd(pending, u, fwdRaw ? 'raw' : undefined, editingId); cancel(); }
      catch (e) { setErr((e && e.message) || String(e)); }
      finally { setBusy(false); }
    };

    return (
      <div className="fl-intlane">
        <div className="fl-intlane__head"><Icon name="zap" size={13} sw={2.2} /> {tr('mqr.flow.int.lanehead')}</div>
        {pending ? (
          // Inline URL form — used to BOTH add a new integration and edit an existing one.
          <div className="fl-inturl">
            <div className="fl-inturl__head">
              <span className="fl-int__ic" style={{ background: INT_META[pending].color }}><Icon name={INT_META[pending].icon} size={14} sw={2} /></span>
              <span className="fl-int__t">{INT_META[pending].label}</span>
            </div>
            <p className="fl-inturl__lbl">{tr('mqr.flow.int.url.label.' + pending)}</p>
            <input
              className="fl-select fl-inturl__in"
              type="url"
              autoFocus
              placeholder={tr('mqr.flow.int.url.placeholder.' + pending)}
              value={url}
              onChange={(e) => { setUrl(e.target.value); if (err) setErr(""); }}
              onKeyDown={(e) => { if (e.key === "Enter") confirm(); if (e.key === "Escape") cancel(); }}
            />
            <label className="fl-inturl__fwd">
              <input type="checkbox" checked={fwdRaw} onChange={(e) => setFwdRaw(e.target.checked)} />
              <span>
                <span className="fl-inturl__fwdt">{tr('mqr.flow.int.fwdloc.label')}</span>
                <span className="fl-inturl__fwdd">{tr('mqr.flow.int.fwdloc.hint')}</span>
              </span>
            </label>
            {err && <div className="fl-inturl__err"><Icon name="x" size={12} sw={2.2} /> {err}</div>}
            <div className="fl-inturl__actions">
              <Button variant="ghost" onClick={cancel} disabled={busy}>{tr('mqr.flow.int.url.cancel')}</Button>
              <Button variant="primary" onClick={confirm} disabled={busy || !url.trim()}>{busy ? tr('mqr.flow.int.url.adding') : (editingId ? tr('mqr.flow.int.url.save') : tr('mqr.flow.int.url.confirm'))}</Button>
            </div>
          </div>
        ) : (
          // One consistent card grid: CONFIGURED integrations (with their target + Edit/Remove)
          // followed by the ones still AVAILABLE to add. Same width + style throughout.
          <div className="fl-recipe-grid">
            {ints.map((i) => {
              const m = INT_META[i.type] || { label: i.type, icon: "link", color: "#888" };
              const sub = i.url ? i.url.replace(/^https?:\/\//, "") : (i.target || m.sub);
              const editable = INT_NEEDS_URL[i.type];
              return (
                <div key={i.id} className="fl-recipe fl-intcard fl-intcard--on">
                  <span className="fl-intcard__active">{tr('mqr.dashboard.status.active')}</span>
                  <span className="fl-recipe__ic" style={{ background: m.color, color: "#fff" }}><Icon name={m.icon} size={18} /></span>
                  <div className="fl-recipe__t">{m.label}
                    {i.forward_location === 'raw' ? <span className="fl-int__loc" title={tr('mqr.flow.int.fwdloc.badge')}><Icon name="globe" size={10} sw={2} /> {tr('mqr.flow.int.fwdloc.badge')}</span> : null}
                  </div>
                  <div className="fl-recipe__d" style={{ wordBreak: "break-all" }}>{sub}</div>
                  <div className="fl-intcard__foot">
                    {editable && <button className="fl-intcard__act" onClick={() => start(i.type, i)}><Icon name="pencil" size={12} sw={2} /> {tr('mqr.flow.int.edit')}</button>}
                    <button className="fl-intcard__act fl-intcard__act--rm" onClick={() => onRemove(i)} title={tr('mqr.flow.int.remove')}><Icon name="x" size={12} sw={2} /> {tr('mqr.flow.int.remove')}</button>
                  </div>
                </div>
              );
            })}
            {available.map((t) => {
              const m = INT_META[t];
              return (
                <button key={t} className="fl-recipe fl-intcard" onClick={() => start(t)} title={tr('mqr.flow.int.add')}>
                  <span className="fl-recipe__ic" style={{ background: m.color, color: "#fff" }}><Icon name={m.icon} size={18} /></span>
                  <div className="fl-recipe__t">{m.label}</div>
                  <div className="fl-recipe__d">{m.sub}</div>
                  <span className="fl-intcard__add"><Icon name="plus" size={13} sw={2.2} /> {tr('mqr.flow.int.add')}</span>
                </button>
              );
            })}
          </div>
        )}
      </div>
    );
  }

  /* ── simulator ──────────────────────────────────────────── */
  function Simulator({ rules, def, ctx, setCtx, result }) {
    const fld = (label, child) => <div className="fl-sim__row"><label>{label}</label>{child}</div>;
    return (
      <div className="fl-sim">
        <div className="fl-sim__title"><Icon name="scan" size={16} sw={2} style={{ color: "var(--accent)" }} /> {tr('mqr.flow.sim.title')}</div>
        <p className="fl-sim__sub">{tr('mqr.flow.sim.sub')}</p>
        {fld(tr('mqr.flow.sim.device'), <select className="fl-select" value={ctx.device} onChange={(e) => setCtx({ ...ctx, device: e.target.value })}>{DEVICES.map((d) => <option key={d}>{d}</option>)}</select>)}
        {fld(tr('mqr.flow.sim.os'), <select className="fl-select" value={ctx.os} onChange={(e) => setCtx({ ...ctx, os: e.target.value })}>{OSES.map((o) => <option key={o}>{o}</option>)}</select>)}
        {fld(tr('mqr.flow.sim.country'), <CountrySelect value={ctx.country} onChange={(v) => setCtx({ ...ctx, country: v })} />)}
        {fld(tr('mqr.flow.sim.region'), <input className="fl-select" value={ctx.region} placeholder={tr('mqr.flow.cond.region.ph')} onChange={(e) => setCtx({ ...ctx, region: e.target.value })} />)}
        {fld(tr('mqr.flow.sim.city'), <input className="fl-select" value={ctx.city} placeholder={tr('mqr.flow.cond.city.ph')} onChange={(e) => setCtx({ ...ctx, city: e.target.value })} />)}
        {fld(tr('mqr.flow.sim.gps'), <span className="fl-sim__gps">
          <input className="fl-select" type="number" step="any" placeholder={tr('mqr.flow.cond.geo.lat')} value={ctx.lat} onChange={(e) => setCtx({ ...ctx, lat: e.target.value })} />
          <input className="fl-select" type="number" step="any" placeholder={tr('mqr.flow.cond.geo.lng')} value={ctx.lng} onChange={(e) => setCtx({ ...ctx, lng: e.target.value })} />
        </span>)}
        {fld(tr('mqr.flow.sim.time'), <input className="fl-select" type="time" value={ctx.time} onChange={(e) => setCtx({ ...ctx, time: e.target.value })} />)}
        {fld(tr('mqr.flow.sim.day'), <select className="fl-select" value={ctx.weekday} onChange={(e) => setCtx({ ...ctx, weekday: Number(e.target.value) })}>{[tr('mqr.flow.day.sun'), tr('mqr.flow.day.mon'), tr('mqr.flow.day.tue'), tr('mqr.flow.day.wed'), tr('mqr.flow.day.thu'), tr('mqr.flow.day.fri'), tr('mqr.flow.day.sat')].map((d, i) => <option key={i} value={i}>{d}</option>)}</select>)}
        {fld(tr('mqr.flow.sim.scannum'), <input className="fl-select" type="number" min="1" value={ctx.scanCount} onChange={(e) => setCtx({ ...ctx, scanCount: Number(e.target.value) })} />)}
        <div className={"fl-sim__result" + (result.blocked ? " is-blocked" : "")}>
          <div className="fl-sim__rlbl">
            <Icon name={result.blocked ? "lock" : "arrow"} size={12} /> {result.blocked ? tr('mqr.flow.sim.blocked') : tr('mqr.flow.sim.goesto')}
          </div>
          {!result.blocked && <div className="fl-sim__rurl">{(result.url || "—").replace(/^https?:\/\//, "")}</div>}
          <div className="fl-sim__rvia">{result.idx === -1 ? tr('mqr.flow.sim.viadefault') : tr('mqr.flow.sim.viarule', { num: result.idx + 1 })}</div>
        </div>
      </div>
    );
  }

  /* ── recipe gallery ─────────────────────────────────────── */
  function RecipeGallery({ base, onPick }) {
    return (
      <div className="fl-recipes">
        <h2 className="fl-recipes__h">{tr('mqr.flow.recipes.heading')}</h2>
        <p className="fl-recipes__sub">{tr('mqr.flow.recipes.sub')}</p>
        <div className="fl-recipe-grid">
          {RECIPES(base).map((r) => (
            <button key={r.key} className="fl-recipe" onClick={() => onPick(r)}>
              <span className="fl-recipe__ic"><Icon name={r.icon} size={18} /></span>
              <div className="fl-recipe__t">{r.title}</div>
              <div className="fl-recipe__d">{r.desc}</div>
            </button>
          ))}
        </div>
      </div>
    );
  }

  /* ── embeddable editor ──────────────────────────────────── */
  // Props: { code, link, baseDestination, name, locked, onUpgrade, onSaved }.
  // Edits the SAME link the Builder owns: routing rules + integrations for that
  // code, with the default destination driven by the builder above (read-only
  // here). Saving persists name + destination + routing for the one code, and
  // reports the updated link back via onSaved. When `locked` (free) the body is
  // dimmed + read-only and a non-covering upgrade banner sits above it.
  // `passcode` is the passcode change to persist with this save: undefined = no
  // change, "" = clear protection, "<value>" = set/replace. The Builder owns the
  // control + computes this; the editor just forwards it on create/update.
  function FlowEditor({ code, link: linkProp, baseDestination, defaultValue, onDefaultChange, name, content, style, passcode, builderDirty, draft, locked, onUpgrade, onSaved, saveRef, onState }) {
    const [link, setLink] = React.useState(linkProp || null);
    const [rules, setRules] = React.useState([]);
    const [ints, setInts] = React.useState([]);
    const [dirty, setDirty] = React.useState(false);
    const [saving, setSaving] = React.useState(false);
    const [savedAt, setSavedAt] = React.useState(false);
    const [loaded, setLoaded] = React.useState(!!linkProp);
    const [ctx, setCtx] = React.useState({ device: "mobile", os: "ios", country: "SE", region: "", city: "", lat: "", lng: "", time: "12:30", weekday: 1, scanCount: 1 });

    // The Builder is the single source of truth for the link + its base
    // destination. Re-seed routing whenever that link changes (load + save). A
    // fresh draft (`draft`) starts empty and is created on first save.
    React.useEffect(() => {
      let alive = true;
      if (draft) { setLink(null); setRules([]); setInts([]); setDirty(false); setSavedAt(false); setLoaded(true); return () => { alive = false; }; }
      (async () => {
        try {
          // Resolve within the caller's OWN links (listLinks is ownership-scoped),
          // never getLink(code) — a ?code= from the URL may be a demo/foreign code
          // we don't own, which would 404. Prefer the link the Builder passed in.
          let l = linkProp;
          if (!l) {
            const ls = await Api.listLinks();
            l = (code && ls.find((x) => x.code === code)) || ls[0] || null;
          }
          if (!alive || !l) return;
          setLink(l);
          const r = l.routing || {};
          setRules((r.rules || []).map(normalizeRule));
          setDirty(false); setSavedAt(false);
          try { const ix = await Api.listIntegrations(l.code); if (alive) setInts(ix); } catch (e) { /* optional */ }
        } catch (e) {
          // Not signed in / offline — render the editor empty rather than hang.
        } finally {
          if (alive) setLoaded(true);
        }
      })();
      return () => { alive = false; };
    }, [linkProp, code, draft]);

    const mut = (fn) => { fn(); setDirty(true); setSavedAt(false); };
    const setRule = (i, r) => mut(() => setRules((rs) => rs.map((x, j) => (j === i ? r : x))));
    const addRule = () => mut(() => setRules((rs) => [...rs, { conditions: [newCondition("device")], destination_url: "" }]));
    const removeRule = (i) => mut(() => setRules((rs) => rs.filter((_, j) => j !== i)));
    const moveRule = (i, dir) => mut(() => setRules((rs) => { const a = rs.slice(); const j = i + dir; if (j < 0 || j >= a.length) return a; [a[i], a[j]] = [a[j], a[i]]; return a; }));
    // Recipes set the conditional rules only; the default stays the builder's destination.
    const applyRecipe = (r) => mut(() => setRules(r.rules.map(normalizeRule)));

    const addInt = async (type, url, forwardLocation, id) => {
      if (!link) return;
      const m = INT_META[type];
      // URL-based integrations require a delivery URL; the server rejects them without one.
      const cfg = { code: link.code, type, label: m.label };
      if (id) cfg.id = id; // editing an existing one → upsert by id
      if (url) cfg.url = url;
      if (forwardLocation) cfg.forward_location = forwardLocation; // 'raw' → precise GPS
      const created = await Api.setIntegration(cfg);
      // Replace if it already existed (edit), else append (add).
      setInts((xs) => [...xs.filter((x) => x.id !== created.id), created]);
    };
    const removeInt = async (i) => { await Api.deleteIntegration(i.id); setInts((xs) => xs.filter((x) => x.id !== i.id)); };

    // The default destination = the builder's destination (one shared value).
    const def = baseDestination || (link && ((link.routing && link.routing.default) || link.destination_url)) || "";
    // Dirty = the builder's design drifted from the saved baseline (name, dest,
    // content OR style — computed in the App and passed as `builderDirty`), the
    // routing rules changed, or it's a draft with a destination to create.
    const anyDirty = draft ? !!def : (dirty || !!builderDirty);

    // Save the WHOLE code at once: create it if this is a new draft, then persist
    // routing + destination + name. Not gated on `locked` — flows are Pro, but a
    // free user can still save their code's base destination/name.
    const save = async () => {
      if (!link && !draft) return;
      setSaving(true);
      try {
        // Upload an inline logo to Storage ONCE up front (returns the same style
        // with a Storage URL + path), so create+update reuse it — no re-upload churn.
        const upStyle = Api.uploadStyle ? await Api.uploadStyle(style) : style;
        const isNew = !link;
        // Set the passcode at creation when the code is brand-new (so it's
        // protected from the first scan); on an existing code, send it with the
        // update. `undefined` means "leave protection unchanged".
        const createPayload = { title: name || null, destination_url: def, content, style: upStyle };
        if (isNew && passcode !== undefined) createPayload.passcode = passcode;
        let l = link;
        if (isNew) { l = await Api.createLink(createPayload); setLink(l); }
        await Api.saveRouting(l.code, { rules, default: def });
        // Persist name + the authoring content (type + form) + visual style so the
        // whole code round-trips when re-opened.
        const updPayload = { code: l.code, title: name != null ? name : l.title, destination_url: def, content, style: upStyle };
        if (!isNew && passcode !== undefined) updPayload.passcode = passcode;
        const saved = await Api.updateLink(updPayload);
        // Reflect the post-save protection state: from createLink's response on a
        // new code, from updateLink's response when we changed it on an existing one.
        const protectedNow = !isNew && saved && saved.passcode_protected !== undefined
          ? saved.passcode_protected
          : !!(l && l.passcode_protected);
        const updated = Object.assign({}, l, { title: name != null ? name : l.title, destination_url: def || l.destination_url, content, style: upStyle, routing: { rules, default: def }, passcode_protected: protectedNow });
        setLink(updated); setDirty(false); setSavedAt(true);
        if (onSaved) onSaved(updated);
      } finally { setSaving(false); }
    };
    // Register the saver + report state up so the sticky topbar drives the save.
    if (saveRef) saveRef.current.save = save;
    React.useEffect(() => {
      if (onState) onState({ dirty: anyDirty, saving, canSave: anyDirty && (!!link || draft) });
    }, [anyDirty, saving, link, draft]);

    const result = React.useMemo(() => evalRouting(rules, def, ctx), [rules, def, ctx]);
    const base = link ? (link.destination_url || "").replace(/\/[^/]*$/, "") : "";
    const linkCode = link ? link.code : (code || "—");

    // Show loading only briefly; once the load settles (success OR failure) we
    // render the editor (empty if no link could load) so it never hangs.
    if (!loaded && !link) return <div className="fl-editor fl-editor--loading">{tr('mqr.flow.loading')}</div>;

    return (
      <div className="fl-editor">
        {/* Save lives in the sticky topbar (one save for the whole code). When
            locked (free), a non-covering upgrade banner sits above the visible,
            read-only editor so users see exactly what they'd unlock. */}
        {locked && (
          <div className="fl-upgrade">
            <span className="fl-upgrade__ic"><Icon name="zap" size={18} sw={2.2} /></span>
            <div className="fl-upgrade__txt">
              <div className="fl-upgrade__t">{tr('mqr.flow.gate.title')}</div>
              <div className="fl-upgrade__d">{tr('mqr.flow.gate.body')}</div>
            </div>
            <Button variant="primary" iconRight={<Icon name="arrow" size={15} />} onClick={onUpgrade}>{tr('mqr.flow.gate.cta')}</Button>
          </div>
        )}

        {!Api.ready && (
          <div className="ap-demo"><Icon name="infinity" size={15} sw={2.2} />
            {tr('mqr.flow.demo.1')} <code className="forge-mono">link.routing</code> {tr('mqr.flow.demo.2')}</div>
        )}

        <div className={"fl-editor__body" + (locked ? " is-locked" : "")} aria-hidden={locked ? "true" : undefined}>
          <div className="fl-layout">
            {/* The flow runs vertically DOWN from the scan: User scans QR ↓ rules ↓ default */}
            <div className="fl-canvas">
              {/* A subtle gradient that drops top→bottom once the scan beam
                  finishes its sweep — reads as the scan "firing" the flow. */}
              <span className="fl-flowpulse" aria-hidden="true" />
              <div className="fl-node fl-node--scan">
                <span className="fl-scanbeam" aria-hidden="true" />
                <span className="fl-node__ic"><Icon name="scan" size={16} sw={2} /></span>
                <span className="fl-node__txt">
                  <span className="fl-node__t">{tr('mqr.flow.node.scan')}</span>
                  <span className="fl-node__u">{tr('mqr.flow.node.scan.url', { code: linkCode })}</span>
                </span>
              </div>
              <div className="fl-wire fl-wire--h" />
              <div className="fl-route">
                {rules.length === 0 ? (
                  <RecipeGallery base={base} onPick={applyRecipe} />
                ) : (
                  <div className="fl-stack">
                    {rules.map((r, i) => (
                      <RuleCard key={i} rule={r} index={i} total={rules.length} hit={result.idx === i}
                        onChange={(nr) => setRule(i, nr)} onMove={moveRule} onRemove={removeRule} />
                    ))}
                  </div>
                )}

                <div className="fl-wire" />
                <button className="fl-addrule" onClick={addRule}><Icon name="plus" size={14} sw={2.2} /> {tr('mqr.flow.addrule')}</button>

                <div className="fl-wire" />
                <div className="fl-node fl-node--default">
                  <span className="fl-node__lbl"><span className="fl-node__ic"><Icon name="infinity" size={14} sw={2.2} /></span>{tr('mqr.flow.default.label')}</span>
                  {/* The default destination IS the builder's destination — one shared
                      value. Editable here too (writes straight back to the builder)
                      for URL-bearing codes; read-only for vcard/wifi (no URL). */}
                  {onDefaultChange ? (
                    <input className="fl-input fl-default-input" value={defaultValue || ""}
                      placeholder={tr('mqr.flow.default.placeholder')}
                      onChange={(e) => onDefaultChange(e.target.value)} />
                  ) : (
                    <span className="fl-default-ro" title={tr('mqr.flow.default.synced')}>
                      {def ? def.replace(/^https?:\/\//, "") : tr('mqr.flow.default.placeholder')}
                      <span className="fl-default-ro__hint">{tr('mqr.flow.default.synced')}</span>
                    </span>
                  )}
                </div>
              </div>

              <div className="fl-wire" />
              <IntegrationsLane ints={ints} onAdd={addInt} onRemove={removeInt} />
            </div>

            <div className="fl-aside">
              <Simulator rules={rules} def={def} ctx={ctx} setCtx={setCtx} result={result} />
            </div>
          </div>
        </div>
      </div>
    );
  }

  window.MQRFlow = { FlowEditor, evalRouting };
})();
