/* ──────────────────────────────────────────────────────────────────────
   capability-grid.jsx — perms v2 (feedback b030cab9)
   ──────────────────────────────────────────────────────────────────────
   Apple-style toggle grid for the Invite + User-detail flows. Capabilities
   are grouped by category (Campaigns, Species, Displays/Zones, …). Toggles
   the inviter doesn't hold are greyed and disabled with a hover tooltip
   ("You don't have this capability, so you can't grant it.") — the funnel.

   The component is data-driven: it fetches /api/capabilities on mount and
   accepts a controlled `value` (array of cap ids) + onChange.

   Templates are loaded from /api/capabilities/templates and shown in a
   dropdown at the top. Picking a template fills all toggles from that set;
   "Save as template" persists the current selection as an org-scoped
   template (or platform-default if the caller is aquaos_admin).
   ────────────────────────────────────────────────────────────────────── */

/* Tiny ARIA-correct switch — matches the visual language of the existing
   chunky permission toggles in users.jsx but is locked to a single binary
   value (atomic capabilities are pure on/off, no none/view/edit/approve
   ladder). Disabled state shows a half-opacity track + tooltip on hover. */
function CapToggle({ id, checked, disabled, onChange, label, helper, disabledReason }) {
  const [hover, setHover] = useState(false);
  const trackBg = checked
    ? 'var(--aq-accent)'
    : 'color-mix(in srgb, var(--aq-text-faint) 30%, transparent)';
  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: '1fr auto',
        alignItems: 'center',
        gap: 12,
        padding: '10px 12px',
        borderRadius: 8,
        background: hover && !disabled ? 'var(--aq-surface-2)' : 'transparent',
        opacity: disabled ? 0.55 : 1,
        cursor: disabled ? 'not-allowed' : 'pointer',
        position: 'relative',
      }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      onClick={() => { if (!disabled) onChange(!checked); }}
      title={disabled ? disabledReason : undefined}
    >
      <div>
        <div style={{ fontSize: 12.5, color: 'var(--aq-text)' }}>{label}</div>
        {helper && (
          <div style={{ fontSize: 11, color: 'var(--aq-text-faint)', marginTop: 2 }}>{helper}</div>
        )}
        {disabled && hover && disabledReason && (
          <div style={{
            position: 'absolute', left: 12, bottom: -6, transform: 'translateY(100%)',
            zIndex: 5, padding: '6px 8px', borderRadius: 6,
            background: 'var(--aq-surface)', border: '1px solid var(--aq-line)',
            fontSize: 11, color: 'var(--aq-text-muted)',
            boxShadow: '0 6px 16px rgba(0,0,0,0.35)', maxWidth: 240,
          }}>{disabledReason}</div>
        )}
      </div>
      <button
        type="button"
        role="switch"
        aria-checked={checked ? 'true' : 'false'}
        aria-label={label}
        disabled={disabled}
        onClick={(e) => { e.stopPropagation(); if (!disabled) onChange(!checked); }}
        style={{
          width: 38, height: 22, padding: 0, borderRadius: 999,
          border: '1px solid var(--aq-line)',
          background: trackBg,
          position: 'relative',
          cursor: disabled ? 'not-allowed' : 'pointer',
          transition: 'background 0.15s ease',
        }}
      >
        <span style={{
          position: 'absolute', top: 2, left: checked ? 18 : 2,
          width: 16, height: 16, borderRadius: '50%',
          background: 'white',
          boxShadow: '0 1px 3px rgba(0,0,0,0.4)',
          transition: 'left 0.15s ease',
        }} />
      </button>
    </div>
  );
}

/* The grid itself.
   Props:
     value           - array of capability ids the form currently has selected
     onChange(arr)   - controlled change handler
     forRole         - optional role string. If set, "Reset to role default"
                       button restores the role baseline (capped by granter).
     hideTemplates   - hide the templates picker (e.g. detail page)
     hideSaveAs      - hide the "Save as template" button
     compact         - tighter padding for inline use in the invite modal
*/
function CapabilityGrid({
  value, onChange, forRole, hideTemplates, hideSaveAs, compact,
}) {
  const [catalogue, setCatalogue] = useState(null);
  const [granterCaps, setGranterCaps] = useState([]);
  const [callerRole, setCallerRole] = useState(null);
  const [roleDefaults, setRoleDefaults] = useState({});
  const [templates, setTemplates] = useState([]);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState(null);

  /* Load catalogue + caller's effective set + visible templates in parallel.
     The catalogue rarely changes so it's safe to cache for the session, but
     we re-fetch on mount for now — premature optimisation otherwise. */
  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    Promise.all([
      apiFetch('/api/capabilities'),
      apiFetch('/api/capabilities/templates'),
    ]).then(([catRes, tplRes]) => {
      if (cancelled) return;
      setCatalogue(catRes.catalogue || []);
      setGranterCaps(catRes.caller_capabilities || []);
      setCallerRole(catRes.caller_role || null);
      setRoleDefaults(catRes.role_defaults || {});
      setTemplates(tplRes.templates || []);
      setLoading(false);
    }).catch((e) => {
      if (cancelled) return;
      setErr(e.message);
      setLoading(false);
    });
    return () => { cancelled = true; };
  }, []);

  /* When the parent passes a new role (e.g. inviter changed the role badge),
     pre-fill the grid with the role baseline capped by the granter. We only
     do this if the value is currently empty — never clobber explicit edits. */
  useEffect(() => {
    if (!forRole || !roleDefaults || !roleDefaults[forRole]) return;
    if (Array.isArray(value) && value.length > 0) return;
    const baseline = roleDefaults[forRole] || [];
    const granterSet = new Set(granterCaps);
    const capped = (callerRole === 'aquaos_admin')
      ? baseline.slice()
      : baseline.filter(c => granterSet.has(c));
    onChange(capped);
  }, [forRole, roleDefaults, granterCaps, callerRole]); // eslint-disable-line

  const granterSet = useMemo(() => new Set(granterCaps), [granterCaps]);
  const isAquaOSAdmin = callerRole === 'aquaos_admin';

  const grouped = useMemo(() => {
    if (!catalogue) return [];
    const byCat = new Map();
    for (const c of catalogue) {
      if (!byCat.has(c.category)) byCat.set(c.category, []);
      byCat.get(c.category).push(c);
    }
    return Array.from(byCat.entries()); // [[category, caps[]], …]
  }, [catalogue]);

  function toggle(id, on) {
    const set = new Set(value || []);
    if (on) set.add(id); else set.delete(id);
    onChange(Array.from(set));
  }

  function applyTemplate(tplId) {
    if (!tplId) return;
    const tpl = templates.find(t => t.id === tplId);
    if (!tpl) return;
    /* Templates are stored uncapped (the funnel is enforced at write time
       on the server side), so cap on apply for the UI to feel honest. */
    const capped = isAquaOSAdmin
      ? (tpl.capabilities || []).slice()
      : (tpl.capabilities || []).filter(c => granterSet.has(c));
    onChange(capped);
  }

  function resetToRoleDefault() {
    if (!forRole || !roleDefaults[forRole]) return;
    const baseline = roleDefaults[forRole];
    const capped = isAquaOSAdmin
      ? baseline.slice()
      : baseline.filter(c => granterSet.has(c));
    onChange(capped);
  }

  /* Save current selection as a new template. Prompts for a name, then POSTs
     to /api/capabilities/templates. We use prompt() for now — proper modal
     can come later, but the affordance is in the toolbar. */
  async function saveAsTemplate() {
    const name = (window.prompt('Template name', forRole ? `${forRole} (custom)` : 'Custom set') || '').trim();
    if (!name) return;
    try {
      const res = await apiFetch('/api/capabilities/templates', {
        method: 'POST',
        body: JSON.stringify({ name, capabilities: value || [] }),
      });
      setTemplates((t) => [...t, res]);
      window.toast && window.toast.success(`Template "${name}" saved`);
    } catch (e) {
      window.toast && window.toast.error(e.message);
    }
  }

  if (loading) {
    return <div style={{ padding: 14, color: 'var(--aq-text-faint)', fontSize: 12.5 }}>Loading capabilities…</div>;
  }
  if (err) {
    return (
      <div style={{
        padding: '10px 12px', borderRadius: 7, fontSize: 12,
        background: 'color-mix(in srgb, var(--aq-danger) 12%, transparent)',
        color: 'var(--aq-danger)',
      }}>{err}</div>
    );
  }

  const value0 = new Set(value || []);
  const platformTpls = templates.filter(t => t.organisation_id === null);
  const orgTpls = templates.filter(t => t.organisation_id !== null);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      {/* Toolbar — templates picker + reset + save-as-template */}
      <div style={{
        display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center',
        padding: compact ? '8px 0' : '10px 12px',
        borderBottom: '1px solid var(--aq-line)',
      }}>
        {!hideTemplates && (
          <select
            onChange={(e) => { applyTemplate(e.target.value); e.target.value = ''; }}
            defaultValue=""
            style={{
              padding: '6px 8px', fontSize: 12, borderRadius: 6,
              background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
              color: 'var(--aq-text)', font: 'inherit',
            }}
          >
            <option value="" disabled>Apply template…</option>
            {platformTpls.length > 0 && (
              <optgroup label="Platform defaults">
                {platformTpls.map(t => (
                  <option key={t.id} value={t.id}>{t.name}</option>
                ))}
              </optgroup>
            )}
            {orgTpls.length > 0 && (
              <optgroup label="Org templates">
                {orgTpls.map(t => (
                  <option key={t.id} value={t.id}>{t.name}</option>
                ))}
              </optgroup>
            )}
          </select>
        )}
        {forRole && (
          <button type="button" className="x-btn ghost" style={{ fontSize: 11.5 }} onClick={resetToRoleDefault}>
            Reset to {forRole} default
          </button>
        )}
        <div style={{ flex: 1 }} />
        {!hideSaveAs && (
          <button type="button" className="x-btn ghost" style={{ fontSize: 11.5 }} onClick={saveAsTemplate}>
            Save as template…
          </button>
        )}
      </div>

      {/* Capability groups */}
      {grouped.map(([category, caps]) => (
        <section key={category} style={{ display: 'flex', flexDirection: 'column' }}>
          <div style={{
            fontSize: 10.5, letterSpacing: 0.6, textTransform: 'uppercase',
            color: 'var(--aq-text-faint)', padding: '2px 12px 6px',
          }}>{category}</div>
          <div style={{
            display: 'flex', flexDirection: 'column', gap: 2,
            border: '1px solid var(--aq-line)', borderRadius: 8,
            background: 'var(--aq-surface)',
          }}>
            {caps.map((cap, i) => {
              const held = isAquaOSAdmin || granterSet.has(cap.id);
              return (
                <React.Fragment key={cap.id}>
                  {i > 0 && <div style={{ height: 1, background: 'var(--aq-line)', margin: '0 12px' }} />}
                  <CapToggle
                    id={cap.id}
                    checked={value0.has(cap.id)}
                    disabled={!held}
                    onChange={(on) => toggle(cap.id, on)}
                    label={cap.label}
                    helper={cap.description}
                    disabledReason="You don't have this capability, so you can't grant it."
                  />
                </React.Fragment>
              );
            })}
          </div>
        </section>
      ))}
    </div>
  );
}

/* Expose globally so other files (users.jsx, capability-templates.jsx, app.jsx)
   can mount it. The CMS uses a single global namespace because the codebase
   compiles JSX in-browser and has no module system. */
window.CapabilityGrid = CapabilityGrid;
window.CapToggle = CapToggle;
